Stop repeating yourself to Claude. captain-hook mines your transcripts for the corrections you keep giving and opens PRs that turn each one into a typed, tested Python hook.
brew install --formula yasyf/tap/captain-hook
uvx capt-hook initThe formula deploys the fixed, signed host at ~/Applications/Captain Hook.app; init scaffolds
.claude/hooks/, wires Claude Code's settings, and arms the session reviewer. Every event runs
the exact capt-hook build the installed app names — nothing resolves "latest" mid-session, and
the app keeps itself current in the background. One block_command later, a force-push dies
at PreToolUse and the hook's inline tests run green:
Driving with an agent? Paste this:
/plugin marketplace add yasyf/captain-hook
/plugin install captain-hook@captain-hook
Prefer a prompt over the plugin?
Install the `yasyf/tap/captain-hook` Homebrew formula, then run `uvx capt-hook init`
in this repo, write one hook that blocks force-pushes,
and verify it with `uvx capt-hook test`. Read https://yasyf.github.io/captain-hook/
if you get stuck.
One bad Bash call rewrites shared history or eats a directory, 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 BLOCKED: Force-pushing rewrites shared history plus the hint, and reaches for --force-with-lease instead. And when a pattern can't decide, walk the parse — this is the heart of the shipped rm guard:
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.
You've typed "use uv, not pip" in a dozen sessions, and session thirteen makes the same mistake. After init, the session reviewer reads each transcript as the session ends, keeps the corrections that are standing rules, and — once a pattern proves itself across sessions — opens a PR that codifies it as a hook. Watch the pipeline:
uvx capt-hook statusThe dashboard lists every correction it's tracking, staged from first sighting to open PR. You review the PR like any other; merged hooks enforce the rule from then on.
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.
- Interactive tutorial — block your first command in the browser, verified against the real engine — start it
- Session reviewer — the full corrections lifecycle, from transcript to merged hook PR — guide
- Conditions — typed filters over tools, files, commands, and transcript history — guide
- LLM hooks — gate on a model's verdict when a regex can't decide — guide
- Workflows — multi-step Stop gates with artifact checks and checklists — guide
- Packs — the shipped
general,python, andgohook packs — guide - Testing — run
uvx capt-hook test --jsonin CI so a regressed hook fails the build — guide
Read the docs for the full guide. Licensed under PolyForm Noncommercial 1.0.0, free for noncommercial use.

