Skip to content

chore(claude): sync vendored hooks with the live baseline - #347

Merged
m2ux merged 1 commit into
mainfrom
chore/sync-vendored-claude-hooks
Jul 29, 2026
Merged

chore(claude): sync vendored hooks with the live baseline#347
m2ux merged 1 commit into
mainfrom
chore/sync-vendored-claude-hooks

Conversation

@m2ux

@m2ux m2ux commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Syncs the vendored Claude baseline in scripts/claude/ with the live per-user
setup, and adds the one permission rule the template was missing.

Why this is a merge, not a copy

The vendored compound-bash-allow.py had drifted from the live copy in both
directions
, so overwriting in either direction would have lost work:

vendored (before) live
control-flow keywords regex prefix stripping, for headers incl. for ((;;)), redirection tails on done, break 2, [[ coarser token-based stripping
subshell groups not handled 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 a
prompt. split_compound() tracks paren depth, so a subshell arrives intact as
one 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 inner
segment rule-checked. Brace groups arrive already shredded into { echo a and
}, 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 \b could
never fire.

Unwrapping is the safe direction, the same argument as for keyword prefixes:
(rm -rf /) reduces to rm -rf /, which DENY_BINARIES rejects. Safe-listing
( would have allowed it outright.

compound-bash.json (new) — eight stdout-only binaries added to the safe
list: strings, diff, cmp, od, comm, join, sha256sum, md5sum.
Deploys with the existing cp -a of the tree and is read in preference to
~/.claude/hooks/compound-bash.json.

xxd is deliberately excluded, and the file records why: xxd [infile [outfile]] takes an output path as a bare positional and -r patches binary in
place, 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 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
rather than being redirected to sbx. This file is 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 a single rule.

One caveat worth stating explicitly: the trailing * also matches --force
written after the path. The rule syntax cannot express "this subcommand but
not that flag", and the existing /tmp rules already allow --force, so this
is consistent rather than a new class of exposure. What is at risk is
uncommitted and untracked files in that worktree — worktree remove does not
touch branches or commits.

Verification

  • 50/50 behavioural cases against the merged hook: subshells, brace groups,
    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.
  • 11/11 on the redirect hook, which dynamically imports the modified
    compound-bash-allow.py.
  • 7/7 against the template's rules rendered exactly the way
    deploy-cursor-workspace.sh renders them (expand_obj + __HOME__), not
    just against the raw template.
  • Diffed the merged hook against origin/main side by side. The one uncovered
    case found, while read -r l ; do … done, prompts in both copies — so it
    is pre-existing, not a regression. Not worth fixing: a useful read needs
    $var, which block-dynamic-shell.py denies anyway.

Deliberately not synced

The live copies of curl-allow.json and webfetch-allow.json carry
project-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.py and
bin/sbx were already identical. The .claude/rules/ files were already in
sync — verified, not assumed.

🤖 Generated with Claude Code

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
m2ux merged commit 834359c into main Jul 29, 2026
2 checks passed
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>
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