RFC-023 hook bundle hardening#21
Conversation
…ave 1 manifest Promote the Codex x Claude hook-audit consensus series (fixed point reached; six owner decisions made) into a tracked Draft RFC, plus a schema-valid Wave 1 (behavior-disclosure docs) Change Manifest draft. Non-normative (docs/proposals/). No hook behavior change. Verified: change-manifest schema clean, hook selftest 151/0, internal links 0 broken. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Clarify RFC-023 Wave 1 hook docs: Layer A vs Layer B wiring, network behavior, Stop and commit blockers, warn triggers, manifest discovery and Lean skip, and intentionally non-bypassable completion/evidence gates.
No hook behavior change; hooks/hooks.json, AGENTS.md, and docs/runtime-hook-contract.md are untouched.
Verification:
- reference-implementations/hooks-claude-code/selftests/selftest.sh -> 151 cases, 0 failed
- reference-implementations/hooks-{codex,cursor,gemini-cli,windsurf}/selftests/selftest.sh -> 4 cases, 0 failed each
- python3 .github/scripts/check-internal-links.py -> 0 broken
- make verify-local
- git diff --check
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Narrow risky-bash protected-branch force-push matching to refspec destinations, add the Bash tests/test .pem read exception, and allow only whitelisted shell-init eval command substitutions. Verification: reference-implementations/hooks-claude-code/selftests/selftest.sh -> 176 cases, 0 failed; adapter selftests -> 4 cases, 0 failed each; python3 .github/scripts/check-internal-links.py -> ok no broken internal links across 310 markdown files; make verify-local -> OK; git diff --check -> clean.
Make the consumer-registry-check network probe opt-in: default-off skips the probe entirely, and the reachability check only runs when explicitly enabled. Adds the default-off-skips-probe fixture plus env fixtures for pass-reachable and warn-unreachable, and updates docs/DEVIATIONS/READMEs accordingly. Includes the Wave 3 Change Manifest. Verification: hooks-claude-code selftest -> 177 cases, 0 failed; adapter selftests (codex/cursor/gemini-cli/windsurf) -> 4 cases, 0 failed each; validate-change-manifests.py -> OK in-flight schema clean; git diff --check -> clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1b8cf5a2d2
ℹ️ 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".
| if printf '%s' "$sql_arg_masked" | grep -Eqi '(^|[[:space:];])UPDATE[[:space:]]+[a-zA-Z_."]+[[:space:]]+SET[[:space:]]+'; then | ||
| if ! printf '%s' "$sql_arg_masked" | grep -Eqi '\bWHERE\b'; then |
There was a problem hiding this comment.
Preserve identifier tokens when masking SQL for MCP checks
Using sql_arg_masked for the UPDATE ... SET / ALTER TABLE ... DROP patterns causes false negatives whenever table names are quoted identifiers (e.g. UPDATE "Users" SET ... or ALTER TABLE "Users" DROP COLUMN ...), because sql_mask_literals strips the quoted identifier text before matching. This regression lets destructive SQL through without AGENT_PROTOCOL_RISKY_MCP_ALLOW=1, which weakens the core safety block for production data-store mutations.
Useful? React with 👍 / 👎.
| if [ "$positional_index" -eq 1 ]; then | ||
| continue | ||
| fi |
There was a problem hiding this comment.
Treat first git-push positional token as possible refspec
The parser now unconditionally skips the first positional token after git push, but per git push syntax ([<repository> [<refspec>...]]) the repository is optional and that first token can be the refspec. As a result, commands like git push --force main (default remote, protected branch refspec) are no longer blocked, creating a force-push escape path for protected branches.
Useful? React with 👍 / 👎.
| [ "$token" = "git" ] && seen_git=1 | ||
| continue |
There was a problem hiding this comment.
Recognize path-qualified git executable in force-push parser
The new token parser only starts matching after a token exactly equal to git, so force pushes invoked as path-qualified binaries (for example /usr/bin/git push --force origin main) bypass Group 1 entirely. The prior implementation matched these forms, so this is a regression that allows protected-branch rewrites to pass without the expected block.
Useful? React with 👍 / 👎.
| cat|head|tail|less|more|grep|awk|sed|xargs|env|printenv|export) | ||
| after_read_verb=1 |
There was a problem hiding this comment.
Match path-qualified read tools in PEM secret-read detection
.pem detection was moved out of cred_path into pem_read_outside_tests(), but that helper only treats bare command names (e.g. cat) as read verbs. Common path-qualified invocations like /bin/cat secrets.pem now evade blocking, whereas they were previously caught, reopening a credential-read bypass for PEM files outside test paths.
Useful? React with 👍 / 👎.
| exit 0 | ||
| } |
There was a problem hiding this comment.
Check all repeated -c SQL payloads for destructive verbs
dash_c_sql_payload() exits after the first -c argument, so commands that use repeated -c options (for example psql -c "select 1" -c "DROP TABLE users") are now treated as safe if the first payload is benign. psql executes -c options in sequence, so this change introduces a bypass for direct destructive database mutations that were previously blocked.
Useful? React with 👍 / 👎.
| location="bash:tool_input.command" | ||
| hint="see docs/runtime-hook-contract.md §Risky-action interception list (defensive eval-or-pipe-sh group) and docs/destructive-action-discipline.md §3; download to a file, review it, then run — do not pipe network content into sh" | ||
| elif printf '%s' "$command_str" | grep -Eq '(^|[[:space:];|&])eval[[:space:]]+'; then | ||
| elif printf '%s' "$command_str" | grep -Eq '(^|[[:space:];|&])eval[[:space:]]+' && ! shell_init_eval_is_allowed; then |
There was a problem hiding this comment.
Block commands that mix allowed init eval with arbitrary eval
The eval guard now blocks only when eval is present and shell_init_eval_is_allowed is false for the whole command line. That allows payloads like eval "$(direnv hook bash)"; eval "$UNTRUSTED" to pass, because one allowed init-eval suppresses blocking for subsequent arbitrary eval usage in the same command string.
Useful? React with 👍 / 👎.
Summary
.pemtest fixtures, and common shell-init eval.Verification
git diff --checkpassed.Boundaries
🤖 Generated with Claude Code