Repository navigation
Conversation
…er-command guards The owner found the per-command hooks slow and glitchy; the measured cost was not latency (50-90 ms each, mostly Python start-up) but refusals, each costing a model turn and a retry. Removed: git-guardrails.py and root-of-trust-guard.py (PreToolUse on every Bash call, with onFailure: block) and context-monitor.py (PostToolUse on every Bash call). Their files stay at tag v2.9.0. Added: - permissions.deny in .claude/settings.json (applied by the owner): git reset --hard, git clean, push --force / -f (not --force-with-lease), add -A / --all / ., checkout -- . / checkout . / restore ., plus .env, *.pem, *.key and credentials* reads. Verified live in a scratch repo: every rule refused its command, including (git reset --hard) and $(git reset --hard), which were open as #172; every control ran. - .claude/hooks/dirty-tree-guard.py: started only through if filters for git merge / pull / rebase; refuses while tracked files have uncommitted changes; untracked files, resume steps and ALLOW_DIRTY_MERGE=1 pass; fails open except when its own file is missing. Battery section j (16 cases); six mutants each fail their own cases. Battery 322 -> 110 cases, grading-the-grader re-measured for the remaining decision hooks. Portability 245 -> 130 (test_rot.py and the removed hooks' cases gone; the Windows code-page cases moved to dirty-tree-guard). The ledger moves the removed hooks' rows and notes, unedited, to "Retired at v2.9.1", with their counts reading "at the time". The status line keeps only Claude Code's own context percentage. README, guide (re-rendered, restamped), TROUBLESHOOTING, docs/index.html, AGENTS.md, SECURITY.md, confidential-data.md (CLAUDE_STRICT_PATHS is gone) and four skills updated. CHANGELOG v2.9.1 leads with the upgrade steps for forks that changed their settings.json; guide and CITATION at 2.9.1.
Codex Review Summary: ❌ FailedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…and launcher patch prepared for the owner Codex CLI review of PR #188 (the cloud review hit its usage limit), eight findings, all confirmed against the code: Fixed here: - the hook searched raw text, so a --abort in a comment or a -m message exempted a real merge, a quoted 'git pull origin' inside a grep was refused, and redirections confused both checks. It now tokenizes with shlex (quotes, comments, ; & | ( ), < and > split off but not ending the command), re-reads $(...) and backticks outside single quotes, skips heredoc bodies, and walks VAR=value, wrappers and git's global options. Found live as well: the hook refused one of this session's own commands, a heredoc whose text held a backticked pull. Battery j17-j29, 123 cases; fifteen mutants each fail their own cases. - the locale-decoding portability case used a clean tree, where a fail-open decoding error also reads as silence; it now uses a dirty tree and requires a deny (a locale-decoding mutant fails it; an unmodified copy passes). Prepared for the owner (settings.json is owner-edited only), and described in the docs as the state this PR merges with: - the deny list missed variants v2.9.0's git-guardrails denied (restore -- ., restore --worktree ., checkout HEAD -- ., add -v -A, add -v --all, add file .) and refused two ordinary commands (push origin feature--force, reset -- tracked--hard.txt). The new list matches whole option words in any position: 30 refused, 24 allowed, 0 wrong in a matcher built from the documented rule semantics. - a registered hook whose file is missing makes python3 exit 2, which refused every command the if filter starts the hook for, including any with $VAR. The registration now goes through sh -c '[ -f "$1" ] || exit 0; exec python3 "$1"', which the ledger-coverage gate already parses.
|
Review. The cloud Codex review hit its usage limit, so I ran the Codex CLI locally (
The hook rewrite also fixes a false refusal I hit live in this session. The hook refused one of my own commands because a heredoc's text held a backticked What backs the fixes in 6311448:
Holding the merge until the patch below is applied. Then I re-run the live check in a scratch repository and merge. The docs on this branch already describe the patched state. To apply, from the repository root on branch --- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -11,18 +11,31 @@
"Read(**)"
],
"deny": [
- "Bash(git reset *--hard*)",
+ "Bash(git reset --hard *)",
+ "Bash(git reset * --hard)",
+ "Bash(git reset * --hard *)",
"Bash(git clean *)",
- "Bash(git push *--force)",
- "Bash(git push *--force *)",
- "Bash(git push -f*)",
- "Bash(git push * -f*)",
- "Bash(git add -A*)",
- "Bash(git add --all*)",
+ "Bash(git push --force *)",
+ "Bash(git push * --force)",
+ "Bash(git push * --force *)",
+ "Bash(git push -f *)",
+ "Bash(git push * -f)",
+ "Bash(git push * -f *)",
+ "Bash(git add -A *)",
+ "Bash(git add * -A)",
+ "Bash(git add * -A *)",
+ "Bash(git add --all *)",
+ "Bash(git add * --all)",
+ "Bash(git add * --all *)",
"Bash(git add . *)",
+ "Bash(git add * .)",
+ "Bash(git add * . *)",
"Bash(git checkout -- . *)",
"Bash(git checkout . *)",
+ "Bash(git checkout * .)",
"Bash(git restore . *)",
+ "Bash(git restore * .)",
+ "Bash(git restore * . *)",
"Read(.env)",
"Read(.env.*)",
"Read(**/.env)",
@@ -49,19 +62,19 @@
{
"type": "command",
"if": "Bash(git merge *)",
- "command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/dirty-tree-guard.py",
+ "command": "sh -c '[ -f \"$1\" ] || exit 0; exec python3 \"$1\"' dirty-tree-guard \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/dirty-tree-guard.py",
"timeout": 10
},
{
"type": "command",
"if": "Bash(git pull *)",
- "command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/dirty-tree-guard.py",
+ "command": "sh -c '[ -f \"$1\" ] || exit 0; exec python3 \"$1\"' dirty-tree-guard \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/dirty-tree-guard.py",
"timeout": 10
},
{
"type": "command",
"if": "Bash(git rebase *)",
- "command": "python3 \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/dirty-tree-guard.py",
+ "command": "sh -c '[ -f \"$1\" ] || exit 0; exec python3 \"$1\"' dirty-tree-guard \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/dirty-tree-guard.py",
"timeout": 10
}
] |
Summary
The per-command guard hooks slowed everyday work and refused ordinary commands too often. Measured cost: 50–90 ms per hook, mostly Python start-up. The real cost was refusals, each costing a model turn and a retry. This release removes them and keeps the protection that mattered, using Claude Code's own permission rules.
Removed
git-guardrails.pyandroot-of-trust-guard.py, which ran before every Bash call withonFailure: block.context-monitor.py, which ran after every Bash call.v2.9.0, and the guide says how to restore one.Added
Native deny rules in
.claude/settings.json(applied by the owner):git reset --hard,git clean,git push --forceand-f(not--force-with-lease),git add -A,--alland.,git checkout -- .,git checkout .andgit restore .;.env,*.pem,*.keyandcredentials*.Claude Code checks them itself. They also apply inside
( )and$(), which closes git-guardrails misses destructive git inside ( … ), $( … ) and backticks #172. They do not match rewritten spellings such asgit -C . push, and every doc that mentions them says so.dirty-tree-guard.py:git merge,git pullandgit rebase(iffilters), and refuses while tracked files have uncommitted changes;--abort/--continue, andALLOW_DIRTY_MERGE=1pass;Changed
issue-guardis the only fail-closed hook left, and it runs only forghcommands. Windows hosts without a working python3: every guard hook fails to start, and since v2.7.0 that blocks every Bash, Edit and Write #179 and A missing hook script blocks every Bash, Edit and Write, and the session cannot repair itself #180 are resolved by the removal.CLAUDE_STRICT_PATHSis gone.confidential-data.mdsays what still catches hardcoded paths, which is less.settings.json.Verification
(git reset --hard)andecho "$(git reset --hard)";reset --soft,push --force-with-lease,push --follow-tags,add <files>,restore --staged <file>,checkout -- <file>,merge-base,log --merges;dirty-tree-guardrefused a livegit merge;.envrules refused Read, Write, and a shell command that would have written one.git -C <scratch> reset --hardran, so git-guardrails was no longer active.dirty-tree-guardbattery: section (j), 16 cases. Six mutants each fail their own cases: check removed, untracked counted, override ignored,GIT_*kept, resume steps not exempt, no real-op check../scripts/backtest.sh: all 11 gates pass, with 110 battery cases and 130 portability cases. The guide is re-rendered and restamped.