fix(#6357): make PostToolUse sanitizers honor Claude Code's hook contract - #6468
fix(#6357): make PostToolUse sanitizers honor Claude Code's hook contract#6468waynesun09 wants to merge 4 commits into
Conversation
PR Summary by QodoFix PostToolUse hooks: honor Claude Code v2 contract with chained sanitizers
AI Description
Diagram
High-Level Assessment
Files changed (15)
|
|
🤖 Review · Commit: |
Code Review by Qodo
1.
|
…ract Read tool_response (fallback tool_result) and replace output via hookSpecificOutput.updatedToolOutput, with suppress → unicode → redact enforced in a single posttool_chain.py driver because Claude runs hooks in parallel. Canary post-tool also redacts leaked tokens, since decision:block does not hide the original result. Signed-off-by: Wayne Sun <gsun@redhat.com>
Claude Code Bash payloads always have stdout, so first-key scan_text missed stderr-only canary leaks. Two PostToolUse hooks also raced on updatedToolOutput; the chain now owns suppress → unicode → redact → canary in one process. Assisted-by: Grok (fix), Claude (review), Gemini (review), Codex (review) Signed-off-by: Wayne Sun <gsun@redhat.com>
FULLSEND_POSTTOOL_SKIP was a test-only env knob the agent could write into workspace .env and disable the chain. Stages are gated by sibling files only. Also type the chain metadata map and add Bash-object tests for unicode and suppress. Assisted-by: Grok (fix) Signed-off-by: Wayne Sun <gsun@redhat.com>
2fae3b5 to
e3eb507
Compare
Site previewPreview: https://7f863de0-site.fullsend-ai.workers.dev Commit: |
|
🤖 Finished Review · ✅ Success · Started 9:48 PM UTC · Completed 10:06 PM UTC Commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
ReviewFindingsMedium
Low
Previous runReviewFindingsMedium
Low
Previous run (2)ReviewFindingsHigh
Medium
Low
Next steps:
Previous run (3)ReviewFindingsMedium
Low
|
|
🤖 Review · Commit: |
|
🤖 Finished Review · ✅ Success · Started 10:50 PM UTC · Completed 11:07 PM UTC Commit: |
|
🤖 Finished Review · ✅ Success · Started 12:04 AM UTC · Completed 12:22 AM UTC Commit: |
Superseded by updated review
Local sandbox validation — closes test plan item 3Built this branch and ran the triage harness against a real sandbox to close out the last unchecked test-plan item ( Setup: Wiring
"PostToolUse": [
{ "matcher": "*",
"hooks": [{ "type": "command",
"command": "python3 /sandbox/claude-config/hooks/posttool_chain.py" }] }
]
1.
|
Canary detection ran on the raw payload before the unicode stage, so a token split with zero-width or fullwidth characters evaded it and the unicode stage then reassembled the clean token into updatedToolOutput. Unicode now runs first and detection reads its result. Detection and redaction share one case-insensitive matcher. Redaction walked a str.lower() copy using indices from the original string; a character that case-folds to more code points than it started with desynchronized the two and could leave a detected token in the output. The canary path fails closed throughout: a scan that raises counts as a hit, output that cannot be verified free of the token is withheld, exit 1 is unconditional, and input the driver cannot read (malformed JSON, oversized) blocks rather than skipping detection - the chain is the only PostToolUse entry point Claude Code schedules. A leak also sets continue: false, the documented field that halts the session. Also: scan_text joins fields on a newline so a match cannot span a boundary the redactors rewrite independently; unicode normalization skips identifier fields so a rewritten path is never reported back to Claude; stage exceptions are guarded per field and recorded in findings.jsonl; and a stage error alone no longer emits a no-op rewrite that could clobber another hook's. docs/runtimes.md now records that PostToolUse covers successful tool calls only - Claude Code routes failures to PostToolUseFailure, which is not wired yet. Assisted-by: Claude (fix, review), Grok (review) Signed-off-by: Wayne Sun <gsun@redhat.com>
d3d954b to
574bc1e
Compare
|
🤖 Finished Review · ✅ Success · Started 1:20 AM UTC · Completed 1:36 AM UTC Commit: |
…time-stub # Conflicts: # docs/runtimes.md
|
Merged into #6467 at 9780c6c (merge commit, conflicts resolved only in |
|
🤖 Finished Retro · ✅ Success · Started 7:00 PM UTC · Completed 7:20 PM UTC Commit: |
Retro: PR #6468 — PostToolUse sanitizer hook contract fixWorkflow shape: Triage → 2× Code (both failed) → human-authored PR → 4× Review (COMMENT → REQUEST_CHANGES → COMMENT → COMMENT) → closed (folded into #6467). Agents repo: Timeline
AssessmentReview agent: strong performance. The cycle-2 HIGH fail-open finding was the most valuable agent contribution — a real, exploitable security vulnerability where a detected canary token could escape un-redacted. The escalation arc (COMMENT → REQUEST_CHANGES → COMMENT) was correctly calibrated. The review agent outperformed Qodo's review bot, which only found the same medium-severity Code agent: two wasted runs (~70 min compute). Both failures trace to two root causes: (1) pre-commit cannot install tools inside the network-isolated sandbox, so the agent cannot self-verify with ruff or ty, and (2) AGENTS.md contains no Python lint/type-check guidance, making Evidence for existing issues (not filing duplicates)
Proposals filed |
Summary
tool_response(fallbacktool_result) and replace output viahookSpecificOutput.updatedToolOutput, preserving structured shapes such as Bash{stdout, stderr, …}.posttool_chain.pydriver on*applies suppress → unicode → redact → canary in-process. Claude Code runs matching hooks in parallel and does not merge twoupdatedToolOutputrewrites, so canary cannot be a separate PostToolUse matcher.scan_textflattens every string field (includingstderr); a leak only on stderr is no longer invisible.apply_textwrites a suppress summary into stdout and blanks other text slots, or leaves unrecognized structured shapes unchanged.decision: blockand redacts the token inupdatedToolOutput— Claude Code's PostToolUseblockonly appends a reason and still shows the original result.docs/runtimes.md. Settings loading (Sandbox tool hooks are never loaded: settings.json is written to /sandbox/workspace/.claude but Claude Code runs from /sandbox/workspace/<repo> #6358) is already onmain, so this contract is effective once this PR merges.Closes #6357
Test plan
uvx pytest internal/security/hooks/(140 passed)go test ./internal/security/--debug hooksconfirmingtool_responsein /updatedToolOutputout