Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
027cc35
invoker: wf-1789406883560-32/implement-hook-build-the-lever — Put the…
Sep 16, 2026
bbebd6e
invoker: wf-1789406883560-32/verify-hook-build-the-lever — Run the de…
Sep 16, 2026
5c0096b
invoker: wf-1789406883560-32/scrub-handoff-artifacts — Terminal read-…
Sep 16, 2026
eccbe83
Merge experiment/wf-1789406883560-32/scrub-handoff-artifacts/g1.t5.a-…
EdbertChan Sep 16, 2026
7757396
invoker: wf-1789406886887-33/implement-hook-cat-mode-default — Put th…
Sep 16, 2026
13ec88c
invoker: wf-1789406886887-33/verify-hook-cat-mode-default — Run the d…
Sep 16, 2026
163d9e8
invoker: wf-1789406886887-33/scrub-handoff-artifacts — Terminal read-…
Sep 16, 2026
87798db
Merge experiment/wf-1789406886887-33/scrub-handoff-artifacts/g1.t5.a-…
EdbertChan Sep 16, 2026
49b348f
invoker: wf-1789406891093-34/implement-hook-categorical-scope-guard —…
Sep 16, 2026
fcfbe21
invoker: wf-1789406891093-34/verify-hook-categorical-scope-guard — Ru…
Sep 16, 2026
0cc8306
invoker: wf-1789406891093-34/scrub-handoff-artifacts — Terminal read-…
Sep 16, 2026
2b76f0a
Merge experiment/wf-1789406891093-34/scrub-handoff-artifacts/g1.t5.a-…
EdbertChan Sep 16, 2026
c67ac17
invoker: wf-1789406894793-35/implement-hook-demo-freeze — Put the dem…
Sep 16, 2026
60c89ff
invoker: wf-1789406894793-35/implement-hook-demo-freeze — Put the dem…
Sep 16, 2026
b9e01d7
invoker: wf-1789406894793-35/verify-hook-demo-freeze — Run the determ…
Sep 16, 2026
63cf3dd
invoker: wf-1789406894793-35/scrub-handoff-artifacts — Terminal read-…
Sep 16, 2026
f60edf6
Merge experiment/wf-1789406894793-35/scrub-handoff-artifacts/g1.t8.a-…
EdbertChan Sep 16, 2026
bec4b7a
invoker: wf-1789406897817-36/implement-hook-diu-stop — Put the diu-st…
Sep 16, 2026
9883c5e
invoker: wf-1789406897817-36/implement-hook-diu-stop — Put the diu-st…
Sep 16, 2026
e7e865b
invoker: wf-1789406897817-36/verify-hook-diu-stop — Run the determini…
Sep 16, 2026
0343ab7
invoker: wf-1789406897817-36/scrub-handoff-artifacts — Terminal read-…
Sep 16, 2026
5920777
Merge experiment/wf-1789406897817-36/scrub-handoff-artifacts/g1.t9.a-…
EdbertChan Sep 16, 2026
281e4f2
Merge remote-tracking branch 'origin/main' into plan/hook-architectur…
EdbertChan Sep 17, 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
26 changes: 15 additions & 11 deletions engine/hooks/diu-stop/claude_prompt_reminder.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@
reminder repeated every turn is exactly the kind of thing this skill tells
the model to cut.
"""
import json
import os
import sys

from diu_limit import rule_text

sys.path.insert(0, os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_sdk"))

from finding import Finding # noqa: E402
from runtime import run_hook # noqa: E402

RULE_REMINDER = "diu-stop.prompt-reminder"

REMINDER = (
"diu reminder: lead with the outcome, no preamble or closing "
f"pleasantries, ELI5 {rule_text()}, unless this turn needs technical "
Expand All @@ -29,17 +37,13 @@
)


def detect(event):
subject = event.get("session_id") or ""
return [Finding(rule_id=RULE_REMINDER, subject=subject, message=REMINDER, evidence=REMINDER)]


def main():
try:
json.load(sys.stdin)
except json.JSONDecodeError:
return
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": REMINDER,
}
}))
run_hook("diu-stop", "claude", detect, "UserPromptSubmit")
Comment thread
cursor[bot] marked this conversation as resolved.


if __name__ == "__main__":
Expand Down
62 changes: 40 additions & 22 deletions engine/hooks/diu-stop/claude_stop_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
Every block names every flagged sentence, so one rewrite that fixes them
all gets through.
"""
import json
import os
import re
import sys
Expand All @@ -55,8 +54,18 @@

