Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
90b2551
Add dormant hook runner metrics
Sep 12, 2026
0ff3ce7
invoker: wf-1789240290960-24/add-hook-runner — Review claim: engine/h…
Sep 12, 2026
5d48158
Describe hook runner metrics
Sep 12, 2026
f65a4f3
invoker: wf-1789240290960-24/describe-hook-runner — Review claim: eng…
Sep 12, 2026
ed0d35e
invoker: wf-1789240290960-24/verify-hook-runner — Review claim: The r…
Sep 12, 2026
39c4939
Invoker: merge experiment/wf-1789240290960-24/describe-hook-runner/g0…
Sep 12, 2026
6d256c4
invoker: wf-1789240290960-24/scrub-handoff-artifacts — Review claim: …
Sep 12, 2026
6ae30ce
Merge experiment/wf-1789240290960-24/scrub-handoff-artifacts/g0.t0.a-…
EdbertChan Sep 12, 2026
6a83264
fix(hooks/_runner): run on Python 3.9 and forward the hook result whe…
EdbertChan Sep 12, 2026
d1a6fb5
Wrap installed hooks with runner
Sep 12, 2026
db5412e
invoker: wf-1789240443754-26/wrap-installed-hooks — Review claim: ./i…
Sep 12, 2026
86cc3b0
Report unwrapped installed hooks
Sep 12, 2026
a6918e5
invoker: wf-1789240443754-26/install-check-flags-bypass — Review clai…
Sep 12, 2026
f7ecc41
docs: describe hook runner install wrapping
Sep 12, 2026
f994b87
invoker: wf-1789240443754-26/describe-wrap-install — Review claim: en…
Sep 12, 2026
14a255b
invoker: wf-1789240443754-26/verify-wrap-installed — Review claim: Th…
Sep 12, 2026
1c42f81
invoker: wf-1789240443754-26/verify-wrap-installed — Review claim: Th…
Sep 12, 2026
1676bcc
Invoker: merge experiment/wf-1789240443754-26/describe-wrap-install/g…
Sep 12, 2026
607810d
invoker: wf-1789240443754-26/scrub-handoff-artifacts — Review claim: …
Sep 12, 2026
e842221
Merge experiment/wf-1789240443754-26/scrub-handoff-artifacts/g0.t1.a-…
EdbertChan Sep 12, 2026
36ef709
fix(install): prune dead hook entries after they are wrapped by the r…
EdbertChan Sep 12, 2026
53ae4f1
Add hook metrics report and health notice
Sep 12, 2026
989eb92
invoker: wf-1789240464119-27/add-report-and-notice — Review claim: re…
Sep 12, 2026
7470a70
docs: describe hook health report and notice
EdbertChan Sep 12, 2026
6116366
invoker: wf-1789240464119-27/describe-report-and-notice — Review clai…
Sep 12, 2026
8132c20
invoker: wf-1789240464119-27/verify-report-and-notice — Review claim:…
Sep 12, 2026
5500592
Invoker: merge experiment/wf-1789240464119-27/describe-report-and-not…
Sep 12, 2026
59cd097
invoker: wf-1789240464119-27/scrub-handoff-artifacts — Review claim: …
Sep 12, 2026
605ba6a
Merge experiment/wf-1789240464119-27/scrub-handoff-artifacts/g0.t1.a-…
EdbertChan Sep 12, 2026
75f467a
Report caught hook exceptions
Sep 12, 2026
a64b793
invoker: wf-1789240490883-28/report-caught-hook-errors — Review claim…
Sep 12, 2026
8b8bbc9
invoker: wf-1789240490883-28/verify-caught-hook-errors — Review claim…
Sep 12, 2026
2f8399d
invoker: wf-1789240490883-28/scrub-handoff-artifacts — Review claim: …
Sep 12, 2026
3dbebbc
Merge experiment/wf-1789240490883-28/scrub-handoff-artifacts/g0.t1.a-…
EdbertChan Sep 12, 2026
7732f8d
test(check_no_silent_hook_except): compare against the resolved temp …
EdbertChan Sep 12, 2026
eda4609
Merge remote-tracking branch 'origin/main' into m514
EdbertChan Sep 13, 2026
7de7a7b
Merge of #514
mergify[bot] Sep 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
- name: hook e2e coverage gate (positive + negative test per detector)
run: python3 scripts/check_hook_test_coverage.py

