A companion binary for Claude Code. Provides a status line and hook handlers.
go install github.com/laurenkt/claudetool@latest
Reads Claude Code JSON from stdin and outputs a formatted terminal status line (directory, branch, model, cost, context usage).
{"hooks": {"Status": [{"hooks": [{"type": "command", "command": "claudetool statusline"}]}]}}Hook handlers that read Claude Code hook JSON from stdin and return structured decisions on stdout.
claudetool hook <handler> [<handler>...] [flags]
Multiple handlers run in sequence against the same stdin. The chain stops at
the first error or Decision: "block"; otherwise non-blocking outputs are
merged (e.g. additionalContext strings are concatenated). Positional args
before the first -flag are treated as handler names; everything from the
first flag onward is the flag tail, passed to every handler so flag-driven
ones like dump -o <path> keep working. Handler names cannot begin with -.
| Handler | Event | Matcher | What it does |
|---|---|---|---|
no-cd |
PreToolUse | Bash |
Blocks cd commands |
use-linear-mcp |
PreToolUse | Bash|WebFetch |
Blocks gh/curl/wget/linear-cli calls and WebFetch to linear.app, points to MCP |
no-shared-pr-body |
PreToolUse | Bash |
Blocks commands touching the fixed /tmp/pr-body.md path — concurrent agents clobber each other's PR body; points to mktemp / --body-file - |
semgrep-check |
PostToolUse | Write|Edit |
Runs semgrep, blocks on findings |
go-augment-style |
PostToolUse | Write|Edit |
Blocks verbose terrors.Augment context strings (e.g. "failed to read" → "read") |
redirect-writes |
PreToolUse | Write|Edit |
Rewrites file paths (set REDIRECT_FROM and REDIRECT_TO env vars) |
backend101 |
PreToolUse | Write|Edit |
Enforces Backend 101 naming: firehose consumers in consumer/consumer.go, streams consumers in streamsconsumer/consumer.go, handler functions in matching snake_case files |
go-fix |
PostToolUse | Write|Edit |
Runs go fix on the package containing the edited .go file; applies modernizations and reports the diff back to Claude |
go-makeslice |
PostToolUse | Write|Edit |
Advisory (non-blocking): suggests var x []T instead of make([]T, 0, n) |
go-named-func |
PostToolUse | Write|Edit |
Advisory (non-blocking): flags named anonymous functions (name := func(...) {...}); skips _test.go |
go-cmp-or |
PostToolUse | Write|Edit |
Advisory (non-blocking): flags hand-rolled first-non-empty-string selectors (a value returned under an x != "" guard with a return "" fallback); points at cmp.Or |
valuable-comments |
PostToolUse | Write|Edit |
Async (see below): dispatches the changed .go code to a headless claude reviewer in the background; wakes the agent only when a comment restates the code, the name, or something that belongs in a validator |
change-detector-tests |
PostToolUse | Write|Edit |
Async (see below): reviews changed _test.go files and pushes back on change-detector tests (coupled to the implementation, not behaviour); leaves legitimate ones alone (codegen golden files, parsing/round-trip, characterization, security tripwires) |
rpc-wrapper |
PostToolUse | Write|Edit |
Async (see below): flags functions that are pointless thin wrappers around a single generated-client RPC call (Request{…}.Send(ctx).DecodeResponse() + trivial error-wrap + return a field); leaves wrappers that transform, orchestrate, or back an interface alone; skips _test.go |
ci-watch |
PostToolUse | Bash |
Async (see below): after a git push / gh pr create / gh pr ready, watches the PR's CI checks in the background for up to 30 min and wakes the agent only if a check fails |
{
"hooks": {
"PreToolUse": [
{"matcher": "Bash", "hooks": [{"type": "command", "command": "claudetool hook no-cd no-diff-master"}]},
{"matcher": "Bash|WebFetch", "hooks": [{"type": "command", "command": "claudetool hook use-linear-mcp"}]}
],
"PostToolUse": [
{"matcher": "Write|Edit", "hooks": [{"type": "command", "command": "claudetool hook go-augment-style backend101 go-fix"}]}
]
}
}Most handlers decide inline and the agent waits for them. An async review hook
instead dispatches the change to a separate, headless claude instance for
evaluation in the background, and only interrupts the working agent if that
reviewer has something to say.
This relies on Claude Code's per-hook asyncRewake:
the hook runs without blocking the agent, and exiting with code 2 wakes the
agent with the hook's stderr as a system reminder. So the handler returns:
- no findings → exit 0, silent — the agent is never interrupted;
- findings → exit 2 with feedback → the agent is woken (a few seconds later) and can go back and revise.
Two such checks ship today:
valuable-commentsjudges whether comments in the changed code add value (explain why, cite scheme/schema docs or examples, clarify what names and validators can't) versus restate the implementation, the name, or a rule that belongs in a proto/go validator.change-detector-testsreviews changed_test.gofiles and pushes back on change-detector tests — ones coupled to the implementation rather than observable behaviour, which break on any refactor and catch no real bug. It deliberately leaves the legitimate cases alone (codegen golden files, message/wire-format round-trips, characterization tests, security/config tripwires).rpc-wrapperflags functions that are pointless thin wrappers around a single generated-client RPC call — build one request,Send().DecodeResponse(), trivial error-wrap, return a field — where inlining the call at the caller would lose nothing. It leaves wrappers that transform inputs/outputs, orchestrate multiple calls, add retry/caching, or implement an interface method alone.
Because asyncRewake is configured per hook, an async handler must be its
own hook entry, not appended to a synchronous chain:
{
"hooks": {
"PostToolUse": [
{"matcher": "Write|Edit", "hooks": [
{"type": "command", "command": "claudetool hook go-augment-style backend101 go-fix"},
{"type": "command", "command": "claudetool hook valuable-comments", "asyncRewake": true},
{"type": "command", "command": "claudetool hook change-detector-tests", "asyncRewake": true},
{"type": "command", "command": "claudetool hook rpc-wrapper", "asyncRewake": true}
]}
]
}
}The reviewer model is a configurable tier — FastCheap (haiku),
MediumBalanced (sonnet, the default), SlowAccurate (opus) — overridable per
invocation with a flag: claudetool hook valuable-comments -tier fast.
New async checks are a few lines: configure an asyncReview (file suffix,
default tier, a cheap precheck gate, and a rubric) and register its
handler(). See internal/hook/handler_asyncreview.go and
internal/hook/handler_valuable_comments.go.
ci-watch reuses the same asyncRewake delivery as the review hooks, but
instead of dispatching a reviewer it watches the PR's CI. When the agent runs a
git push, gh pr create, or gh pr ready, the hook looks up the PR for the
current branch (gh pr view) and blocks on gh pr checks --watch --fail-fast
in the background. CI at Monzo takes 8–30+ minutes, so without this the agent
moves on and the human finds the red build later; here the hook wakes the agent
(exit 2) on the first failing check while the context is still fresh.
- Failure-only: silent on success — only a failed check wakes the agent.
- No PR yet: a bare push before
gh pr createfinds no PR and does nothing. - Timeout: gives up silently after 30 min — it never reports a false failure.
- Preemption: a new push for the same branch kills the previous watcher (via
a pidfile under
~/.claude/ci-watch/), so only the latest push is watched.
Because asyncRewake is per hook, ci-watch is its own Bash entry with a
timeout matching its 30-min watch window (not chained onto a synchronous hook):
{
"hooks": {
"PostToolUse": [
{"matcher": "Bash", "hooks": [
{"type": "command", "command": "claudetool hook ci-watch", "asyncRewake": true, "timeout": 1800}
]}
]
}
}Requires gh on PATH and authenticated. A PR opened via the GitHub web UI
(rather than gh) after a bare push is not detected, and branch CI without a PR
is not watched — both by design.
Create internal/hook/handler_yourname.go:
func init() { Register("your-name", handleYourName) }
func handleYourName(in *Input) (*Output, error) {
// return nil, nil to allow
// return nil, fmt.Errorf("reason") to block (exit 2)
// return &Output{...}, nil for structured responses
}