Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/ecosystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ again.
| `hook-freshness` | hook (advisory) |
| `hook-health` | hook (advisory) |
| `llm-judge` | hook (shared background model judge; its inbox delivers finished verdicts on the next turn: Claude `UserPromptSubmit`, Cursor `stop`, Codex `notify`) |
| `unverified-tag-check` | hook (advisory; background read-only check of each unverified tag, reported through llm-judge's inbox) |
| `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) |

Expand Down
54 changes: 54 additions & 0 deletions engine/hooks/unverified-tag-check/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# unverified-tag-check

Check every well-formed `CAT-UNVERIFIED` tag in the background and report
whether the blocker held.

The hook fires on a finished assistant reply from Claude `Stop`, Cursor
`stop`, and Codex `notify` (`agent-turn-complete`). It looks for a tag shaped
like:

```text
{{CAT-UNVERIFIED: <claim> -- cannot verify: <blocker>}}
```

Malformed tags stay silent. Tags inside closed code fences or inline code stay
silent. A claim already checked in the same transcript within 2 hours stays
silent. Anything after the first 3 tags in one reply is ignored.

The live reply is never blocked or delayed. The hook hands one read-only
investigation job to [`llm-judge`](../llm-judge/README.md) for each new tag and
returns.

On the next step, the shared `llm-judge` inbox shows the job's `on_hit` text
and the checker's report sentence:

```text
unverified-tag-check: checked "<claim clipped to 120 chars>". Tell the user this result in plain words: <report>
```

The checker reports whether the stated blocker was real and whether the claim
is true, false, or unknown from readable evidence. It only gets Read, Grep, and
Glob, so it cannot run shell or network checks. If local files and transcripts
cannot answer the question, it says unknown.

Fail-open. An unreadable reply, missing transcript, broken judge, broken state,
or missing runner hands off nothing or comes back unchecked. It never becomes a
true result, and it never blocks the reply.

## Files

- `detect.py` - tag extraction, two-hour state, and `llm-judge` job enqueue
- `claude_stop_check.py` - Claude `Stop`
- `cursor_session.py` - Cursor `stop` / `sessionEnd`
- `codex_notify.py` - Codex `notify`
- `install_claude_hook.py` / `install_cursor_hook.py` / `install_codex_notify.py`

## Install

`./install.sh` from the repo root. Restart the harness.

## Tests

```sh
python3 -m unittest discover -s engine/hooks/unverified-tag-check/tests -v
```
16 changes: 16 additions & 0 deletions engine/hooks/unverified-tag-check/claude.hook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"hooks": {
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "python3 $HOME/.claude/hooks/unverified-tag-check/claude_stop_check.py",
"timeout": 10
}
]
}
]
}
}
21 changes: 21 additions & 0 deletions engine/hooks/unverified-tag-check/claude_stop_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
from __future__ import annotations

import json
import sys

from detect import try_check_reply


def main() -> None:
try:
payload = json.load(sys.stdin)
except Exception as exc:
print(f"catstack-hook-error unverified-tag-check: {type(exc).__name__}: {exc}", file=sys.stderr)
return
payload = payload if isinstance(payload, dict) else {}
try_check_reply(payload)


if __name__ == "__main__":
main()
36 changes: 36 additions & 0 deletions engine/hooks/unverified-tag-check/codex_notify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
from __future__ import annotations

import json
import subprocess
import sys

from detect import try_check_reply


def main() -> None:
if len(sys.argv) < 2:
return
raw = sys.argv[-1]
chain = sys.argv[1:-1]

if chain:
try:
subprocess.run(chain + [raw], timeout=5, check=False)
except Exception as exc:
print(f"catstack-hook-error unverified-tag-check: {type(exc).__name__}: {exc}", file=sys.stderr)

try:
payload = json.loads(raw)
except Exception as exc:
print(f"catstack-hook-error unverified-tag-check: {type(exc).__name__}: {exc}", file=sys.stderr)
return

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

try_check_reply(payload)


if __name__ == "__main__":
main()
23 changes: 23 additions & 0 deletions engine/hooks/unverified-tag-check/cursor_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env python3
from __future__ import annotations

import json
import sys

from detect import try_check_reply


def main() -> None:
try:
payload = json.load(sys.stdin)
except Exception as exc:
print(f"catstack-hook-error unverified-tag-check: {type(exc).__name__}: {exc}", file=sys.stderr)
print(json.dumps({"followup_message": ""}))
return
payload = payload if isinstance(payload, dict) else {}
try_check_reply(payload)
print(json.dumps({"followup_message": ""}))


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