Scott's sidekick: small, fast, typed judgments from TypeSafe's Jev wired into Claude Code, Codex, Grok and Hermes, where ordinary code needs a bit of common sense.
| Part | Hook (Claude Code, Codex, Grok · Hermes) / command | What it does |
|---|---|---|
publish_guard |
PreToolUse (Bash) · pre_tool_call (terminal) |
Before gh/grog publishes a comment: exact rules in code (local paths, attribution lines), Jev for local-only state, effort estimates, process narration, billing. Deny with reasons, or ask when borderline. Before a grog chat send: a file handed to a text command (grog send, discord-send, telegram-send, whatsapp-send) that is not a .md/.txt message is denied, exactly and without Jev, because grog would post it as text; the answer names the command that attaches it (*-send-image, *-send-document). |
reply_check |
Stop · pre_verify |
Blocks a final reply that gives an effort or time estimate, or asks the user to paste a secret into the chat, once; the second stop always passes. Each rule reaches Jev only when its text match fires (a duration, a secret's name), in one request. |
skill_router |
UserPromptSubmit · pre_llm_call |
Ranks every skill against the turn; answers at once when the ranking is sure, re-checks the top three only when it is not; adds one <skill_relevance> line the agent may ignore, once per skill per session. When the top skill was already suggested it stops after the ranking. Harness texts (stop-hook feedback, compaction summaries) are skipped, and under Grok it never runs. |
doc "<task>" |
CLI | Mechanical, low-risk work the router is confident about goes to oclaude; everything else to claude. --dry-run, -p. |
The TypeSafe key lives in hush as TYPESAFE_API_KEY; bin/doc-hook injects it with
hush run --redact. Without hush or python3 a hook checks nothing and exits 0.
Every Jev call (tokens, latency) and every decision (publish_guard: deny,
reply_check: passed, skill_router: suggested:devo, route: oclaude) is logged to
~/.cache/doc/usage.jsonl with the agent that ran it and Jev's scores behind it
("scores": {"local_state": 0.98, ...}), so thresholds can be tuned on real traffic —
never the text that was judged.
Works the same in Claude Code, Codex, Grok and Hermes. Codex hands reply_check the
reply itself (last_assistant_message) and keeps its skills in ~/.codex/skills;
skill_router reads the skills of whichever agent calls it. Grok runs the hooks it
finds in ~/.claude/settings.json with a camelCase payload (toolInput,
lastAssistantMessage, stopHookActive), which doc/event.py reads into the same
shape; it also fires an observe-only Stop at session end, which is skipped, and it
discards what a UserPromptSubmit hook adds, so skill_router doesn't run there.
Hermes runs the hooks listed in its own ~/.hermes/config.yaml, under its event names
(pre_tool_call, pre_llm_call, pre_verify), with the prompt and the reply under
extra, and reads back {"decision": "block"}, {"action": "approve"} and
{"context": ...} instead of hookSpecificOutput: doc/event.py reads and answers in
its shape. Its pre_verify fires only on turns that edited files, so there
reply_check checks those replies only.
DOC_RULES names a JSON file that turns built-in Jev rules off and adds rules of the
owner's own, for reply_check ("reply"), publish_guard ("comment") or both. Cora
writes it from its panel for its own agents; without the variable nothing changes. The
exact rules in code (local paths, attribution lines) are not Jev rules and stay on.
{"off": ["process_log"],
"add": [{"key": "client_names", "on": ["reply", "comment"],
"question": "Does it name a client company or a client's contact person?",
"reason": "names a client", "fix": "Leave the client's name out.",
"keywords": ["acme"], "threshold": 0.8}]}A built-in rule is turned off by its key, or for one kind of text by reply:<key> or
comment:<key>; an added rule with "enabled": false is kept but not asked. An added
rule reads every text of its kind, or only those holding one of its keywords,
and stops one when Jev's score reaches its threshold (0.8 unless set). A file or a rule
that does not make sense is left out. python3 -m doc.rules lists the built-in Jev rules,
and python3 -m doc.rules try scores one draft rule on a text read as
{"target", "text", "rule"} from stdin, so a rule can be tried before it is saved.
Register the hooks in ~/.claude/settings.json and/or ~/.codex/hooks.json:
"PreToolUse": [{"matcher": "Bash", "hooks": [{"type": "command", "command": "~/doc/bin/doc-hook publish_guard", "timeout": 20}]}],
"Stop": [{"hooks": [{"type": "command", "command": "~/doc/bin/doc-hook reply_check", "timeout": 20}]}],
"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "~/doc/bin/doc-hook skill_router", "timeout": 20}]}]and link the CLI: ln -s ~/doc/bin/doc /usr/local/bin/doc.
For Hermes, in ~/.hermes/config.yaml (its shell tool is terminal, and commands run
without a shell):
hooks:
pre_tool_call:
- {matcher: terminal, command: ~/doc/bin/doc-hook publish_guard, timeout: 20}
pre_llm_call:
- {command: ~/doc/bin/doc-hook skill_router, timeout: 20}
pre_verify:
- {command: ~/doc/bin/doc-hook reply_check, timeout: 20}Codex runs a hook only once you trust it: on its next start it lists new or
changed hooks for review, or use /hooks. Hermes asks once per hook at its first
interactive start (or hermes --accept-hooks). Grok needs nothing: it reads the Claude
Code settings unless [compat.claude] hooks = false is set in ~/.grok/config.toml.
DOC_USAGE_LOG, DOC_SKILL_SEEN_DIR, DOC_JEV_MODEL and DOC_RULES override the defaults;
bin/doc-hook and bin/doc pass them through hush run, whose child otherwise
inherits only the key. The child may run on the host of a container, so these paths
must exist there.
PYTHONPATH=. python3 -m unittest tests.test_offline
hush run --name TYPESAFE_API_KEY --env TYPESAFE_API_KEY --redact -- env PYTHONPATH=. python3 -m unittest tests.test_liveJev reads literally, doesn't count or compare dates, and content written to steer
it can move an answer (see its jaggedness page). So these hooks only add checks:
none of them allows anything a rule forbids. publish_guard sees the command text,
so a comment posted by a script it can't read is not checked.