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.

uvx capt-hook init

Quickstart Tutorial

Poke the gate above

That Stop gate is live on this page — flip the session state and watch the verdict change. Every preset is a row in the parity suite, so the browser’s answer is capt-hook test’s answer.

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, 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(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.