Skip to content

[Hook architecture](16) Put the demo-freeze hook onto the shared hook code - #770

Open
EdbertChan wants to merge 26 commits into
plan/hook-architecture-15-put-the-categorical-scope-guard-hook-onto-the-shared-hook-codefrom
plan/hook-architecture-16-put-the-demo-freeze-hook-onto-the-shared-hook-code
Open

EdbertChan wants to merge 26 commits into
plan/hook-architecture-15-put-the-categorical-scope-guard-hook-onto-the-shared-hook-codefrom
plan/hook-architecture-16-put-the-demo-freeze-hook-onto-the-shared-hook-code

Conversation

@EdbertChan

@EdbertChan EdbertChan commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Summary

Claude runs small checker scripts before it edits a file. One of them stops edits to files the user is showing live, so the screen does not change mid-test.

The problem: that checker decided on its own how loud to be, and wrote its own output. Changing it meant editing the checker itself.

The fix: the checker now only reports what it found. Shared code picks stop, warn, or quiet, and logs each finding. It still stops by default.

Review Claim

The live-screen freeze checker now hands its findings to the shared checker code, which picks the response and logs each finding, while the default response stays "stop".

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

Part 16 of 50 in the hook architecture stack described in docs/hook-architecture.md. One hook per PR, so each migration is reviewed on its own. The review unit engine-runtime means the review group for code in the engine folder, such as hooks.

Non-goals

  • No change to what the hook detects: same marker file, same path matching, same 2-hour expiry, same message text.
  • No other hook changes.
  • No change to engine/hooks/demo-freeze/install_claude_hook.py; the plan listed it, but the entry command it installs did not need to change.
  • No change to the shared runtime under engine/hooks/_sdk/.

Architecture

Before

graph TD
    A["PreToolUse event on stdin"] --> B["claude_pretooluse_check.main() parses JSON"]
    B --> C["path matches frozen pattern?"]
    C -- yes --> D["write message to stderr, exit 2"]
    C -- no --> E["return, exit 0"]
Loading

After

graph TD
    A["PreToolUse event on stdin"] --> B["run_hook('demo-freeze', 'claude', detect, 'PreToolUse')"]
    B --> C["detect(event) returns Finding list"]
    C --> D["runtime reads mode (default stop, env CATSTACK_HOOK_MODE_DEMO_FREEZE)"]
    D --> E["stop: stderr + exit 2"]
    D --> F["warn: additionalContext JSON on stdout, exit 0"]
    D --> G["one event row per finding, rule_id demo-freeze.frozen-path"]
Loading

Test Plan

Test Plan
  • python3 -m unittest discover -s engine/hooks/demo-freeze/tests: Ran 10 tests in 0.022s / OK
  • python3 scripts/ci/check_hook_test_coverage.py engine/hooks/demo-freeze: check_hook_test_coverage: OK (1 hook(s) checked)
  • python3 engine/skills/make-pr/scripts/preflight.py --base <base> --body-file <body>: ok preflight passed (ruff E9/F: All checks passed!)
  • bash scripts/scrub-handoff-artifacts.sh: finished in the workflow's final check
  • New tests/test_hooks_sdk_mode.py:
    • test_mode_override_warn_changes_block_to_warning: with CATSTACK_HOOK_MODE_DEMO_FREEZE=warn, an edit to a frozen path exits 0 and emits hookSpecificOutput.additionalContext instead of stopping.
    • test_each_finding_writes_one_event_row_with_rule_id: default mode exits 2 and writes exactly one stopped row with rule_id demo-freeze.frozen-path.
  • tests/test_hooks.py: the no-marker case now tolerates the runtime's SystemExit(0); its assertion (empty stderr) is unchanged.

Revert Plan

Revert Plan
  • Safe to revert? Yes
  • Revert command: git revert <merge-sha>
  • Post-revert steps: None; the hook goes back to writing its own stderr message and exit code.
  • Data migration? No; event rows already written stay as history.

🤖 Generated with Claude Code


Note

Low Risk
Behavior-preserving refactor of one hook with expanded tests; default still blocks frozen paths with the same message.

Overview
Migrates the demo-freeze PreToolUse hook onto the shared _sdk runtime so detection is separate from how the harness responds.

claude_pretooluse_check.py no longer parses stdin or calls sys.exit(2) directly. A detect(event) function returns Finding objects (rule demo-freeze.frozen-path) when a tool targets a frozen path; main() delegates to run_hook("demo-freeze", "claude", detect, "PreToolUse"). Frozen-path logic, marker expiry, and the user-facing message text are unchanged — only who emits stderr/stdout and the exit code changes.

Tests: existing hook tests tolerate SystemExit from the runtime on the no-marker path. New test_hooks_sdk_mode.py covers CATSTACK_HOOK_MODE_DEMO_FREEZE=warn (exit 0 + additionalContext on stdout) and default stop mode with one metrics stopped row per finding.

Reviewed by Cursor Bugbot for commit 39c409f. Bugbot is set up for automated code reviews on this repo. Configure here.

