chore(claude): sync vendored hooks with the live baseline - #347
Merged
Conversation
Merged rather than overwritten: the vendored compound-bash-allow.py had
diverged from the live copy in BOTH directions. It was already ahead on
control-flow keywords (regex prefix stripping, `for` headers including the
arithmetic form, redirection tails on `done`, `break 2`), while the live copy
had gained subshell-group unwrapping. Copying either way would have regressed
the other.
compound-bash-allow.py
- strip_group_wrapper(): split_compound() tracks paren depth, so a subshell
arrives intact and `cat f || (echo missing; find . -name f)` was judged on
the binary `(echo`, matching nothing. The body is now re-split and each
inner segment rule-checked. Unwrapping is the safe direction, as with
keyword prefixes: `(rm -rf /)` reduces to `rm -rf /`, which DENY_BINARIES
rejects; safe-listing `(` would have allowed it outright.
- Brace groups: `{` joins _KW_PREFIX_RE, where the whitespace lookahead is
what keeps `${VAR}` and brace expansion `{a,b}` out. `}` is matched ahead
of _KW_BARE_RE because a trailing \b could never fire there — `}` and
end-of-segment are both non-word, so no boundary exists. Like `done`, `}`
may carry the group's redirections, so `{ echo a; } > out` is covered.
- The trailing-redirection check after `)` reuses _REDIR_TAIL_RE rather than
introducing a second regex for the same idea.
compound-bash.json (new)
- Eight stdout-only binaries: strings, diff, cmp, od, comm, join, sha256sum,
md5sum. Deploys via the existing `cp -a` of the tree, and is read in
preference to ~/.claude/hooks/compound-bash.json.
- xxd is deliberately excluded, with the reason recorded in the file: `xxd
[infile [outfile]]` takes an output path as a bare positional and `-r`
patches binary in place, so it writes with no redirection operator. Every
entry in the list has to be reads-only for the safe list to mean anything.
redirect-inline-eval.py
- Ported _reads_program_from_stdin() from the live copy: `python3 - <<'EOF'`,
`python3 <<'EOF'`, `cat x.py | node` and `deno run -` are inline eval by
another spelling, and were falling through to a prompt instead of being
redirected to sbx. Now byte-identical to the live hook.
settings.template.json
- Added `git worktree remove __HOME__/projects/*/.worktrees/*`. Worktrees
under the project itself were unreachable — only `/tmp` targets had a rule.
Written WITHOUT `-C` so normalize_git_prefix() covers both invocation
forms from one rule. Note the trailing `*` means `--force` written after
the path is also matched; the rule syntax cannot express "this subcommand
but not that flag", and the existing `/tmp` rules already allow --force.
Verified: 50/50 behavioural cases against the merged hook — subshells, brace
groups, the keyword features that were already vendored, the new safe
commands, and refusal of sudo/rm/mv/chmod/ln, curl|sh, $(), backticks and
process substitution; 11/11 on the redirect hook; 7/7 against template rules
rendered the way deploy-cursor-workspace.sh renders them. Diffing the merged
hook against origin/main confirms that `while read` — the one uncovered case
found — prompts in both, so it is pre-existing rather than a regression.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
m2ux
added a commit
that referenced
this pull request
Jul 29, 2026
#347 landed the same hook merge this branch carried (compound-bash-allow.py, redirect-inline-eval.py, compound-bash.json, the README line, and the plain worktree-remove rule), and did the hook better: "}" handled explicitly ahead of _KW_BARE_RE with the reasoning about why "\b" cannot fire after a non-word character, _REDIR_TAIL_RE reused instead of a second near-identical regex, and the config lookup order documented in the docstring. Those are dropped here in favour of main. What is left is the part #347 did not cover: settings.template.json collapse the four "git rebase --abort/--continue/--quit/--skip" rules into "git rebase *"; add "git tag" reads, the "git -C" worktree-remove forms to match the plain one #347 added, and docs.rs, which pairs with the cargo block and rust-analyzer plugin the template already ships. bash-composition.md document the stdin-eval denial that redirect-inline-eval.py now enforces. The global CLAUDE.md predates that hook change and does not mention it, so the vendored rule otherwise ships a hook its own docs contradict. Skipped as personal: docs.polkadot.com, paritytech.github.io, effortLevel. Verified: deploy renders valid JSON with every new rule expanded and no leftover placeholders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Syncs the vendored Claude baseline in
scripts/claude/with the live per-usersetup, and adds the one permission rule the template was missing.
Why this is a merge, not a copy
The vendored
compound-bash-allow.pyhad drifted from the live copy in bothdirections, so overwriting in either direction would have lost work:
forheaders incl.for ((;;)), redirection tails ondone,break 2,[[strip_group_wrapper()The vendored keyword implementation is kept as-is and the group unwrapping is
ported onto it.
Changes
compound-bash-allow.py— subshell and brace groups no longer force aprompt.
split_compound()tracks paren depth, so a subshell arrives intact asone segment whose apparent binary is
(echo;cat f || (echo missing; find . -name f)therefore matched no rule. The body is now re-split and each innersegment rule-checked. Brace groups arrive already shredded into
{ echo aand}, so they are handled by the keyword machinery instead:{joins_KW_PREFIX_RE(the whitespace lookahead is what keeps${VAR}and{a,b}out), and
}is matched ahead of_KW_BARE_RE, where a trailing\bcouldnever fire.
Unwrapping is the safe direction, the same argument as for keyword prefixes:
(rm -rf /)reduces torm -rf /, whichDENY_BINARIESrejects. Safe-listing(would have allowed it outright.compound-bash.json(new) — eight stdout-only binaries added to the safelist:
strings,diff,cmp,od,comm,join,sha256sum,md5sum.Deploys with the existing
cp -aof the tree and is read in preference to~/.claude/hooks/compound-bash.json.xxdis deliberately excluded, and the file records why:xxd [infile [outfile]]takes an output path as a bare positional and-rpatches binary inplace, so it writes without a redirection operator. The safe list only means
something if every entry is reads-only.
redirect-inline-eval.py— ported_reads_program_from_stdin()from thelive copy.
python3 - <<'EOF',python3 <<'EOF',cat x.py | nodeanddeno run -are inline eval by another spelling and were falling through to a promptrather than being redirected to
sbx. This file is now byte-identical to thelive hook.
settings.template.json— addedgit worktree remove __HOME__/projects/*/.worktrees/*. Worktrees under the project itself wereunreachable; only
/tmptargets had a rule. Written without-Csonormalize_git_prefix()covers both invocation forms from a single rule.One caveat worth stating explicitly: the trailing
*also matches--forcewritten after the path. The rule syntax cannot express "this subcommand but
not that flag", and the existing
/tmprules already allow--force, so thisis consistent rather than a new class of exposure. What is at risk is
uncommitted and untracked files in that worktree —
worktree removedoes nottouch branches or commits.
Verification
the keyword features that were already vendored (so the merge is proven
non-regressing), the new safe commands, and refusal of
sudo/rm/mv/chmod/ln,curl | sh,$(), backticks and process substitution.compound-bash-allow.py.deploy-cursor-workspace.shrenders them (expand_obj+__HOME__), notjust against the raw template.
origin/mainside by side. The one uncoveredcase found,
while read -r l ; do … done, prompts in both copies — so itis pre-existing, not a regression. Not worth fixing: a useful
readneeds$var, whichblock-dynamic-shell.pydenies anyway.Deliberately not synced
The live copies of
curl-allow.jsonandwebfetch-allow.jsoncarryproject-specific hosts and a GitHub org scope. Those stay out of a generic
template.
allow-project-scripts.py,block-gh-api-writes.py,curl-read-allow.py,webfetch-allow.py,lib/project_scripts.pyandbin/sbxwere already identical. The.claude/rules/files were already insync — verified, not assumed.
🤖 Generated with Claude Code