captain-hook

Stop repeating yourself to Claude.

Claude Code hooks are shell commands wired through JSON. I wanted them to be what every other guardrail in a Python repo already is: typed functions with tests. capt-hook gives you block_command, gate, and friends — one init, zero settings surgery.

Check out the inspiration in this blog post.

uvx capt-hook init

Quickstart Tutorial

Poke the gate

The gate below is live. Change what happened in the session and watch the verdict flip.

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")],
)

Terminal recording: demo

Block force-push before it runs

One bad Bash call rewrites shared history, and by the time you spot it in the transcript it already ran. Declare the block once, tests inline:

# .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(),
    },
)

The next git push --force never executes: the agent sees the reason plus the hint, and reaches for --force-with-lease instead. And when a regex isn’t enough, walk the parse — the shipped general pack guards rm exactly like this:

for call in evt.command.calls("rm"):
    if call.targets.expand().exhausted:
        return evt.block("rm targets too broad to verify")
    return call.sub("rm", "trash", args=call.targets)

evt.command is the parsed command line — every rm across && and pipes, each target resolved against the working directory, the rewrite quote-safe. Learn the walk in the tutorial.

Turn repeated corrections into rules Claude can’t forget

You’ve typed “use uv, not pip” in a dozen sessions, and session thirteen makes the same mistake. The session reviewer reads each transcript as the session ends, keeps the corrections that are standing rules, and — once a pattern proves itself — opens a PR that codifies it as a hook:

uvx capt-hook status

The dashboard stages every correction from first sighting to open PR. You review the PR like any other; merged hooks enforce the rule from then on. See the corrections lifecycle.

Gate “done” until the tests actually pass

The agent declares victory while the suite is red. A Stop gate holds the line:

# .claude/hooks/quality.py
from captain_hook import RanCommand, Regex, TouchedFile, gate

gate(
    "You edited Python files but never ran the tests. Run `uv run pytest` before finishing.",
    only_if=[TouchedFile("**/*.py")],
    skip_if=[RanCommand(Regex(r"\bpytest\b"))],
)

The agent can’t end the turn until a pytest run shows up in the transcript, and the gate stands down on its own once one does. More Stop-gate patterns.


Tutorial

Block your first command in the browser — live widgets, verified against the real engine.

Guide

How-to pages for writing, testing, and shipping hooks.

Examples

Runnable hooks with inline tests, one per pattern.

Reference

Every class, condition, and CLI command, generated from source.

Note

Every command on this site is the full uvx capt-hook ... form — copy-paste from a clean machine and it runs.