- name: silent hook exception gate
run: python3 scripts/check_no_silent_hook_except.py

- name: rule wiring gate (every phrase checker has a caller or a stated reason)
run: python3 scripts/check_rules_are_wired.py

Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/answer-overrides-menu/claude_posttooluse.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def main() -> None:
return
try:
text = decide(payload)
except Exception:
except Exception as exc:
print(f"catstack-hook-error answer-overrides-menu: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if not text:
return
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/auto-pr/claude_stop_autopr.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def main() -> None:
deliver=False,
debounce=True,
)
except Exception:
except Exception as exc:
print(f"catstack-hook-error auto-pr: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if not message:
return
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/auto-pr/cursor_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ def main() -> None:
payload if isinstance(payload, dict) else {},
argv=sys.argv[1:],
)
except Exception:
except Exception as exc:
print(f"catstack-hook-error auto-pr: {type(exc).__name__}: {exc}", file=sys.stderr)
print(json.dumps({"followup_message": ""}))
return
print(json.dumps({"followup_message": message or ""}))
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/bug-complaint-leak/claude_posttooluse.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ def main() -> None:
return
try:
process(payload)
except Exception:
except Exception as exc:
print(f"catstack-hook-error bug-complaint-leak: {type(exc).__name__}: {exc}", file=sys.stderr)
return


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/bug-complaint-leak/claude_pretooluse_grep.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ def main() -> None:
from state import save_state

