diff --git a/engine/hooks/cat-mode-default/README.md b/engine/hooks/cat-mode-default/README.md index dc571635..d293dfc9 100644 --- a/engine/hooks/cat-mode-default/README.md +++ b/engine/hooks/cat-mode-default/README.md @@ -60,13 +60,22 @@ It stays silent when the flag is off or when the prompt already mentions cat-mode anywhere (a parent that told the subagent to read it gets no second copy). Same flag resolution as the prompt hook. +The shared registry keeps this hook in `warn` mode. Set +`CATSTACK_HOOK_MODE_CAT_MODE_DEFAULT=off|warn|stop` for a machine-local +override; `stop` turns the `PreToolUse` (Agent) companion into a real block +(exit 2) instead of rewriting the subagent's prompt. Detection or metrics +failures allow the harness action. + ## Files -- `detect.py`: flag resolution, typed `/cat-mode` detection, context text. -- `claude_prompt_submit.py`: the Claude entrypoint; fail-open, never denies. -- `claude.prompt.hook.json`: settings fragment `install_claude_hook.py` merges. -- `claude_pretooluse_agent.py` + `claude.agent.hook.json`: the `PreToolUse` - (`Agent`) companion that carries the default into subagent prompts. +- `detect.py`: flag resolution, typed `/cat-mode` detection, context text, + and `detect(event)`, the SDK entrypoint returning `Finding` objects. +- `claude_prompt_submit.py` / `claude_pretooluse_agent.py`: thin calls into + `engine/hooks/_sdk/runtime.py`. The agent finding carries + `output={"updatedInput": ...}`, which the shared renderer emits as + `updatedInput` rather than its generic `additionalContext`. +- `claude.prompt.hook.json` / `claude.agent.hook.json`: settings fragments + `install_claude_hook.py` merges. - `tests/fixtures/*.json`: one scenario each (fires / silent) with the environment, optional `.env` content, and payload. - `tests/fixtures/agent_*.json`: the same for the Agent-tool companion. diff --git a/engine/hooks/cat-mode-default/tests/test_agent_hook.py b/engine/hooks/cat-mode-default/tests/test_agent_hook.py index c87cfafd..c4427043 100644 --- a/engine/hooks/cat-mode-default/tests/test_agent_hook.py +++ b/engine/hooks/cat-mode-default/tests/test_agent_hook.py @@ -13,6 +13,7 @@ import json import os import sys +import tempfile import unittest from contextlib import redirect_stderr, redirect_stdout from unittest.mock import patch @@ -150,6 +151,19 @@ def test_unrelated_prompt_does_not_count(self) -> None: class FailOpenCase(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + self.metrics_env = patch.dict( + os.environ, + {"CATSTACK_HOOK_METRICS_DIR": self.tmp.name}, + clear=False, + ) + self.metrics_env.start() + + def tearDown(self) -> None: + self.metrics_env.stop() + self.tmp.cleanup() + def test_malformed_stdin_prints_nothing(self) -> None: out = io.StringIO() with patch.object(sys, "stdin", io.StringIO("not json")): diff --git a/engine/hooks/cat-mode-default/tests/test_hooks.py b/engine/hooks/cat-mode-default/tests/test_hooks.py index 9a080562..61b0a782 100644 --- a/engine/hooks/cat-mode-default/tests/test_hooks.py +++ b/engine/hooks/cat-mode-default/tests/test_hooks.py @@ -260,6 +260,19 @@ def test_flags_missing_install_instead_of_dead_path(self) -> None: class FailOpenCase(unittest.TestCase): + def setUp(self) -> None: + self.tmp = tempfile.TemporaryDirectory() + self.metrics_env = patch.dict( + os.environ, + {"CATSTACK_HOOK_METRICS_DIR": self.tmp.name}, + clear=False, + ) + self.metrics_env.start() + + def tearDown(self) -> None: + self.metrics_env.stop() + self.tmp.cleanup() + def test_malformed_stdin_prints_nothing(self) -> None: out = io.StringIO() with patch.object(sys, "stdin", io.StringIO("not json")):