[Hook architecture](15) Put the categorical-scope-guard hook onto the shared hook code - #730
Conversation
… build-the-lever hook onto the shared hook code. Review claim: This hook reports findings to the shared hook code, which applies its registry mode and writes event rows. It keeps mode warn. Review lane: behavior Safety invariant: The hook gives the same stop, warn, or silent result on every case in its current test folder, except the mode change named in this claim, and its test folder keeps exiting 0. Effectiveness measurement: `python3 -m unittest discover -s engine/hooks/build-the-lever/tests` exits 0, and the new mode-override case fails before this change. Slice rationale: One hook per workflow, as the user asked, so each migration is reviewed on its own. Architectural effect: The build-the-lever entry scripts become thin calls into the shared runtime; its detection returns findings. Goal: Suggests a script when many files are hand-edited. Keep that behavior while its mode moves into the registry. Motivation: Mode and output shape live inside each hook today; the shared code makes a mode change a one-line registry edit. Alternative considerations: Migrating several hooks per workflow was set aside because the user asked for one hook per workflow. Implementation details: Turn this hook's detection into detect(event) returning Finding objects with stable rule ids, and make each harness entry script call run_hook from engine/hooks/_sdk/runtime.py. It keeps mode warn. Non-goals: No change to what the hook detects. No other hook changes. Layer: domain Feature state: active Files: engine/hooks/build-the-lever/claude_posttooluse.py, engine/hooks/build-the-lever/claude_prompt_submit.py, engine/hooks/build-the-lever/codex_posttooluse.py, engine/hooks/build-the-lever/codex_prompt_submit.py, engine/hooks/build-the-lever/cursor_before_submit.py, engine/hooks/build-the-lever/cursor_post_tool_use.py, engine/hooks/build-the-lever/detect.py, engine/hooks/build-the-lever/install_claude_hook.py, engine/hooks/build-the-lever/install_codex_hook.py, engine/hooks/build-the-lever/install_cursor_hook.py, engine/hooks/build-the-lever/state.py, engine/hooks/build-the-lever/tests/test_hooks_sdk_mode.py Change types: - engine/hooks/build-the-lever/claude_posttooluse.py: modify - engine/hooks/build-the-lever/claude_prompt_submit.py: modify - engine/hooks/build-the-lever/codex_posttooluse.py: modify - engine/hooks/build-the-lever/codex_prompt_submit.py: modify - engine/hooks/build-the-lever/cursor_before_submit.py: modify - engine/hooks/build-the-lever/cursor_post_tool_use.py: modify - engine/hooks/build-the-lever/detect.py: modify - engine/hooks/build-the-lever/install_claude_hook.py: modify - engine/hooks/build-the-lever/install_codex_hook.py: modify - engine/hooks/build-the-lever/install_cursor_hook.py: modify - engine/hooks/build-the-lever/state.py: modify - engine/hooks/build-the-lever/tests/test_hooks_sdk_mode.py: create Acceptance criteria: - `python3 -m unittest discover -s engine/hooks/build-the-lever/tests` exits 0. - With CATSTACK_HOOK_MODE_BUILD_THE_LEVER set to warn, a case that stops today produces a warning instead, proving the registry mode drives the response. - Each finding writes one event row with the hook's rule_id. Solution: Put the build-the-lever hook onto the shared hook code. Review claim: This hook reports findings to the shared hook code, which applies its registry mode and writes event rows. It keeps mode warn. Review lane: behavior Safety invariant: The hook gives the same stop, warn, or silent result on every case in its current test folder, except the mode change named in this claim, and its test folder keeps exiting 0. Effectiveness measurement: `python3 -m unittest discover -s engine/hooks/build-the-lever/tests` exits 0, and the new mode-override case fails before this change. Slice rationale: One hook per workflow, as the user asked, so each migration is reviewed on its own. Architectural effect: The build-the-lever entry scripts become thin calls into the shared runtime; its detection returns findings. Goal: Suggests a script when many files are hand-edited. Keep that behavior while its mode moves into the registry. Motivation: Mode and output shape live inside each hook today; the shared code makes a mode change a one-line registry edit. Alternative considerations: Migrating several hooks per workflow was set aside because the user asked for one hook per workflow. Implementation details: Turn this hook's detection into detect(event) returning Finding objects with stable rule ids, and make each harness entry script call run_hook from engine/hooks/_sdk/runtime.py. It keeps mode warn. Non-goals: No change to what the hook detects. No other hook changes. Layer: domain Feature state: active Files: engine/hooks/build-the-lever/claude_posttooluse.py, engine/hooks/build-the-lever/claude_prompt_submit.py, engine/hooks/build-the-lever/codex_posttooluse.py, engine/hooks/build-the-lever/codex_prompt_submit.py, engine/hooks/build-the-lever/cursor_before_submit.py, engine/hooks/build-the-lever/cursor_post_tool_use.py, engine/hooks/build-the-lever/detect.py, engine/hooks/build-the-lever/install_claude_hook.py, engine/hooks/build-the-lever/install_codex_hook.py, engine/hooks/build-the-lever/install_cursor_hook.py, engine/hooks/build-the-lever/state.py, engine/hooks/build-the-lever/tests/test_hooks_sdk_mode.py Change types: - engine/hooks/build-the-lever/claude_posttooluse.py: modify - engine/hooks/build-the-lever/claude_prompt_submit.py: modify - engine/hooks/build-the-lever/codex_posttooluse.py: modify - engine/hooks/build-the-lever/codex_prompt_submit.py: modify - engine/hooks/build-the-lever/cursor_before_submit.py: modify - engine/hooks/build-the-lever/cursor_post_tool_use.py: modify - engine/hooks/build-the-lever/detect.py: modify - engine/hooks/build-the-lever/install_claude_hook.py: modify - engine/hooks/build-the-lever/install_codex_hook.py: modify - engine/hooks/build-the-lever/install_cursor_hook.py: modify - engine/hooks/build-the-lever/state.py: modify - engine/hooks/build-the-lever/tests/test_hooks_sdk_mode.py: create Acceptance criteria: - `python3 -m unittest discover -s engine/hooks/build-the-lever/tests` exits 0. - With CATSTACK_HOOK_MODE_BUILD_THE_LEVER set to warn, a case that stops today produces a warning instead, proving the registry mode drives the response. - Each finding writes one event row with the hook's rule_id. Invoker-Finalize-Id: 35d0555b-3f55-47f9-9e41-97f12e84dc55
…terministic proof for put the build-the-lever hook onto the shared hook code. Review claim: The proof exits 0 only when put the build-the-lever hook onto the shared hook code holds. Review lane: proof Safety invariant: Proof only; it changes no product behavior. Effectiveness measurement: The exit status of `python3 -m unittest discover -s engine/hooks/build-the-lever/tests` is the signal for this slice. Slice rationale: One proof unit for this workflow. Architectural effect: None; verification only. Goal: Prove put the build-the-lever hook onto the shared hook code with one deterministic run. Motivation: Each workflow carries its own proof so a reviewer can trust the slice alone. Alternative considerations: Manual inspection was set aside as non-deterministic. Implementation details: Execute the proof as a terminal gate. Non-goals: No product edits here. Layer: e2e_regression Feature state: active Exit code: 0 Invoker-Finalize-Id: 3fe2349d-7d5a-4cf2-a6ab-f4c370ef2d0f
…only gate confirming no ephemeral handoff files were left behind. Review claim: The workflow leaves no ephemeral handoff files in the tree. Review lane: proof Safety invariant: Read-only; it never deletes files, alters the index, or commits caller work. Effectiveness measurement: A non-zero exit when ephemeral handoff files remain is the signal. Slice rationale: One unit: the hygiene gate. Architectural effect: None. Goal: Confirm no ephemeral handoff files remain after every other task finishes. Motivation: Ephemeral inter-task files leak into the diff and read as part of the change. Alternative considerations: Manual inspection was set aside as non-deterministic. Implementation details: Run scripts/scrub-handoff-artifacts.sh in check mode. Non-goals: No deletion, no index changes, no commits. Layer: e2e_regression Feature state: active Exit code: 0 Invoker-Finalize-Id: 70147de4-ee65-46de-90da-48ab84cce9d0
…ae3e57046-efa4e452 — Terminal read-only gate confirming no ephemeral handoff files were left behind. Review claim: The workflow leaves no ephemeral handoff files in the tree. Review lane: proof Safety invariant: Read-only; it never deletes files, alters the index, or commits caller work. Effectiveness measurement: A non-zero exit when ephemeral handoff files remain is the signal. Slice rationale: One unit: the hygiene gate. Architectural effect: None. Goal: Confirm no ephemeral handoff files remain after every other task finishes. Motivation: Ephemeral inter-task files leak into the diff and read as part of the change. Alternative considerations: Manual inspection was set aside as non-deterministic. Implementation details: Run scripts/scrub-handoff-artifacts.sh in check mode. Non-goals: No deletion, no index changes, no commits. Layer: e2e_regression Feature state: active
…e cat-mode-default hook onto the shared hook code. Review claim: This hook reports findings to the shared hook code, which applies its registry mode and writes event rows. It keeps mode warn. Review lane: behavior Safety invariant: The hook gives the same stop, warn, or silent result on every case in its current test folder, except the mode change named in this claim, and its test folder keeps exiting 0. Effectiveness measurement: `python3 -m unittest discover -s engine/hooks/cat-mode-default/tests` exits 0, and the new mode-override case fails before this change. Slice rationale: One hook per workflow, as the user asked, so each migration is reviewed on its own. Architectural effect: The cat-mode-default entry scripts become thin calls into the shared runtime; its detection returns findings. Goal: Applies the user's working style each turn. Keep that behavior while its mode moves into the registry. Motivation: Mode and output shape live inside each hook today; the shared code makes a mode change a one-line registry edit. Alternative considerations: Migrating several hooks per workflow was set aside because the user asked for one hook per workflow. Implementation details: Turn this hook's detection into detect(event) returning Finding objects with stable rule ids, and make each harness entry script call run_hook from engine/hooks/_sdk/runtime.py. It keeps mode warn. Non-goals: No change to what the hook detects. No other hook changes. Layer: domain Feature state: active Files: engine/hooks/cat-mode-default/claude_pretooluse_agent.py, engine/hooks/cat-mode-default/claude_prompt_submit.py, engine/hooks/cat-mode-default/detect.py, engine/hooks/cat-mode-default/install_claude_hook.py, engine/hooks/cat-mode-default/tests/test_hooks_sdk_mode.py Change types: - engine/hooks/cat-mode-default/claude_pretooluse_agent.py: modify - engine/hooks/cat-mode-default/claude_prompt_submit.py: modify - engine/hooks/cat-mode-default/detect.py: modify - engine/hooks/cat-mode-default/install_claude_hook.py: modify - engine/hooks/cat-mode-default/tests/test_hooks_sdk_mode.py: create Acceptance criteria: - `python3 -m unittest discover -s engine/hooks/cat-mode-default/tests` exits 0. - With CATSTACK_HOOK_MODE_CAT_MODE_DEFAULT set to warn, a case that stops today produces a warning instead, proving the registry mode drives the response. - Each finding writes one event row with the hook's rule_id. Solution: Put the cat-mode-default hook onto the shared hook code. Review claim: This hook reports findings to the shared hook code, which applies its registry mode and writes event rows. It keeps mode warn. Review lane: behavior Safety invariant: The hook gives the same stop, warn, or silent result on every case in its current test folder, except the mode change named in this claim, and its test folder keeps exiting 0. Effectiveness measurement: `python3 -m unittest discover -s engine/hooks/cat-mode-default/tests` exits 0, and the new mode-override case fails before this change. Slice rationale: One hook per workflow, as the user asked, so each migration is reviewed on its own. Architectural effect: The cat-mode-default entry scripts become thin calls into the shared runtime; its detection returns findings. Goal: Applies the user's working style each turn. Keep that behavior while its mode moves into the registry. Motivation: Mode and output shape live inside each hook today; the shared code makes a mode change a one-line registry edit. Alternative considerations: Migrating several hooks per workflow was set aside because the user asked for one hook per workflow. Implementation details: Turn this hook's detection into detect(event) returning Finding objects with stable rule ids, and make each harness entry script call run_hook from engine/hooks/_sdk/runtime.py. It keeps mode warn. Non-goals: No change to what the hook detects. No other hook changes. Layer: domain Feature state: active Files: engine/hooks/cat-mode-default/claude_pretooluse_agent.py, engine/hooks/cat-mode-default/claude_prompt_submit.py, engine/hooks/cat-mode-default/detect.py, engine/hooks/cat-mode-default/install_claude_hook.py, engine/hooks/cat-mode-default/tests/test_hooks_sdk_mode.py Change types: - engine/hooks/cat-mode-default/claude_pretooluse_agent.py: modify - engine/hooks/cat-mode-default/claude_prompt_submit.py: modify - engine/hooks/cat-mode-default/detect.py: modify - engine/hooks/cat-mode-default/install_claude_hook.py: modify - engine/hooks/cat-mode-default/tests/test_hooks_sdk_mode.py: create Acceptance criteria: - `python3 -m unittest discover -s engine/hooks/cat-mode-default/tests` exits 0. - With CATSTACK_HOOK_MODE_CAT_MODE_DEFAULT set to warn, a case that stops today produces a warning instead, proving the registry mode drives the response. - Each finding writes one event row with the hook's rule_id. Invoker-Finalize-Id: a0c6917e-a7c5-46f3-a6a1-b370f2d14108
…eterministic proof for put the cat-mode-default hook onto the shared hook code. Review claim: The proof exits 0 only when put the cat-mode-default hook onto the shared hook code holds. Review lane: proof Safety invariant: Proof only; it changes no product behavior. Effectiveness measurement: The exit status of `python3 -m unittest discover -s engine/hooks/cat-mode-default/tests` is the signal for this slice. Slice rationale: One proof unit for this workflow. Architectural effect: None; verification only. Goal: Prove put the cat-mode-default hook onto the shared hook code with one deterministic run. Motivation: Each workflow carries its own proof so a reviewer can trust the slice alone. Alternative considerations: Manual inspection was set aside as non-deterministic. Implementation details: Execute the proof as a terminal gate. Non-goals: No product edits here. Layer: e2e_regression Feature state: active Exit code: 0 Invoker-Finalize-Id: 8132f734-9bf7-4d89-8c3c-56bc4b969d79
…only gate confirming no ephemeral handoff files were left behind. Review claim: The workflow leaves no ephemeral handoff files in the tree. Review lane: proof Safety invariant: Read-only; it never deletes files, alters the index, or commits caller work. Effectiveness measurement: A non-zero exit when ephemeral handoff files remain is the signal. Slice rationale: One unit: the hygiene gate. Architectural effect: None. Goal: Confirm no ephemeral handoff files remain after every other task finishes. Motivation: Ephemeral inter-task files leak into the diff and read as part of the change. Alternative considerations: Manual inspection was set aside as non-deterministic. Implementation details: Run scripts/scrub-handoff-artifacts.sh in check mode. Non-goals: No deletion, no index changes, no commits. Layer: e2e_regression Feature state: active Exit code: 0 Invoker-Finalize-Id: 74c962cc-f188-4db3-be0c-c3da68c20e9a
…a23c8b9a3-964c6d87 — Terminal read-only gate confirming no ephemeral handoff files were left behind. Review claim: The workflow leaves no ephemeral handoff files in the tree. Review lane: proof Safety invariant: Read-only; it never deletes files, alters the index, or commits caller work. Effectiveness measurement: A non-zero exit when ephemeral handoff files remain is the signal. Slice rationale: One unit: the hygiene gate. Architectural effect: None. Goal: Confirm no ephemeral handoff files remain after every other task finishes. Motivation: Ephemeral inter-task files leak into the diff and read as part of the change. Alternative considerations: Manual inspection was set aside as non-deterministic. Implementation details: Run scripts/scrub-handoff-artifacts.sh in check mode. Non-goals: No deletion, no index changes, no commits. Layer: e2e_regression Feature state: active
… Put the categorical-scope-guard hook onto the shared hook code. Review claim: This hook reports findings to the shared hook code, which applies its registry mode and writes event rows. It keeps mode stop. Review lane: behavior Safety invariant: The hook gives the same stop, warn, or silent result on every case in its current test folder, except the mode change named in this claim, and its test folder keeps exiting 0. Effectiveness measurement: `python3 -m unittest discover -s engine/hooks/categorical-scope-guard/tests` exits 0, and the new mode-override case fails before this change. Slice rationale: One hook per workflow, as the user asked, so each migration is reviewed on its own. Architectural effect: The categorical-scope-guard entry scripts become thin calls into the shared runtime; its detection returns findings. Goal: Stops a change that covers only part of an all request. Keep that behavior while its mode moves into the registry. Motivation: Mode and output shape live inside each hook today; the shared code makes a mode change a one-line registry edit. Alternative considerations: Migrating several hooks per workflow was set aside because the user asked for one hook per workflow. Implementation details: Turn this hook's detection into detect(event) returning Finding objects with stable rule ids, and make each harness entry script call run_hook from engine/hooks/_sdk/runtime.py. It keeps mode stop. Non-goals: No change to what the hook detects. No other hook changes. Layer: domain Feature state: active Files: engine/hooks/categorical-scope-guard/claude_pretooluse.py, engine/hooks/categorical-scope-guard/detect.py, engine/hooks/categorical-scope-guard/install_claude_hook.py, engine/hooks/categorical-scope-guard/tests/test_hooks_sdk_mode.py Change types: - engine/hooks/categorical-scope-guard/claude_pretooluse.py: modify - engine/hooks/categorical-scope-guard/detect.py: modify - engine/hooks/categorical-scope-guard/install_claude_hook.py: modify - engine/hooks/categorical-scope-guard/tests/test_hooks_sdk_mode.py: create Acceptance criteria: - `python3 -m unittest discover -s engine/hooks/categorical-scope-guard/tests` exits 0. - With CATSTACK_HOOK_MODE_CATEGORICAL_SCOPE_GUARD set to warn, a case that stops today produces a warning instead, proving the registry mode drives the response. - Each finding writes one event row with the hook's rule_id. Solution: Put the categorical-scope-guard hook onto the shared hook code. Review claim: This hook reports findings to the shared hook code, which applies its registry mode and writes event rows. It keeps mode stop. Review lane: behavior Safety invariant: The hook gives the same stop, warn, or silent result on every case in its current test folder, except the mode change named in this claim, and its test folder keeps exiting 0. Effectiveness measurement: `python3 -m unittest discover -s engine/hooks/categorical-scope-guard/tests` exits 0, and the new mode-override case fails before this change. Slice rationale: One hook per workflow, as the user asked, so each migration is reviewed on its own. Architectural effect: The categorical-scope-guard entry scripts become thin calls into the shared runtime; its detection returns findings. Goal: Stops a change that covers only part of an all request. Keep that behavior while its mode moves into the registry. Motivation: Mode and output shape live inside each hook today; the shared code makes a mode change a one-line registry edit. Alternative considerations: Migrating several hooks per workflow was set aside because the user asked for one hook per workflow. Implementation details: Turn this hook's detection into detect(event) returning Finding objects with stable rule ids, and make each harness entry script call run_hook from engine/hooks/_sdk/runtime.py. It keeps mode stop. Non-goals: No change to what the hook detects. No other hook changes. Layer: domain Feature state: active Files: engine/hooks/categorical-scope-guard/claude_pretooluse.py, engine/hooks/categorical-scope-guard/detect.py, engine/hooks/categorical-scope-guard/install_claude_hook.py, engine/hooks/categorical-scope-guard/tests/test_hooks_sdk_mode.py Change types: - engine/hooks/categorical-scope-guard/claude_pretooluse.py: modify - engine/hooks/categorical-scope-guard/detect.py: modify - engine/hooks/categorical-scope-guard/install_claude_hook.py: modify - engine/hooks/categorical-scope-guard/tests/test_hooks_sdk_mode.py: create Acceptance criteria: - `python3 -m unittest discover -s engine/hooks/categorical-scope-guard/tests` exits 0. - With CATSTACK_HOOK_MODE_CATEGORICAL_SCOPE_GUARD set to warn, a case that stops today produces a warning instead, proving the registry mode drives the response. - Each finding writes one event row with the hook's rule_id. Invoker-Finalize-Id: 1540cec2-0149-477b-b36f-03c02bb58574
…n the deterministic proof for put the categorical-scope-guard hook onto the shared hook code. Review claim: The proof exits 0 only when put the categorical-scope-guard hook onto the shared hook code holds. Review lane: proof Safety invariant: Proof only; it changes no product behavior. Effectiveness measurement: The exit status of `python3 -m unittest discover -s engine/hooks/categorical-scope-guard/tests` is the signal for this slice. Slice rationale: One proof unit for this workflow. Architectural effect: None; verification only. Goal: Prove put the categorical-scope-guard hook onto the shared hook code with one deterministic run. Motivation: Each workflow carries its own proof so a reviewer can trust the slice alone. Alternative considerations: Manual inspection was set aside as non-deterministic. Implementation details: Execute the proof as a terminal gate. Non-goals: No product edits here. Layer: e2e_regression Feature state: active Exit code: 0 Invoker-Finalize-Id: 30fc01e9-dbf4-4302-9c20-a786ccb160ef
…only gate confirming no ephemeral handoff files were left behind. Review claim: The workflow leaves no ephemeral handoff files in the tree. Review lane: proof Safety invariant: Read-only; it never deletes files, alters the index, or commits caller work. Effectiveness measurement: A non-zero exit when ephemeral handoff files remain is the signal. Slice rationale: One unit: the hygiene gate. Architectural effect: None. Goal: Confirm no ephemeral handoff files remain after every other task finishes. Motivation: Ephemeral inter-task files leak into the diff and read as part of the change. Alternative considerations: Manual inspection was set aside as non-deterministic. Implementation details: Run scripts/scrub-handoff-artifacts.sh in check mode. Non-goals: No deletion, no index changes, no commits. Layer: e2e_regression Feature state: active Exit code: 0 Invoker-Finalize-Id: 85155944-670a-40b9-a845-1252af44dacd
…ad1e0ee86-5f6e3e25 — Terminal read-only gate confirming no ephemeral handoff files were left behind. Review claim: The workflow leaves no ephemeral handoff files in the tree. Review lane: proof Safety invariant: Read-only; it never deletes files, alters the index, or commits caller work. Effectiveness measurement: A non-zero exit when ephemeral handoff files remain is the signal. Slice rationale: One unit: the hygiene gate. Architectural effect: None. Goal: Confirm no ephemeral handoff files remain after every other task finishes. Motivation: Ephemeral inter-task files leak into the diff and read as part of the change. Alternative considerations: Manual inspection was set aside as non-deterministic. Implementation details: Run scripts/scrub-handoff-artifacts.sh in check mode. Non-goals: No deletion, no index changes, no commits. Layer: e2e_regression Feature state: active
…e-15-put-the-categorical-scope-guard-hook-onto-the-shared-hook-code Change-Id: I5b7e84437cae5ef1a737146b5366119f4c82ba7d # Conflicts: # engine/hooks/_sdk/runtime.py
|
Queued — the merge queue status continues in this comment ↓. |
…CHECKED check_no_silent_hook_except flagged the broad handler in detect() because it returned a finding without writing the error to stderr. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Change-Id: I10e84179b9915593bd69889b7ebe3ebdaed05060
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5bcd8b5. Configure here.
| try: | ||
| verdict = decide_payload(event) | ||
| except Exception as exc: | ||
| print(f"catstack-hook-error categorical-scope-guard: {type(exc).__name__}: {exc}", file=sys.stderr) |
There was a problem hiding this comment.
Detector prints crash line with finding
Low Severity
When decide_payload raises, detect writes a catstack-hook-error line to stderr and still returns an unchecked finding. Shared runtime already records and renders that finding, so the extra line prefixes the Claude block reason and makes the runner treat a warn-mode result as caught_error.
Reviewed by Cursor Bugbot for commit 5bcd8b5. Configure here.
|
@Mergifyio queue admin-bypass |
Merge Queue Status
This pull request spent 7 minutes 16 seconds in the queue, including 6 minutes 44 seconds running CI. Required conditions to merge
|
|
Mergify repair stopped: unresolved human review thread PRRT_kwDOT3uYWs6jOw4J |