save_state(payload, state)
except Exception:
except Exception as exc:
print(f"catstack-hook-error bug-complaint-leak: {type(exc).__name__}: {exc}", file=sys.stderr)
return


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/bug-complaint-leak/claude_prompt_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def main() -> None:
}
)
)
except Exception:
except Exception as exc:
print(f"catstack-hook-error bug-complaint-leak: {type(exc).__name__}: {exc}", file=sys.stderr)
return


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/bug-complaint-leak/cursor_before_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def main() -> None:
quotes = extract_quoted_symptoms(prompt)
remember_bug_complaint(payload, prompt, quotes, checklist)
print(json.dumps({"continue": True}))
except Exception:
except Exception as exc:
print(f"catstack-hook-error bug-complaint-leak: {type(exc).__name__}: {exc}", file=sys.stderr)
print(json.dumps({"continue": True}))


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/bug-complaint-leak/cursor_post_tool_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def main() -> None:
state["cursor_checklist_pending"] = False
save_state(payload, state)
print(json.dumps({"additional_context": checklist}))
except Exception:
except Exception as exc:
print(f"catstack-hook-error bug-complaint-leak: {type(exc).__name__}: {exc}", file=sys.stderr)
return


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/build-the-lever/claude_posttooluse.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def main() -> None:
}
)
)
except Exception:
except Exception as exc:
print(f"catstack-hook-error build-the-lever: {type(exc).__name__}: {exc}", file=sys.stderr)
return


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/build-the-lever/claude_prompt_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def main() -> None:
}
)
)
except Exception:
except Exception as exc:
print(f"catstack-hook-error build-the-lever: {type(exc).__name__}: {exc}", file=sys.stderr)
return


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/build-the-lever/codex_posttooluse.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def main() -> None:
}
)
)
except Exception:
except Exception as exc:
print(f"catstack-hook-error build-the-lever: {type(exc).__name__}: {exc}", file=sys.stderr)
return


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/build-the-lever/codex_prompt_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def main() -> None:
}
)
)
except Exception:
except Exception as exc:
print(f"catstack-hook-error build-the-lever: {type(exc).__name__}: {exc}", file=sys.stderr)
return


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/build-the-lever/cursor_before_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def main() -> None:
if is_bulk_work(extract_prompt_text(payload if isinstance(payload, dict) else {})):
remember_bulk_prompt(payload)
print(json.dumps({"continue": True}))
except Exception:
except Exception as exc:
print(f"catstack-hook-error build-the-lever: {type(exc).__name__}: {exc}", file=sys.stderr)
print(json.dumps({"continue": True}))


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/build-the-lever/cursor_post_tool_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def main() -> None:
record_file_mutation(payload if isinstance(payload, dict) else {})
if consume_prompt_pending(payload) or should_inject_for_edits(payload):
print(json.dumps({"additional_context": reminder_text()}))
except Exception:
except Exception as exc:
print(f"catstack-hook-error build-the-lever: {type(exc).__name__}: {exc}", file=sys.stderr)
return


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/cat-mode-default/claude_pretooluse_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ def main() -> None:
return
try:
updated = agent_updated_input(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error cat-mode-default: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if updated is None:
return
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/cat-mode-default/claude_prompt_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def main() -> None:
return
try:
context = decide(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error cat-mode-default: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if context is None:
return
Expand Down
2 changes: 2 additions & 0 deletions engine/hooks/categorical-scope-guard/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
mutation by status but the filter values or the human turns could not be read.
"""
from __future__ import annotations
import sys

import json
import os
Expand Down Expand Up @@ -830,6 +831,7 @@ def decide(command: str, window_loader) -> Verdict:
try:
found = scoped_mutation_filters(command)
except Exception as exc:
print(f"catstack-hook-error categorical-scope-guard: {type(exc).__name__}: {exc}", file=sys.stderr)
return Verdict(UNCHECKED, (
f"categorical-scope-guard: UNCHECKED -- the command parser failed ({exc!r}) on a command that "
"mentions a status or state. Blocked rather than passed; drop the status filter or rephrase the command."
Expand Down
1 change: 1 addition & 0 deletions engine/hooks/external-claim-gate/claude_pretooluse.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def main() -> None:
try:
findings = evaluate(command, cwd)
except Exception as exc:
print(f"catstack-hook-error external-claim-gate: {type(exc).__name__}: {exc}", file=sys.stderr)
_refuse_if_destination(command, f"the detector failed ({exc!r})")
return
if findings:
Expand Down
4 changes: 3 additions & 1 deletion engine/hooks/frustration-watchdog/claude_stop_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@ def main():
try:
msgs = human_user_messages(transcript_path)
kinds = impatience_kinds(msgs)
except Exception:
except Exception as exc:
print(f"catstack-hook-error frustration-watchdog: {type(exc).__name__}: {exc}", file=sys.stderr)
return # fail open: a broken watchdog must never brick a session
if not kinds:
return
Expand All @@ -227,6 +228,7 @@ def main():
refused = turn_has_hook_refusal(transcript_path)
unchecked = None
except Exception as e:
print(f"catstack-hook-error frustration-watchdog: {type(e).__name__}: {e}", file=sys.stderr)
refused = False
unchecked = f"{type(e).__name__}: {e}"
head = (
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/frustration-watchdog/tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ def test_unreadable_tool_results_use_default_wording_and_say_so(self):
with patch.object(claude_stop_check, "turn_has_hook_refusal", side_effect=OSError("disk gone")):
blocked, err = self.run_lines([human(WAITING)] + tool_turn(HOOK_REFUSAL_TEXT))
self.assertTrue(blocked)
self.assertTrue(err.startswith(DEFAULT_WAITING_FEEDBACK))
self.assertTrue(err.startswith("catstack-hook-error frustration-watchdog: OSError: disk gone\n"))
self.assertIn(DEFAULT_WAITING_FEEDBACK, err)
self.assertIn("could not read this turn's tool results (OSError: disk gone)", err)


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/hook-freshness/claude_prompt_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def main() -> None:
return
try:
out = decide_json(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error hook-freshness: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if out:
print(out)
Expand Down
1 change: 1 addition & 0 deletions engine/hooks/llm-judge/judge.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ def run_job(path: str) -> dict:
job.setdefault("id", stem)
result = verdict(job, ask(str(job["prompt"])))
except Exception as exc:
print(f"catstack-hook-error llm-judge: {type(exc).__name__}: {exc}", file=sys.stderr)
log(f"job {job.get('id')} failed: {type(exc).__name__}: {exc}\n{traceback.format_exc()}")
result = verdict(job, {"outcome": "unchecked", "attempts": []})
result["reason"] = clip(f"judge error {type(exc).__name__}", str(exc))
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/named-verb-guard/claude_stop_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ def main() -> None:
return
try:
message = decide(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error named-verb-guard: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if not message:
return
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/narrow-the-scope/claude_posttooluse.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ def main() -> None:
return
try:
text = observe(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error narrow-the-scope: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if not text:
return
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/no-comments/claude_pretooluse.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ def main() -> None:
return
try:
message = decide(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error no-comments: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if not message:
return
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/playbook-router/claude_prompt_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ def main() -> None:
return
try:
context = decide(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error playbook-router: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if context is None:
return
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/prove-it-ship-gate/claude_stop_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ def main() -> None:
return
try:
message = decide(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error prove-it-ship-gate: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if not message:
return
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/reflect-on-thrash/claude_stop_reflect.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def main() -> None:
message = decide(
payload if isinstance(payload, dict) else {},
)
except Exception:
except Exception as exc:
print(f"catstack-hook-error reflect-on-thrash: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if message:
sys.stderr.write(message + "\n")
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/reflect-on-thrash/cursor_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def main() -> None:
payload if isinstance(payload, dict) else {},
argv=sys.argv[1:],
)
except Exception:
except Exception as exc:
print(f"catstack-hook-error reflect-on-thrash: {type(exc).__name__}: {exc}", file=sys.stderr)
print(json.dumps({"followup_message": ""}))
return
print(json.dumps({"followup_message": message or ""}))
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/reflect-on-thrash/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ def thrash_hits(path: str) -> list[str]:
result = token_audit.audit_codex(path)
return _hits_from_flags(list(result.get("flags") or []))
return _cursor_duplicate_hits(path)
except Exception:
except Exception as exc:
print(f"catstack-hook-error reflect-on-thrash: {type(exc).__name__}: {exc}", file=sys.stderr)
return []


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/repeat-error-stop/claude_posttooluse.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ def main() -> None:
try:
payload = json.load(sys.stdin)
kind, reason = record_result(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error repeat-error-stop: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if kind == "block":
print(json.dumps({
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/repeat-error-stop/claude_pretooluse.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def main() -> None:
try:
payload = json.load(sys.stdin)
blocked, reason = tool_block_reason(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error repeat-error-stop: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if blocked:
sys.stderr.write(reason + "\n")
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/repeat-error-stop/claude_prompt_reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def main() -> None:
try:
payload = json.load(sys.stdin)
handle_prompt(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error repeat-error-stop: {type(exc).__name__}: {exc}", file=sys.stderr)
return


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/repeat-error-stop/codex_posttooluse.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def main() -> None:
try:
payload = json.load(sys.stdin)
kind, reason = record_result(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error repeat-error-stop: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if kind == "block":
print(json.dumps({
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/repeat-error-stop/codex_pretooluse.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def main() -> None:
try:
payload = json.load(sys.stdin)
blocked, reason = tool_block_reason(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error repeat-error-stop: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if blocked:
print(json.dumps({
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/repeat-error-stop/codex_prompt_reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def main() -> None:
try:
payload = json.load(sys.stdin)
handle_prompt(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error repeat-error-stop: {type(exc).__name__}: {exc}", file=sys.stderr)
return


Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/repeat-error-stop/cursor_before_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def main() -> None:
try:
payload = json.load(sys.stdin)
handle_prompt(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error repeat-error-stop: {type(exc).__name__}: {exc}", file=sys.stderr)
pass
print(json.dumps({"continue": True}))

Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/repeat-error-stop/cursor_post_tool_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def main() -> None:
try:
payload = json.load(sys.stdin)
kind, reason = record_result(payload if isinstance(payload, dict) else {})
except Exception:
except Exception as exc:
print(f"catstack-hook-error repeat-error-stop: {type(exc).__name__}: {exc}", file=sys.stderr)
return
if kind in ("block", "nudge"):
print(json.dumps({"additional_context": reason}))
Expand Down
Loading
Loading