Skip to content

Hard-block rm by default in both harnesses, no prompt - #56

Merged
ankitsingh015 merged 1 commit into
mainfrom
claude/rm-deny-hardblock
Aug 27, 2026
Merged

Hard-block rm by default in both harnesses, no prompt#56
ankitsingh015 merged 1 commit into
mainfrom
claude/rm-deny-hardblock

Conversation

@ankitsingh015

Copy link
Copy Markdown
Owner

Kya kiya

User ne kaha tha: rm command disallow karo by default, na pooche, dono (Claude Code + OpenCode) me, baaki allow karo.

  • Claude Code: .claude/settings.json me permissions.deny (Bash(rm *), Bash(rm)) -- pehle se hi verified working (hard deny, no prompt).
  • OpenCode: opencode.jsonc/agent frontmatter ka declarative permission.bash glob-deny (rm ** deny + * allow) empirically kaam nahi kar raha tha -- multiple pattern/ordering hypotheses live-tested, sab fail. Real fix: scripts/hooks/scope_gate_hook.py me ek naya unconditional _is_rm_command() check add kiya -- scope/engagement state se independent, chained commands (semicolons, &&, ||, pipes, subshells) ke through bhi catch karta hai. .opencode/plugin/scope-gate.ts already har Bash call pe yehi script invoke karta hai, so ye fix free me inherit ho gaya.

Verification

  • Live test via real opencode run (huntbrain to recon-agent subagent): rm attempt on a scratch file was hard-blocked, file survived on disk.
  • Same test under opencode run --auto (OpenCode's bypass-permissions mode, jo declarative permission block ko skip karta hai) -- still blocked. Proof ye hook-level enforcement hai, permission-glob-level nahi.
  • 19 new tests in tests/test_scope_gate_hook.py: unit tests for the rm-detection helper (chaining, sudo/env prefix, absolute path, false-positive guards for a bare ")" and a lone backtick from markdown text -- the second guard caught a real bug in this PR's own first draft) plus end-to-end main() tests confirming the block fires with zero engagement/scope state.
  • pytest tests/ -> 249 passed, 4 pre-existing failures (documented, unrelated -- real active engagement on this machine collides with scope_guard.DEFAULT_PATH's import-time binding, zero touched lines near that codepath).
  • python -m py_compile scripts/hooks/scope_gate_hook.py clean.

Non-functional declarative rm-deny glob entries kept as documented defense-in-depth (harmless, comment points at the real enforcement point).

🤖 Generated with Claude Code

Claude Code: .claude/settings.json permissions.deny (Bash(rm *)/Bash(rm))
already blocks rm outright, no prompt -- verified live.

OpenCode: opencode.jsonc's declarative permission.bash glob deny
("rm **": deny alongside "*": allow) does NOT actually block rm in
practice -- confirmed empirically across several pattern/ordering
attempts. The real fix is scripts/hooks/scope_gate_hook.py: a new
unconditional _is_rm_command() check (independent of scope/engagement
state entirely, chain-aware across ;/&&/||/|/subshells/backticks so it
can't be smuggled in as the second half of a chained command) that
blocks any Bash command whose (sub-)command is rm. .opencode/plugin/
scope-gate.ts already invokes this exact script on every Bash call via
OpenCode's tool.execute.before hook, so it inherits this for free --
confirmed live via a real opencode run through huntbrain -> recon-agent,
including under --auto (OpenCode's bypass-permissions mode), which
skips the declarative permission.bash block entirely but still hit this
hook-level block.

The now-nonfunctional declarative rm-deny glob entries in opencode.jsonc
and the three .opencode/agents/*.md files are kept as documented
defense-in-depth (harmless, and would help if a future OpenCode version
fixes the glob matching), with a comment pointing at the real
enforcement point so nobody mistakes them for the actual guarantee.

17 new tests in tests/test_scope_gate_hook.py cover _is_rm_command()
directly (chained commands, sudo/env prefixes, absolute paths, and the
false-positive case of a bare ')' from unrelated command text) plus two
end-to-end main() tests confirming the block fires with no
engagement.yaml and no in-scope host at all -- this is a blanket rule,
not a scope rule.

4 pre-existing test_scope_gate_hook.py failures remain, caused by a
real active engagement on this machine colliding with
scope_guard.DEFAULT_PATH's import-time binding (documented, unrelated
to this change -- zero of the touched lines are anywhere near that
codepath).
@ankitsingh015
ankitsingh015 merged commit 6509e89 into main Aug 27, 2026
8 checks passed
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