Skip to content

fix(security): unconditional hard-block for critical destructive commands (sable-nf2i) - #181

Merged
Rome-1 merged 1 commit into
mainfrom
fix/sable-nf2i-rm-rf-hardblock
Jun 20, 2026
Merged

fix(security): unconditional hard-block for critical destructive commands (sable-nf2i)#181
Rome-1 merged 1 commit into
mainfrom
fix/sable-nf2i-rm-rf-hardblock

Conversation

@Rome-1

@Rome-1 Rome-1 commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes sable-nf2i (P1 security): CommandInterceptor.evaluate() returned {allowed:true, requiresApproval:false, riskLevel:'critical'} for catastrophic destructive commands (rm -rf /, rm -fr /, fork bombs, dd to disk, mkfs, …) whenever no commandPolicy was configured — the real-world default, since a user's ~/.rafter/config.json often omits agent.commandPolicy.

The pretool hook denies only when (!allowed && !requiresApproval) || requiresApproval, so allowed=true + requiresApproval=false bypassed the hard-block → a security CLI that did not block rm -rf / by default.

Fix

Add an unconditional critical-destructive hard-block at the very top of evaluate(), before any policy is loaded, in both implementations:

  • Critical commands always return allowed:false, requiresApproval:false, surfacing the matched built-in pattern — regardless of whether a policy exists or which mode is set. allow-all and custom deny-lists can no longer opt out of catastrophic commands (closing the trivial bypass).
  • With no policy configured, high-risk commands now fall back to requiring approval instead of allow-all.
  • New helpers matchedCriticalPattern() / match_critical_pattern() report which built-in rule matched.

Contract change (intentional)

Previously allow-all mode (and custom deny-lists that replaced defaults, and approve-dangerous for critical) permitted / merely-prompted rm -rf /. Now critical-destructive commands are always hard-blocked. The policy-mode tests that encoded the old behavior were updated to assert the hard-block.

Verification

  • Repairs the 13 red command-interceptor.test.ts cases, the hook-integration "blocks rm -rf / even in a git repo" case, and the two error-handling-gauntlet interceptor cases.
  • Node: full suite 1926 passed / 9 skipped. The 3 remaining failures are pre-existing & unrelated (version-parity stale artifact, AuditLogger JSONL, openclaw flaky) — confirmed on a clean tree. Beaded: sable-lcch, sable-qqmc, sable-j3ic.
  • Python: full suite 1443 passed. The 1 failure is the same pre-existing version-artifact issue (sable-lcch).
  • Exact node/python parity verified: 16/16 commands identical decisions under the same config.
  • No ReDoS (50–100k-char pathological inputs classify in single-digit ms; helper reuses existing patterns, runs only post-classification).

Security gate (per CLAUDE.md)

  • rafter-code-review walked (CWE Top-25 / CLI): no new findings; surfaced 2 pre-existing items (sable-619t over-block, sable-c3w1 config-default parity gap).
  • rafter secrets clean on source changes.
  • WARNING: rafter run (remote SAST/SCA) not run locally — RAFTER_API_KEY not set in this env. Please ensure CI runs it on this PR.

Closes sable-nf2i.

🤖 Generated with Claude Code

…ands (sable-nf2i)

CommandInterceptor.evaluate() returned {allowed:true, requiresApproval:false}
for critical destructive commands (rm -rf /, fork bombs, dd to disk, mkfs, …)
whenever no commandPolicy was configured — which is the real-world default
(the user's ~/.rafter/config.json often omits agent.commandPolicy). The pretool
hook only denies on (!allowed && !requiresApproval) || requiresApproval, so
allowed=true+approval=false sailed straight through: a security CLI that did not
block `rm -rf /` by default.

Fix: add an UNCONDITIONAL critical-destructive hard-block at the very top of
evaluate(), before any policy is loaded, in both implementations. Critical
commands now always return allowed=false, requiresApproval=false — independent
of whether a policy exists or which mode is set (allow-all and custom deny-lists
can no longer opt out of catastrophic commands). With no policy configured,
high-risk commands now fall back to requiring approval instead of allow-all.

- node/src/core/risk-rules.ts: add matchedCriticalPattern() helper
- python/rafter_cli/core/risk_rules.py: add match_critical_pattern() helper
- both interceptors: hard-block keyed on assessCommandRisk()=="critical",
  surfacing the matched built-in pattern; exact node/python parity verified
  (16/16 commands identical under the same config)

Updated the policy-mode tests that encoded the old (insecure) contract
(allow-all / custom deny-list / approve-dangerous permitting critical commands)
to assert the hard-block. Repairs the 13 red command-interceptor.test.ts cases,
the hook-integration "blocks rm -rf / even in a git repo" case, and the two
error-handling-gauntlet interceptor cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Rome-1
Rome-1 merged commit 4cb78d7 into main Jun 20, 2026
13 checks passed
@Rome-1
Rome-1 deleted the fix/sable-nf2i-rm-rf-hardblock branch June 20, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant