From f1144412d416eb94c648ca8ccfc4344e99242eb1 Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Tue, 8 Sep 2026 22:01:00 -0700 Subject: [PATCH] install: wire ui-input-guard and hook-freshness into the inventories Both hooks join install.sh (link plus settings merge), the README hook table, the ecosystem ownership table, and test_install's per-hook link and event assertions. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KU2pPKob4MJ1NqjsfTNyYJ Change-Id: I09cebee8542c340a89165d7691256797e13fe7e6 --- README.md | 2 ++ docs/ecosystem.md | 2 ++ install.sh | 6 ++++++ tests/test_install.py | 18 ++++++++++++++++++ 4 files changed, 28 insertions(+) diff --git a/README.md b/README.md index 99035f89..75337c17 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,8 @@ Full sourcing notes, including what was left out and why: [docs/provenance.md](d | `new-file-callout` | A new untracked file at the repo root or under `scripts/`: the reply must name it and say why. | | `agent-relay-attribution` | Advisory: facts relayed from a subagent's report must say so or be re-verified. | | `scratchpad-collision` | Two agents writing the same scratchpad file within ten minutes: use a uniquely named file. | +| `ui-input-guard` | Synthetic keystrokes, clicks, or screen recording aimed at the user's own session: blocked unless a hands-off window is open, the screen is unlocked, and the user is idle. | +| `hook-freshness` | Advisory: the catstack checkout behind `~/.claude/hooks` is off `main` or behind `origin/main`, so merged hook fixes are not live on this machine. | | `auto-pr` | catstack itself changed: tell the agent to open a PR, no request needed. | | `cat-mode-default` | Every investigation or execution prompt, and every subagent prompt sent through the Agent tool: apply `cat-mode` without typing `/cat-mode`. Off unless `CATSTACK_CAT_MODE_DEFAULT=1` (env or `.env`; see `engine/hooks/cat-mode-default/README.md`). | | `plan-discipline` | **Not installed yet** (needs Agent mode): block product `.py` writes after a declined SwitchMode; require "How we test" on new-module plans; no eval numbers without a verifying run; warn on semantic plan-churn. Spec: `engine/hooks/plan-discipline/README.md`. | diff --git a/docs/ecosystem.md b/docs/ecosystem.md index c9a7b2ff..a06794fe 100644 --- a/docs/ecosystem.md +++ b/docs/ecosystem.md @@ -82,6 +82,8 @@ again. | `new-file-callout` | hook | | `agent-relay-attribution` | hook (advisory) | | `scratchpad-collision` | hook | +| `ui-input-guard` | hook | +| `hook-freshness` | hook (advisory) | | `engine/CLAUDE.core.md` | global hand-written Claude rules | | `scripts/`, `always-on/`, `cursor/rules/` (repo root), root `install.sh` | runtime (engine-owned entrypoints at root for CI) | diff --git a/install.sh b/install.sh index e146adf7..b136a8c5 100755 --- a/install.sh +++ b/install.sh @@ -239,6 +239,8 @@ link_item "hedge-runs-prove-it" "$REPO_DIR/engine/hooks/hedge-runs-prove-it" "$H link_item "new-file-callout" "$REPO_DIR/engine/hooks/new-file-callout" "$HOME/.claude/hooks/new-file-callout" link_item "agent-relay-attribution" "$REPO_DIR/engine/hooks/agent-relay-attribution" "$HOME/.claude/hooks/agent-relay-attribution" link_item "scratchpad-collision" "$REPO_DIR/engine/hooks/scratchpad-collision" "$HOME/.claude/hooks/scratchpad-collision" +link_item "ui-input-guard" "$REPO_DIR/engine/hooks/ui-input-guard" "$HOME/.claude/hooks/ui-input-guard" +link_item "hook-freshness" "$REPO_DIR/engine/hooks/hook-freshness" "$HOME/.claude/hooks/hook-freshness" echo "--- cursor hooks dir (\$HOME/.cursor/hooks) ---" mkdir -p "$HOME/.cursor/hooks" @@ -250,6 +252,7 @@ link_item "pr-schema-gate" "$REPO_DIR/engine/hooks/pr-schema-gate" "$HOME/.curso link_item "wrong-check-reflect" "$REPO_DIR/engine/hooks/wrong-check-reflect" "$HOME/.cursor/hooks/wrong-check-reflect" link_item "build-the-lever" "$REPO_DIR/engine/hooks/build-the-lever" "$HOME/.cursor/hooks/build-the-lever" link_item "repeat-error-stop" "$REPO_DIR/engine/hooks/repeat-error-stop" "$HOME/.cursor/hooks/repeat-error-stop" +link_item "ui-input-guard" "$REPO_DIR/engine/hooks/ui-input-guard" "$HOME/.cursor/hooks/ui-input-guard" echo "--- codex hooks (\$HOME/.codex/hooks) ---" mkdir -p "$HOME/.codex/hooks" @@ -260,6 +263,7 @@ link_item "pr-schema-gate" "$REPO_DIR/engine/hooks/pr-schema-gate" "$HOME/.codex link_item "wrong-check-reflect" "$REPO_DIR/engine/hooks/wrong-check-reflect" "$HOME/.codex/hooks/wrong-check-reflect" link_item "build-the-lever" "$REPO_DIR/engine/hooks/build-the-lever" "$HOME/.codex/hooks/build-the-lever" link_item "repeat-error-stop" "$REPO_DIR/engine/hooks/repeat-error-stop" "$HOME/.codex/hooks/repeat-error-stop" +link_item "ui-input-guard" "$REPO_DIR/engine/hooks/ui-input-guard" "$HOME/.codex/hooks/ui-input-guard" # Deleted worktrees leave symlinks behind that point into this repo but at a # path that no longer exists (e.g. .worktrees//engine/hooks/). @@ -334,6 +338,8 @@ python3 "$REPO_DIR/engine/hooks/hedge-runs-prove-it/install_claude_hook.py" python3 "$REPO_DIR/engine/hooks/new-file-callout/install_claude_hook.py" python3 "$REPO_DIR/engine/hooks/agent-relay-attribution/install_claude_hook.py" python3 "$REPO_DIR/engine/hooks/scratchpad-collision/install_claude_hook.py" +python3 "$REPO_DIR/engine/hooks/ui-input-guard/install_claude_hook.py" +python3 "$REPO_DIR/engine/hooks/hook-freshness/install_claude_hook.py" python3 "$REPO_DIR/scripts/prune_dead_hook_entries.py" echo "--- cursor bug-complaint-leak merge (\$HOME/.cursor/hooks.json) ---" diff --git a/tests/test_install.py b/tests/test_install.py index 08936296..c69eb906 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -189,6 +189,24 @@ def test_hedge_runs_prove_it_linked_and_stop_wired_for_claude(self): stop = [h["command"] for e in settings["hooks"]["Stop"] for h in e["hooks"]] self.assertTrue(any("hedge-runs-prove-it/claude_stop_check.py" in c for c in stop), stop) + def test_ui_input_guard_linked_and_pretooluse_wired_for_claude(self): + target = os.path.join(self.fake_home, ".claude", "hooks", "ui-input-guard") + self.assertTrue(os.path.islink(target), target) + self.assertEqual(os.readlink(target), hook_src("ui-input-guard")) + commands = self._claude_hook_commands("PreToolUse") + self.assertTrue( + any("ui-input-guard/claude_pretooluse_check.py" in c for c in commands), commands + ) + + def test_hook_freshness_linked_and_prompt_wired_for_claude(self): + target = os.path.join(self.fake_home, ".claude", "hooks", "hook-freshness") + self.assertTrue(os.path.islink(target), target) + self.assertEqual(os.readlink(target), hook_src("hook-freshness")) + commands = self._claude_hook_commands("UserPromptSubmit") + self.assertTrue( + any("hook-freshness/claude_prompt_submit.py" in c for c in commands), commands + ) + def test_new_file_callout_linked_and_stop_wired_for_claude(self): target = os.path.join(self.fake_home, ".claude", "hooks", "new-file-callout") self.assertTrue(os.path.islink(target), target)