Hook in workflow
stop-gate
A Stop hook that runs the project's test command before Claude may end a turn with uncommitted changes, blocking the stop and returning the failure output when tests fail. Detects the command from package.json, Makefile, Cargo.toml, go.mod, or pytest config, or takes one from .claude/dotclaude.json. Never loops (respects stop_hook_active) and never runs on a clean tree. Use to make "done" mean the tests pass, mechanically, without relying on the model remembering to run them.
Install
As a plugin
Bundled in the workflow-hooks plugin. Add the marketplace once, then install the bundle; it updates with the marketplace.
/plugin marketplace add KhaledSaeed18/dotclaude/plugin install workflow-hooks@dotclaudeAs a single item
Run from the project root; the hook lands under .claude/. Copy it to ~/.claude/ to make it global.
npx shadcn@latest add KhaledSaeed18/dotclaude/stop-gateInstalled as a single item, a hook still needs the settings block documented below added to .claude/settings.json. The plugin route wires it automatically.
Files installed
.claude/hooks/stop-gate/HOOK.md.claude/hooks/stop-gate/stop-gate.mjs
What it does
stop-gate
A Claude Code hook that turns the verify-completion discipline into a mechanism. When Claude tries to finish a turn while the working tree has uncommitted changes, the hook runs the project's tests. If they fail, the stop is blocked and the failure output goes straight back to Claude, which continues working. If they pass (or there is nothing to run), the turn ends normally.
- Project-driven detection.
stopGate.commandin config wins; otherwise thetestscript inpackage.json(run with pnpm, yarn, bun, or npm according to the lockfile), a Makefiletesttarget,cargo test,go test ./..., orpytest -qwhen pytest is configured. npm'sno test specifiedplaceholder does not count. - Only when it matters. Runs only if
git statusshows uncommitted changes, so a question-and-answer session never triggers a test run. - Cannot loop. Claude Code sets
stop_hook_activewhen a stop was itself caused by a hook; the gate then stands down. - Bounded. 120-second cap by default; a timeout or a crash fails open. Only a genuine non-zero exit blocks.
- Zero dependencies. Node standard library only (
node >= 18).
Files
| File | Purpose |
|---|---|
stop-gate.mjs |
The hook script. Detects and runs the test command. |
HOOK.md |
This file. |
Activate it (required manual step)
Add this to .claude/settings.json (project) or ~/.claude/settings.json (global):
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/stop-gate/stop-gate.mjs\"",
"timeout": 180
}
]
}
]
}
}
The timeout (seconds) must exceed the hook's own cap, or Claude Code kills the hook first and the gate silently passes.
Installing the workflow-hooks plugin from the dotclaude marketplace wires this automatically.
Verify it
# In a project with a failing test and an uncommitted change:
echo '{"hook_event_name":"Stop","cwd":"'$PWD'","stop_hook_active":false}' \
| node .claude/hooks/stop-gate/stop-gate.mjs; echo "exit=$?"
Exit 2 with the failure on stderr means the gate is working.
Tune it
{
"stopGate": {
"enabled": true,
"command": "pnpm test --run",
"timeoutMs": 120000,
"onlyWhenDirty": true
}
}
Set command to a fast subset (pnpm vitest run --changed, make quick) if the full suite is slow; the gate is worth far more at ten seconds than at ten minutes. onlyWhenDirty: false runs it on every stop.
Hooks run arbitrary commands on your machine with your credentials whenever their event fires. Read any hook script (including this one) before enabling it.
This is the hook as installed, rendered from its source on GitHub. Read it as Markdown.
More in workflow
- branch-protectA UserPromptSubmit hook that, once per session, tells Claude the checkout is on a protected branch (main, master, develop, or a configured list) so it...
- git-guardA PreToolUse hook for Bash that blocks git operations which destroy work or bypass review, with a reason Claude can act on. Stops force-pushes and...
- session-contextA SessionStart hook that orients every new session before the first prompt by injecting the current branch, ahead/behind status, uncommitted files, the...
- subagent-summaryA SubagentStop hook that appends one JSON line per finished subagent to a project log (timestamp, session, agent id, and the first 300 characters of its...
- typecheck-on-editA PostToolUse hook that type-checks the project after Claude edits a TypeScript, Python, or Go file and returns the errors as context immediately, listing...