Escalation Logic
How severity, context, and policy layers combine — and why escalation can only go up
shellfirm determines the effective challenge type through a multi-layer escalation pipeline. Severity, per-group/per-check overrides, runtime context, and project policies all contribute — and each layer can only make challenges harder, never easier. This page explains the full logic.
The full escalation pipeline
The effective challenge is resolved through six layers, applied in order. Each layer can only raise the challenge, never lower it:
| Layer | Source | When it applies |
|---|---|---|
| 1. Base | settings.challenge | Always — your configured default (e.g., Math) |
| 2. Severity | Check severity level | Critical → Yes, High → Enter (by default) |
| 3. Group | group_escalation in settings | When the matched check's group has an override |
| 4. Check ID | check_escalation in settings | When the matched check's ID has an override |
| 5. Context | Runtime environment signals | SSH → Enter, root/production → Yes |
| 6. Policy | .shellfirm.yaml overrides | When the project policy overrides the check |
effective = max(base, severity_floor, group_floor, check_floor, context_floor, policy_floor)
Risk level computation
Context signals (layer 5) are evaluated in priority order. The first Critical signal wins:
The algorithm short-circuits: if root is detected, the risk level is immediately Critical regardless of other signals. But all signals are still detected and recorded as labels for visibility.
Risk levels
| Level | Meaning | Default challenge floor |
|---|---|---|
| Normal | No risk signals | Math (your configured default) |
| Elevated | Moderate risk (SSH) | Enter |
| Critical | High risk (root, production branch, production k8s, production env) | Yes |
Challenge escalation order
shellfirm has three challenge types, ordered by difficulty:
Math < Enter < Yes
- Math -- solve a simple arithmetic problem (e.g., "7 + 3 = ?")
- Enter -- press Enter to confirm
- Yes -- type the word "yes" to confirm
How escalation works
Each layer applies the max() function — the stricter of the current challenge and the layer's floor:
effective = max(current, layer_floor)
Examples with the full pipeline
| Check severity | Base | After severity | Context | After context | Result |
|---|---|---|---|---|---|
| High | Math | Enter | Normal | Enter | Enter |
| High | Math | Enter | Critical | Yes | Yes |
| Critical | Math | Yes | Normal | Yes | Yes |
| Medium | Math | Math | Elevated | Enter | Enter |
| Critical | Math | Yes | Elevated | Yes | Yes |
| Low | Enter | Enter | Normal | Enter | Enter |
Notice that escalation never lowers the challenge. If severity already set the challenge to Yes, context escalation to Enter will not lower it.
The security invariant
This is the fundamental guarantee:
Escalation can only go up, never down.
This invariant applies everywhere in shellfirm:
- Severity escalation -- check severity sets a challenge floor (Critical→Yes, High→Enter)
- Group/check-id escalation -- user-configured overrides can only raise the floor
- Context escalation -- risk level can only raise the challenge floor
- Policy overrides -- team policies can only escalate challenge types, never downgrade them
- LLM analysis -- LLM can only flag additional risks, never dismiss existing ones
- Deny lists -- patterns can be added to deny lists but never removed by policies
Multiple signals stacking
When multiple signals are detected, they all contribute to the risk level. Since any Critical signal produces Critical risk, additional signals do not further increase the risk level (there is nothing above Critical). However, all signals are recorded as labels for transparency:
The risk level is Critical (same as with just one Critical signal), but the full context is visible in the challenge banner and audit log.
Configuring escalation
The mapping from risk levels to challenge types is configurable:
# In ~/.shellfirm/settings.yaml
context:
escalation:
elevated: Enter # default: Enter
critical: Yes # default: Yes
You can make Elevated stricter:
context:
escalation:
elevated: Yes # SSH sessions now require "yes" confirmation
critical: Yes
Or make it more lenient (though this weakens protection):
context:
escalation:
elevated: Math # SSH sessions use Math (no escalation)
critical: Enter # Root/production use Enter instead of Yes
Full example walkthrough
Consider this scenario: you SSH into a production server, switch to root, and run git push --force on the main branch with NODE_ENV=production.
Detected signals:
- SSH session (
SSH_CONNECTIONpresent) -- would be Elevated - Root user (
EUID=0) -- Critical - Protected branch (
main) -- Critical - Production env (
NODE_ENV=production) -- Critical
Risk level: Critical (first Critical signal from root short-circuits)
Labels: ssh=true, root=true, branch=main, NODE_ENV=production
Escalation pipeline for git push --force (High severity):
- Base: Math (your configured default)
- Severity:
max(Math, Enter) = Enter(High severity → Enter) - Group/Check-ID: no overrides configured → Enter
- Context:
max(Enter, Yes) = Yes(Critical risk level → Yes) - Policy: no overrides → Yes
Result: Yes — you must type "yes" to confirm. The challenge banner shows all four context labels so you understand exactly why shellfirm is being extra cautious.