Invite & Earn

How invite rewards work

Share your invite link. When a friend registers through it and tops up, you receive the displayed reward on their subsequent top-ups.

Stop Hooks in Claude Code: Verify Before “Done”

Add a small Stop Hook check to Claude Code, verify tests or files with an exit code, and keep secrets out of local logs.

Contents

Stop Hooks in Claude Code: Verify Before You Say “Done”

Claude Code can stop after producing a plausible answer. A Stop Hook gives the project one small, repeatable check before that stop: run a test, inspect a file, or validate the working tree. It does not replace CI or prove that every requirement is correct.

If Claude Code uses an API provider, open the current BetterToken setup guide, configure your own API key in the tool, and send a short test request. Confirm the expected model, status, and token usage in Dashboard. Keep the key out of the local hook and its logs.

Stop Hook, CLAUDE.md, and CI have different jobs

A Stop Hook runs the short command at the end of a turn. CLAUDE.md tells the agent which command and boundary to follow but does not execute anything. CI runs independently after push or pull request and remains the final gate.

Pick one claim and one signal

Write down the claim you want to verify, the command that proves it, and the exit code that means success. For a Node project, the contract might be: “the focused tests pass and dist/app.js exists.” Start with a read-only or local check. Do not put deploys, publishing, or external writes in a hook that runs at every stop.

Configure a minimal hook

Create .claude/settings.json with a Stop command hook and a short timeout:

{
  "hooks": {
    "Stop": [{
      "hooks": [{
        "type": "command",
        "command": "./scripts/check-before-stop.sh",
        "timeout": 30
      }]
    }]
  }
}

Keep the script explicit:

#!/usr/bin/env sh
set -eu
if [ -t 0 ]; then
  input='{"stop_hook_active":false}'
else
  input=$(cat)
fi
stop_hook_active=$(printf '%s' "$input" | node -e 'let r="";process.stdin.on("data",c=>r+=c);process.stdin.on("end",()=>{try{process.stdout.write(String(Boolean(JSON.parse(r).stop_hook_active)))}catch{process.stdout.write("false")}})')
block_stop() {
  if [ "$stop_hook_active" = "true" ]; then
    printf '%s\n' "Stop check still fails: $block_reason. Run it manually; CI remains required." >&2
    exit 0
  fi
  BLOCK_REASON="$block_reason" node -e 'process.stdout.write(JSON.stringify({decision:"block",reason:`Stop check failed: ${process.env.BLOCK_REASON}`}) + "\n")'
  exit 0
}
npm test -- --runInBand >/dev/null 2>&1 || { block_reason='npm test'; block_stop; }
test -s dist/app.js || { block_reason='dist/app.js is missing'; block_stop; }
exit 0

Run the script manually first, make it executable, and replace the commands with the ones that exist in your repository. Then verify the workflow:

  1. Break the expected file name and run printf '%s\n' '{"stop_hook_active":false}' | ./scripts/check-before-stop.sh; echo $?; expect JSON with "decision":"block", a concise reason, and code 0.
  2. Restore the path and repeat the command; expect code 0.
  3. Ask Claude Code to complete a small task and confirm that the decision: "block" response prevents Stop and sends the concise reason back to Claude.
  4. With the check still broken, pipe {"stop_hook_active":true} into the script; expect a short stderr warning and code 0. Then restore the check and confirm normal exit 0. This branch deliberately fails open after one remediation attempt while CI remains required. If you need repeated blocking, implement an explicit project-owned counter and boundary instead of assuming a fixed undocumented retry count.

Log only the check name and exit result. Never print API keys, .env contents, full prompts, or complete test logs.

Handle a failed stop

Separate two cases. If the hook returns JSON with decision: "block", read reason: the checked condition failed normally. Run that check directly, fix the code or test, confirm that any expected file came from the current command, and retry the Claude Code task.

If the hook command itself exits nonzero or times out, treat it as a hook execution error, not a confirmed reason block. Read the hook error and stderr, run the script manually, and fix its path, permissions, dependency, or time budget before retesting. A passing git diff --check does not prove business logic, and an existing build file may be stale.

Sources

Ready to optimize your LLM workflow?

Join thousands of developers building faster, smarter, and more cost-effective AI applications with BetterToken.

Get Started for Free