Stop gates
Everything so far fired on a single tool call. Stop gates fire on a different event entirely: the agent deciding it’s done. This is the hook from the front page — it holds the session open until edited UI files got a visual review.
The widget below simulates a session instead of a command line. Toggle what happened this session, then try to stop:
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")],
)
Walk the combinations. Edit /repo/src/components/Button.tsx without a review and the stop bounces: the agent gets the gate’s message and keeps working, so you never see a “done” that isn’t. Flip on agent-browser and skip_if stands down — the gate isn’t a nag, it’s a condition, and the condition is satisfiable. Edit only /repo/README.md and the gate has no opinion at all.
TouchedFile and UsedSkill read the session’s history, not the current tool call — that’s what makes gating “done” possible at all. The same machinery drives test gates (RanCommand(Regex(r"\bpytest\b"))) and artifact checks.