sys.path.insert(0, os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_markers"))
sys.path.insert(0, os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_sdk"))

import markers # noqa: E402
from finding import Finding # noqa: E402
from runtime import run_hook # noqa: E402

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Installed hooks cannot import shared SDK

High Severity

The new scripts import finding and runtime from a sibling _sdk directory, but install.sh never links _sdk into ~/.claude/hooks or ~/.codex/hooks. After install, each entrypoint looks next to those hook roots, misses the package, and fails on import. The Stop check, prompt reminder, and Codex notify then never run, so long or unproven replies are no longer blocked.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 281e4f2. Configure here.


RULE_WORD_LIMIT = "diu-stop.word-limit"
RULE_UNVERIFIED_CLAIM = "diu-stop.unverified-claim"
RULE_MALFORMED_MARKER_TAG = "diu-stop.malformed-marker-tag"
RULE_LEGACY_MARKER = "diu-stop.legacy-marker"
RULE_PLAIN_WORDS = "diu-stop.plain-words"

# Phrases banned outright (from this user's global CLAUDE.md evidence
# rules) -- rarely legitimate even mid-sentence, so no opener restriction.
Expand Down Expand Up @@ -219,31 +228,28 @@ def find_unverified_claim(message):
return claims[0][0] if claims else None


def main():
try:
data = json.load(sys.stdin)
except json.JSONDecodeError:
return

if data.get("agent_id"):
return
retry = bool(data.get("stop_hook_active"))
def detect(event):
if event.get("agent_id"):
return []
retry = bool(event.get("stop_hook_active"))

message = data.get("last_assistant_message") or ""
message = event.get("last_assistant_message") or ""

plain_words_note = try_check_reply(data)
plain_words_note = try_check_reply(event)

word_count = counted_words(message)
over_limit = word_count > WORD_LIMIT and not retry
claims = find_unverified_claims(message)
marker_problems = find_marker_problems(message)

if not over_limit and not claims and not marker_problems and not plain_words_note:
return

parts = []
findings = []
if plain_words_note:
parts.append(plain_words_note)
findings.append(Finding(
rule_id=RULE_PLAIN_WORDS,
subject=message,
message=plain_words_note,
evidence=plain_words_note,
))
if claims:
lines = [
"This message makes an unverified-shaped claim with no adjacent "
Expand All @@ -259,18 +265,30 @@ def main():
"of what was actually run/checked in its paragraph, or -- only if "
"the check cannot run -- tag the claim there and say why."
)
parts.append("\n".join(lines))
parts.extend(marker_problems)
claim_message = "\n".join(lines)
findings.append(Finding(
rule_id=RULE_UNVERIFIED_CLAIM,
subject=claim_message,
message=claim_message,
evidence=claim_message,
))
for problem in marker_problems:
rule_id = RULE_MALFORMED_MARKER_TAG if problem == markers.MALFORMED_TAG_MESSAGE else RULE_LEGACY_MARKER
findings.append(Finding(rule_id=rule_id, subject=message, message=problem, evidence=problem))
if over_limit:
parts.append(
over_message = (
f"Apply diu: {word_count} words, over the {WORD_LIMIT}-word "
f"guideline. Cut at least {word_count - WORD_LIMIT} words by "
"dropping a whole section or list, not by trimming words. "
"Unless this turn genuinely asked for full technical detail "
"or a specific long format."
)
sys.stderr.write("\n".join(parts) + "\n")
sys.exit(2)
findings.append(Finding(rule_id=RULE_WORD_LIMIT, subject=message, message=over_message, evidence=over_message))
return findings


def main():
run_hook("diu-stop", "claude", detect, "Stop")


if __name__ == "__main__":
Expand Down
44 changes: 34 additions & 10 deletions engine/hooks/diu-stop/codex_notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,34 @@
becomes `old-notify-binary some-arg <json-payload>` when this fires.
"""
import json
import os
import subprocess
import sys

sys.path.insert(0, os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_sdk"))

from events import write_events # noqa: E402
from finding import Finding # noqa: E402
from modes import effective_mode # noqa: E402

WORD_LIMIT = 150
RULE_WORD_LIMIT = "diu-stop.word-limit"


def detect(payload):
if payload.get("type") != "agent-turn-complete":
return []
message = payload.get("last-assistant-message") or ""
word_count = len(message.split())
if word_count <= WORD_LIMIT:
return []
message_text = (
f"diu-stop: last response was {word_count} words (over the "
f"{WORD_LIMIT}-word diu guideline). Codex can't be forced to redo "
"it -- check by hand whether it should have been ELI5."
)
return [Finding(rule_id=RULE_WORD_LIMIT, subject=message, message=message_text, evidence=message_text)]


def main():
Expand All @@ -40,18 +64,18 @@ def main():
except json.JSONDecodeError:
return

if payload.get("type") != "agent-turn-complete":
findings = detect(payload)
if not findings:
return

message = payload.get("last-assistant-message") or ""
word_count = len(message.split())
if word_count > WORD_LIMIT:
print(
f"diu-stop: last response was {word_count} words (over the "
f"{WORD_LIMIT}-word diu guideline). Codex can't be forced to redo "
"it -- check by hand whether it should have been ELI5.",
file=sys.stderr,
)
mode, mode_source = effective_mode("diu-stop", payload)
if mode == "off":
return

for finding in findings:
print(finding.message, file=sys.stderr)

write_events("diu-stop", "codex", payload, findings, mode, mode_source, 0)
Comment thread
cursor[bot] marked this conversation as resolved.


if __name__ == "__main__":
Expand Down
15 changes: 12 additions & 3 deletions engine/hooks/diu-stop/tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ def run_prompt_reminder(stdin_obj):
buf = io.StringIO()
with patch.object(sys, "stdin", io.StringIO(json.dumps(stdin_obj))):
with redirect_stdout(buf):
claude_prompt_reminder.main()
try:
claude_prompt_reminder.main()
except SystemExit:
pass
Comment thread
cursor[bot] marked this conversation as resolved.
return buf.getvalue()


Expand Down Expand Up @@ -140,7 +143,10 @@ def test_malformed_stdin_json_does_not_crash(self):
buf = io.StringIO()
with patch.object(sys, "stdin", io.StringIO("not json")):
with redirect_stdout(buf):
claude_stop_check.main() # must not raise
try:
claude_stop_check.main()
except SystemExit:
pass
self.assertEqual(buf.getvalue(), "")


Expand Down Expand Up @@ -176,7 +182,10 @@ def test_malformed_stdin_json_does_not_crash(self):
buf = io.StringIO()
with patch.object(sys, "stdin", io.StringIO("not json")):
with redirect_stdout(buf):
claude_prompt_reminder.main() # must not raise
try:
claude_prompt_reminder.main()
except SystemExit:
pass
self.assertEqual(buf.getvalue(), "")


Expand Down
79 changes: 79 additions & 0 deletions engine/hooks/diu-stop/tests/test_hooks_sdk_mode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from __future__ import annotations

import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

HERE = os.path.dirname(os.path.abspath(__file__))
HOOK_DIR = os.path.dirname(HERE)
ENTRYPOINT = os.path.join(HOOK_DIR, "claude_stop_check.py")

LONG_MESSAGE = " ".join(["word"] * 200)


def run_entrypoint(payload: dict[str, object], env: dict[str, str]) -> subprocess.CompletedProcess[str]:
merged_env = os.environ.copy()
merged_env.update(env)
return subprocess.run(
[sys.executable, ENTRYPOINT],
input=json.dumps(payload),
capture_output=True,
text=True,
env=merged_env,
)


class SdkModeTest(unittest.TestCase):
def test_mode_override_warn_changes_block_to_warning(self) -> None:
result = run_entrypoint(
{"last_assistant_message": LONG_MESSAGE},
{"CATSTACK_HOOK_MODE_DIU_STOP": "warn"},
)

self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual("", result.stderr)
rendered = json.loads(result.stdout)
self.assertIn(
"Apply diu",
rendered["hookSpecificOutput"]["additionalContext"],
)

def test_mode_stop_still_blocks_by_default(self) -> None:
result = run_entrypoint({"last_assistant_message": LONG_MESSAGE}, {})

self.assertEqual(2, result.returncode)
self.assertIn("Apply diu", result.stderr)

def test_each_finding_writes_one_event_row_with_rule_id(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
result = run_entrypoint(
{
"session_id": "diu-stop-sdk-mode",
"last_assistant_message": LONG_MESSAGE,
},
{
"CATSTACK_HOOK_METRICS_DIR": tmp,
"CATSTACK_HOOK_MODE_DIU_STOP": "warn",
},
)
rows = [
json.loads(line)
for file in Path(tmp).glob("events-*.jsonl")
for line in file.read_text(encoding="utf-8").splitlines()
]

self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(1, len(rows))
self.assertEqual("diu-stop", rows[0]["hook"])
self.assertEqual("diu-stop.word-limit", rows[0]["rule_id"])
self.assertEqual("warn", rows[0]["mode"])
self.assertEqual("override", rows[0]["mode_source"])
self.assertEqual("warned", rows[0]["action"])


if __name__ == "__main__":
unittest.main()
Loading