Invoker Bot and others added 26 commits September 16, 2026 07:07
… 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
…e-14-put-the-cat-mode-default-hook-onto-the-shared-hook-code

Change-Id: I4bb19c1ea9dd0cef3dc21e32dcd9e66b5cf7f0a1
…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.

Exit code: 1
Invoker-Finalize-Id: d1bf500d-c8f0-47fc-8c0d-c636c7506b4c
…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: publish-approved-fix
…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: 5699f69c-cb1c-4890-b373-d374ac8be49f
…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: 946f5c14-9399-42ca-a370-bbe5a0fedc02
…a41c195df-751fc996

main already carries this workflow's cat-mode-default migration as #714
(Finding.output + shared renderer emits updatedInput). The branch had a
second implementation (runtime render_fn hook). Conflicts resolved to
main's landed version for detect.py, claude_pretooluse_agent.py,
test_hooks_sdk_mode.py, and the now-unused render_fn in runtime.py.
Kept from the branch: README mode-override docs (reworded to main's
mechanism) and metrics-dir isolation in the FailOpenCase tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… 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.

Exit code: 1
Invoker-Finalize-Id: 1e2b1721-154a-454f-a751-5d3992277030
… 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: publish-approved-fix
…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: 01877aec-305b-4f1e-a159-7e7dceb4eb8a
…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: 497f3e6c-8be6-461a-a001-72f4932e75bf
…a40fd4d22-84933511 — 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
…o-freeze 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/demo-freeze/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 demo-freeze entry scripts become thin calls into the shared runtime; its detection returns findings.
Goal: Stops edits to what the user is demoing. 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/demo-freeze/claude_pretooluse_check.py, engine/hooks/demo-freeze/install_claude_hook.py, engine/hooks/demo-freeze/tests/test_hooks_sdk_mode.py
Change types:
- engine/hooks/demo-freeze/claude_pretooluse_check.py: modify
- engine/hooks/demo-freeze/install_claude_hook.py: modify
- engine/hooks/demo-freeze/tests/test_hooks_sdk_mode.py: create
Acceptance criteria:
- `python3 -m unittest discover -s engine/hooks/demo-freeze/tests` exits 0.
- With CATSTACK_HOOK_MODE_DEMO_FREEZE 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.

Exit code: 1
Invoker-Finalize-Id: 04f511ed-29f7-452a-8036-8f716b1ef99c
The prior commit (266591b) recorded this task as done but its diff was
empty — the remote Codex run hit a usage-limit error before writing
any code, so claude_pretooluse_check.py never actually moved onto
engine/hooks/_sdk/runtime.py.

claude_pretooluse_check.py now exposes detect(event), which returns a
Finding (rule_id demo-freeze.frozen-path) for the first frozen-path
match, and main() is a thin call to run_hook("demo-freeze", "claude",
detect, "PreToolUse"), so the registry's mode (still "stop") and event
writing are handled by the shared runtime instead of hardcoded here.

tests/test_hooks.py: run_hook always calls sys.exit, so
test_no_marker_fails_open now wraps its main() call in
try/except SystemExit, matching the pattern already used elsewhere in
this test file's run_hook() helper.

tests/test_hooks_sdk_mode.py: new, asserts CATSTACK_HOOK_MODE_DEMO_FREEZE=warn
turns a stop into a warning, and that a stop writes one event row
tagged with the hook's rule_id.
…o-freeze 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/demo-freeze/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 demo-freeze entry scripts become thin calls into the shared runtime; its detection returns findings.
Goal: Stops edits to what the user is demoing. 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/demo-freeze/claude_pretooluse_check.py, engine/hooks/demo-freeze/install_claude_hook.py, engine/hooks/demo-freeze/tests/test_hooks_sdk_mode.py
Change types:
- engine/hooks/demo-freeze/claude_pretooluse_check.py: modify
- engine/hooks/demo-freeze/install_claude_hook.py: modify
- engine/hooks/demo-freeze/tests/test_hooks_sdk_mode.py: create
Acceptance criteria:
- `python3 -m unittest discover -s engine/hooks/demo-freeze/tests` exits 0.
- With CATSTACK_HOOK_MODE_DEMO_FREEZE 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.

Exit code: 0
Invoker-Finalize-Id: publish-approved-fix
…inistic proof for put the demo-freeze hook onto the shared hook code.

Review claim: The proof exits 0 only when put the demo-freeze 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/demo-freeze/tests` is the signal for this slice.
Slice rationale: One proof unit for this workflow.
Architectural effect: None; verification only.
Goal: Prove put the demo-freeze 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: 6fc62b2e-ec42-4f6e-bf06-8d3cff19523a
…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: fe00f104-876a-4ea0-af6a-06704d7b70c3
…-a37b95379-4ece69c4 — 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
@EdbertChan
EdbertChan force-pushed the plan/hook-architecture-15-put-the-categorical-scope-guard-hook-onto-the-shared-hook-code branch from b23ffd0 to f1c98a1 Compare September 17, 2026 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant