Quickstart

Write your first tested hook in under five minutes.

A hook is an event, some conditions, an action, and a verdict Claude Code has to obey. In the next five minutes you’ll install the signed host, scaffold a project, write a hook that blocks a dangerous command, and watch its inline tests pass.

1. Install the signed host

captain-hook dispatches through one fixed, signed application on macOS 15 or newer:

brew install --formula yasyf/tap/captain-hook
uvx capt-hook helper install

The formula fills the Cellar; helper install deploys ~/Applications/Captain Hook.app from it and verifies the host answers. The helper embedded in that exact path owns the per-user host and its Python workers. There is no unsigned or project-local daemon and no cold fallback when the host is missing.

2. Scaffold

From your repo root:

uvx capt-hook init

init creates .claude/hooks/ with a starter hook, wires Claude Code’s settings to dispatch every hook event through the signed host, 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.

The app is the version authority: every event runs the exact capt-hook build the installed host names, and the host upgrades itself in the background when a new release ships — the wheel and app cannot drift apart. captain-hook never substitutes an older protocol, starts a private daemon, or reruns the event cold.

3. 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.

4. 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.

5. Test it

uvx capt-hook test
PASS  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