Summary
This guard stops Claude from changing only some items when the user asked for all of them.
For example, it blocks a change that skips done tasks after the user said "all tasks."
The problem: each guard decided on its own whether to block or just warn. Changing that choice meant editing the guard's code.
The fix: this guard now only reports what it found. Shared code decides whether to block or warn, and logs one record per finding.
It still blocks by default, same as before. A local setting can now soften it to a warning.
Cursor and Codex now get their own entry scripts for this guard too.
Review Claim
This guard now reports what it found to the shared hook code, which picks block or warn from the shared settings and logs each finding. The default is still to block.
Review Lane
behavior
Review Unit
engine-runtime
Safety Invariant
The hook gives the same stop, warn, or silent result on every case in its current test folder, except the mode change named in this claim, and its test folder keeps exiting 0.
Slice Rationale
One hook per workflow, as the user asked, so each migration is reviewed on its own. This is part 15 of the hook architecture stack described in
docs/hook-architecture.md, stacked on the cat-mode-default migration (part 14).Non-goals
decide(),decide_payload(), and the transcript reader are untouched.install_claude_hook.pyorclaude.hook.json; Claude still runs the same entry script._sdkedit is the wording of the bad-JSON error line inruntime.py.Architecture
Before
graph TD A["claude_pretooluse.py"] --> B["parse stdin JSON"] B --> C["decide_payload()"] C -->|"HIT or UNCHECKED"| D["stderr message, exit 2"] C -->|"CLEAN"| E["exit 0"]After
graph TD A["claude / cursor / codex entry script"] --> R["_sdk runtime.run_hook()"] R --> F["detect(event) returns Finding list"] F --> C["decide_payload()"] R --> M["registry mode, default stop, env override"] R --> L["one event row per finding, with rule_id"] M -->|"stop"| D["block, exit 2"] M -->|"warn"| W["additionalContext warning, exit 0"]Rule ids:
categorical-scope-guard.partial-status-filterfor a HIT, andcategorical-scope-guard.uncheckedfor UNCHECKED or a detector crash. Under the defaultstopmode, both still block the command.Test Plan
Test Plan
python3 -m unittest discover -s engine/hooks/categorical-scope-guard/tests→Ran 45 tests in 0.637s/OKtests/test_hooks_sdk_mode.pycopied onto the base branch (part 14) →FAIL: test_each_finding_writes_one_event_row_with_rule_id,FAIL: test_mode_override_warn_changes_block_to_warning,FAILED (failures=2)update_tasks_status_in_pending_queued.txtfixture sent toclaude_pretooluse.pywithCATSTACK_HOOK_MODE_CATEGORICAL_SCOPE_GUARDunset, after a turn that said "all tasks" →default exit=2, stderr startscategorical-scope-guard: you said "can you make all tasks use claude and local executor". "all tasks" names every task, and this command narrows tasks by a status filterpython3 -m unittest discover -s engine/hooks/_sdk/tests→Ran 31 tests in 0.036s/OKpython3 scripts/ci/check_hook_test_coverage.py engine/hooks/categorical-scope-guard→check_hook_test_coverage: OK (1 hook(s) checked)python3 scripts/ci/check_hook_test_coverage.py engine/hooks/_sdk→check_hook_test_coverage: OK (1 hook(s) checked)python3 engine/skills/make-pr/scripts/preflight.py --base origin/plan/hook-architecture-14-put-the-cat-mode-default-hook-onto-the-shared-hook-code --body-file <this body>→declare Review Unit: engine-runtime, ruff gateAll checks passed!,description clean,ok preflight passednode engine/skills/draft-pr/scripts/validate-pr-body.mjs --body-file <this body> --changed-files-file <diff vs base>→PR body validation passed.bash scripts/scrub-handoff-artifacts.sh→ completed (workflow gate)Revert Plan
Revert Plan
git revert <merge-sha>🤖 Generated with Claude Code
Note
Medium Risk
Changes PreToolUse enforcement plumbing (centralized block/warn and metrics) for a guard that intercepts shell commands, though core
decide()logic is unchanged and default remains block.Overview
Migrates
categorical-scope-guardonto the shared hook SDK so detection returns structuredFindings instead of the entry script choosing exit codes directly.detect.pynow exposesdetect(event)with rule idscategorical-scope-guard.partial-status-filter(HIT) andcategorical-scope-guard.unchecked(UNCHECKED / detector failure), keyed by a hashed command subject.claude_pretooluse.pyis reduced torun_hook(..., detect, "PreToolUse"), and matchingcursor_pretooluse.py/codex_pretooluse.pyentrypoints are added. Block vs warn and per-finding metrics/event rows are handled by_sdk(effective_mode,render,write_events);CATSTACK_HOOK_MODE_CATEGORICAL_SCOPE_GUARD=warncan soften a hit to stdout context while still logging the finding.Docs now describe outcomes in terms of findings and registry default
stop. Shared runtime’s bad-JSON stderr line is slightly clarified. Newtest_hooks_sdk_mode.pycovers warn override and one event row per finding withrule_id.Reviewed by Cursor Bugbot for commit 5bcd8b5. Bugbot is set up for automated code reviews on this repo. Configure here.