Quickstart
A hook is an event, some conditions, an action, and a verdict Claude Code has to obey. In the next five minutes you’ll scaffold a project, write a hook that blocks a dangerous command, and watch its inline tests pass.
1. Scaffold
From your repo root:
uvx capt-hook initinit creates .claude/hooks/ with a starter hook, wires Claude Code’s settings to dispatch every hook event through capt-hook, and registers the plugin so the authoring skills load from it (nothing is copied into your repo). Prefer uv tool install capt-hook if you want the bare capt-hook on your PATH, or uv add capt-hook only if you’re consuming it as a library — every command here uses the uvx form.
2. Read a hook
init scaffolds .claude/hooks/example.py with a tour of the primitives — skim it later. First, the shape of a hook, using the gate from the front page:
from captain_hook import TouchedFile, UsedSkill, gate
# A Stop gate: before the agent finishes, block if it edited UI files without doing a visual review.
gate(
# the one-line reason shown to the agent when the gate fires
"You edited UI files. Open them with agent-browser and verify they render before finishing.",
# fires only if UI files changed
only_if=[TouchedFile("**/src/routes/**", "**/src/components/**")],
# already reviewed this session -> don't block
skip_if=[UsedSkill("agent-browser", scope="session")],
)Read it top to bottom: the event is the agent trying to finish, only_if scopes it to sessions that touched UI files, skip_if stands it down once a visual review happened, and the message is what the agent sees when the gate holds.
3. Write your own
Create .claude/hooks/safety.py:
# .claude/hooks/safety.py
from captain_hook import Allow, Block, Input, block_command
block_command(
["git", "push", "--force"],
reason="Force-pushing rewrites shared history",
hint="Use `git push --force-with-lease` instead",
tests={
Input(command="git push --force"): Block(),
Input(command="git push origin main"): Allow(),
},
)One declaration, and the block covers the whole command family — chained after &&, buried in a pipeline, even mentioned in quotes. The tests dict is the hook’s contract: each Input is a synthetic event, each value the verdict you expect.
4. Test it
uvx capt-hook testPASS safety.py::block_command git push --force → Block
PASS safety.py::block_command git push origin main → Allow
5 tests: 5 passed, 0 failed, 0 errors, 0 skipped
That’s the whole loop: declare, test, done. The next git push --force an agent attempts dies at PreToolUse with your reason and hint; LLM-backed hooks run their tests against recorded verdicts the same way.
Where next
- The tutorial — the same machinery, interactive in your browser.
- Inspect and rewrite commands —
evt.command, precision vs recall, rewrites. - Testing — strict verdicts, fixtures, and running
capt-hook test --jsonin CI. - Under the hood — how a hook actually runs, from stdin JSON to verdict.