reason=$(printf '%s' "$result" | python3 -c \
Root cause. The engine contract (integration.md:412) is: in warn mode the engine downgrades BLOCK → ALLOW and carries the warning text in the bridge JSON reason field. The CLI (/usr/local/bin/terminal-jail-sh, and the standalone variant at /usr/local/lib/terminal-jail/standalone/terminal-jail) dispatched with if block / elif modify / fi — the implicit else (allow) branch was empty, so the downgraded warning was dropped: the command passed through with no stderr. Enforce mode was unaffected because there the engine returns action=block and the block branch prints.
Fix. Add an else-branch that prints the engine's reason to stderr only when TERMINAL_JAIL_INTERRUPTOR_MODE=warn and a reason is present (so benign allow in warn mode stays silent and no false positives occur).
For terminal-jail-sh (and the equivalent block in any CLI copy):
if [[ "$action" == "block" ]]; then
rule_id=$(printf '%s' "$result" | python3 -c \
"import json,sys; print(json.load(sys.stdin).get('rule_id') or 'unknown')" \
2>/dev/null) || rule_id="unknown"
reason=$(printf '%s' "$result" | python3 -c \
"import json,sys; print(json.load(sys.stdin).get('reason') or 'Blocked by security policy')" \
2>/dev/null) || reason="Blocked by security policy"
printf 'terminal-jail: command blocked (%s): %s\n' "$rule_id" "$reason" >&2
exit 126
elif [[ "$action" == "modify" ]]; then
modified=$(printf '%s' "$result" | python3 -c \
"import json,sys; print(json.load(sys.stdin).get('modified') or '')" \
2>/dev/null) || modified=''
[[ -n "$modified" ]] && cmd="$modified"
else
# action=allow. Warn mode downgrades BLOCK->ALLOW and keeps the
# warning in `reason`; surface it (integration.md:412).
if [[ "$MODE" == "warn" ]]; then
reason=$(printf '%s' "$result" | python3 -c \
"import json,sys; print(json.load(sys.stdin).get('reason') or '')" \
2>/dev/null || true)
[[ -n "$reason" ]] && \
printf 'terminal-jail: WARN — would have blocked: %s\n' "$reason" >&2
fi
fi
fi
Same fix for the standalone variant's dispatch (its elif modify closes into a new else):
elif [ "$action" = "modify" ]; then
modified_cmd=$(...)
echo "[terminal-jail] Modified: ${CMD_STR:0:40}... → sandboxed" >&2
CMD_STR="$modified_cmd"
set -- bash -c "$CMD_STR"
else
# warn mode: engine downgraded BLOCK->ALLOW; reason holds the warning
if [ "$INTERRUPTOR_MODE" = "warn" ]; then
reason=$(echo "$bridge_result" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('reason','') or '')" 2>/dev/null || echo "")
if [ -n "$reason" ]; then
echo "[terminal-jail] WARN: Would have blocked: $reason" >&2
fi
fi
fi
Design notes: the warning is emitted even though the command still executes (pass-through preserved, warn mode semantics unchanged); the reason gate prevents noise for genuinely benign commands; MODE == warn gate means enforce/disabled are byte-for-byte unchanged.
Reproduced in `~/tj-e2e` with a mock engine bridge emulating the contract (warn mode returns `{"action":"allow","rule_id":...,"reason":"[warn] would have blocked (...): ..."}`).
**Bug reproduced (before fix, warn + `rm -rf /tmp/foo`):**
```
$ TERMINAL_JAIL_INTERRUPTOR_MODE=warn ./cli/before.sh -c "rm -rf /tmp/foo"
(no warning on stderr — silent pass-through; only the jail launcher's own
unshare error appears in this container)
```
**After fix, same probe:**
```
$ TERMINAL_JAIL_INTERRUPTOR_MODE=warn ./cli/after.sh -c "rm -rf /tmp/foo"
terminal-jail: WARN — would have blocked: [warn] would have blocked (rule-fs-destructive): destructive filesystem command blocked
```
Regression suite (`run_tests.sh`, dry-run variants isolate the bridge-dispatch layer from the jail launcher, which this container can't run due to missing `unshare --user` permission — pre-existing infra, unrelated to tick #77):
| # | Probe | Result |
|---|-------|--------|
| T1 | warn + would-block → WARN + reason on stderr | PASS |
| T2 | control: buggy CLI is silent | PASS |
| T3 | enforce + would-block → block msg, exit 126 | PASS |
| T4 | warn + benign → no spurious warning | PASS |
| T5 | enforce + benign → clean pass-through | PASS |
| T6 | disabled + would-block → no warning | PASS |
| T7 | warn + modify → no stray else-warning, still rewritten | PASS |
| T8 | `bash -n` on fixed CLI | PASS |
Edge cases covered: empty/absent `reason` (no print), `modify` action not double-printing, disabled mode skipping the bridge entirely, enforce-mode block path unchanged (exit 126 + reason), benign commands silent in warn mode. `tests=8 passed=8 failed=0`.{"model": "deepseek-v4-flash", "problem_class": "cli-warn-mode-silent-passthrough", "result": "passed", "tests": 8}