Repository navigation
feat(guardrails): check repository files a shell command changed - #6690
Conversation
block-hook-bypass judges a command by its shape, so `node x.js` or `python3 x.py` writes repository files that no Write/Edit guard sees. The new check-bash-file-changes hook gates the outcome instead: a PreToolUse fire on Bash and PowerShell records git status plus each dirty path's size and mtime, and the PostToolUse and PostToolUseFailure fires run the guards of the PreToolUse Write|Edit row on every file the command changed, a new file as a Write of its content and a tracked file as an Edit of the lines it adds against the index. A finding reaches Claude with the guard's own message. It fails open and starts nothing outside a git repository; bash_file_change_check_enabled switches it off. block-hook-bypass is unchanged. Refs #6674 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFqTQRnCGywRxhv4nw4o4V
Security review of the new hook found: a shared-temp snapshot fallback another local user could plant a symlink at; a git diff header parser that content, spaced or quoted names could steer off a file; git running fsmonitor or textconv a repository config could name; symbolic links followed to files outside the repository; and raw file names in the feedback to Claude. Snapshots now live only under CLAUDE_PLUGIN_DATA, each tracked file gets its own diff keyed by the requested name, git runs with core.fsmonitor=false and --no-textconv, links are lstat-skipped, and names are JSON-quoted. Each case has a test that fails on the prior hook. Refs #6674 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFqTQRnCGywRxhv4nw4o4V
Codex Review SummaryThis 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. |
|
Claude finished @kyle-sexton's task in 4m 49s —— View job Code review (
|
|
Claude finished @kyle-sexton's task in 6m 21s —— View job Security review in progress
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1cc088b081
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
claude-review has reviewed this pull request through c64e767; a later push is reviewed from there. |
|
claude-security-review has reviewed this pull request through c64e767; a later push is reviewed from there. |
A command that wrote and then staged a file left `git diff` against the index empty, and one that committed left `git status` clean, so neither write was checked. The snapshot now records HEAD from the same porcelain v2 status call, a tracked file's added lines are diffed against that pre-command HEAD, and when the HEAD reflog shows only commits since it, the paths those commits added or modified are checked as well. A checkout, pull, merge or reset adds no paths. Refs #6674 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFqTQRnCGywRxhv4nw4o4V
|
Claude finished @kyle-sexton's task in 10m 57s —— View job Security review complete
Scope per the lane wrapper: this is the incremental pass, reviewing only what changed since Finding (IMPORTANT) — the new write+commit detection is trivially bypassedPosted inline on
git add secret.txt && git commit -qm "msg" && git reset --soft HEAD
I verified both the Suggest keying detection off the target commit specifically ( Other changes in scope
No GitHub Actions / zizmor-lane findings in this diff (no workflow files changed here). |
|
Claude finished @kyle-sexton's task in 5m 25s —— View job Code review (
|
…mmits A `git reset --soft HEAD` after a commit adds a `reset:` reflog entry that moves nothing, and it made the commit-only check give up, so the committed write went unchecked. An entry whose commit equals the one before it is now skipped. Refs #6674 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFqTQRnCGywRxhv4nw4o4V
|
Claude finished @kyle-sexton's task in 3m 49s —— View job Code review (
|
|
Claude finished @kyle-sexton's task in 2m 21s —— View job Security review in progress
|
Walking the HEAD reflog back to the pre-command HEAD gave up at any non-commit entry, so a checkout round trip after the commit hid it. The snapshot now records the size of the worktree's logs/HEAD file, and the check reads only the entries appended since: each `commit` entry names a commit the command made, and one `git log --no-walk` lists the paths those commits added or modified. No reflog walk, no process at snapshot time, and an initial commit is seen too. Refs #6674 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFqTQRnCGywRxhv4nw4o4V
|
Claude finished @kyle-sexton's task in 6m 43s —— View job Security review in progress
|
|
Claude finished @kyle-sexton's task in 5m 11s —— View job Code review (
|
git log suppresses the path list for a merge commit unless asked, so a conflicted merge the command resolved and committed contributed no paths. --cc lists the paths whose result differs from every parent: the ones the command resolved, not what the merge brought in. Refs #6674 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFqTQRnCGywRxhv4nw4o4V
|
Claude finished @kyle-sexton's task in 3m 37s —— View job Security review in progress
|
|
Claude finished @kyle-sexton's task in 5m 4s —— View job Code review (
|
…e exec-bash routing test check-bash-file-changes.mjs runs as node with no bash on the common path, so the hooks.json shape check rejected its rows in the merge queue. Accept it as the one native-node exception: args exactly [entry, snapshot] on PreToolUse or [entry, check] on PostToolUse and PostToolUseFailure, no shell, matcher Bash|PowerShell, three such rows. Every other row still must start with exec-bash.mjs; fixture rows pin that a different .mjs, a wrong mode, a launcher flag, a shell, or another matcher still fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFqTQRnCGywRxhv4nw4o4V
|
Claude finished @kyle-sexton's task in 43s —— View job Security review in progress
|
|
Claude finished @kyle-sexton's task in 2m 28s —— View job Code review (
|

Closes #6674
Summary
block-hook-bypass.shkeys on command shape, so a script file run by an interpreter (node x.js,python3 x.py) writes repository files that no Write/Edit guard sees. This addscheck-bash-file-changes, which gates the outcome: every repository file a Bash or PowerShell command changed goes through the same guards a Write or Edit of it runs. Steps 1 and 3 of the issue; step 2 (narrowingblock-hook-bypass.sh) is #6689, andblock-hook-bypass.shis unchanged here.Fix
hooks/check-bash-file-changes.mjs, one Node script with two modes, exec form (command: node), no bash on the common path:snapshot(PreToolUse, Bash|PowerShell): finds the repository holding the call'scwdby walking up to a.gitentry (no process), runs onegit status --porcelain -z --untracked-files=all, and stores each dirty path's size and mtime underCLAUDE_PLUGIN_DATA, keyed by session andtool_use_id. Outside a git repository it starts nothing and writes nothing.check(PostToolUse and PostToolUseFailure, since a Bash command that exits non-zero fires the latter): runsgit statusagain; a path new to it, or whose size or mtime moved, is changed. Each one runs through the guards of the PreToolUseWrite|Editrow, read fromhooks.jsonso the two cannot drift (today secret-pattern-detection, hardcoded-path-check, block-windows-drive-tmp): a new file as a Write of its content, a tracked file as an Edit of the linesgit diffadds against the index (hook-precision rule 1). A guard's block message reaches Claude as the PostToolUse blockreason, or asadditionalContextafter a failed command. The command already ran, so this reports and does not deny.GIT_OPTIONAL_LOCKS=0, so it never takes the index lock a concurrent git command needs. At most 20 files and 40 s of guard runs per call.bash_file_change_check_enabled(default on). README guard table, kill-switch table and generated options reference updated; changelog fragment added..performance/ratchets.jsoncounters for the always-on path (snapshot, and a check after a command that changed nothing), ceiling 3 each (node plus one git). No strace on this host, so the ceilings are derived, not measured; the setup and check clauses were dry-run with a stand-in strace.Verification
hooks/check-bash-file-changes.test.sh: 23 assertions pass. The issue's step 3 case:node <scratch script>writes a new repo file with a hardcoded Linux home path, and the check's reason contains the exact stderr thatrun-guards.shgives a Write of that content (and the Edit-path message for an appended tracked file, and a secret). Plus PostToolUseFailure, the PowerShell tool, and stay-quiet cases: no change, benign change, an already-dirty file the command did not touch, a gitignored file, a non-git cwd, no snapshot, kill switch, malformed stdin.scripts/affected-tests.sh --run: all 6 selected suites pass (run-guards, coverage-manifest, abort-boundary, require-jq-*).check-hook-exec-form.sh,check-hooks-description.sh,check-hook-userconfig-argv.sh,check-hook-slow-shapes.sh,check-killswitch-hoist.sh,claude plugin validate plugins/guardrails: pass. shellcheck and shfmt clean.git statusin this repository (about 8,000 files) takes about 30 ms.Not verified: the hook on Windows (the Windows lane runs a fixed list of suites and this one is not on it), and the strace counts above.
Related
block-hook-bypass.sh)test-scan-bash.shreadstool_response.bashEditDiff, which Claude Code adds only whenbashEditDiffEnabledis set; this hook needs no setting.🤖 Generated with Claude Code
https://claude.ai/code/session_01EFqTQRnCGywRxhv4nw4o4V