// guide · claude code stop hook

Stop Claude Code from stopping halfway

One of my sessions ended with "Still open: step two is the textures." Eleven times in a row, I typed "keep going". The rule against that was already in the instructions. The Stop hook is what actually fixed it, and this guide gives you the config, a generic script, and the test that tells you whether it will work on your sessions.

The written version of my Stop hook video. The session numbers below are my own counts from one week, and the hook behavior comes from Claude Code's own hooks documentation, listed at the bottom with the date I checked.

The scoreboard that started it

I count my agent sessions every week, from the transcripts. One week stood out: 204 sessions, and I typed "keep going" 62 times, in 28 sessions. The week before it was 8 sessions. In that same week my agents ended their turn with a question 237 times. "Shall I build the test page?" "Do you want me to fix that too?" "Let me know if you want this."

Then I checked my own answers: 98 times I said yes, do it, go. 12 times I said no. Eight to one. Most of those questions never needed to be asked.

Here is the strange part. My instructions already said: don't announce the next step, do it. Every session loaded that rule. The model read it, weighed it against everything else in the conversation, and at the end of a long turn, handing control back to the human still looked like the safe choice. The rule lost.

A rule is advice. A hook is a door.

That is the whole idea. A rule in your instructions file, or in a skill, is advice: the model reads it and sometimes it loses the weight-guessing contest. A hook is code. Claude Code runs it at a fixed moment whether the model likes it or not.

The Stop event fires every time Claude finishes responding, right before the turn actually ends. Your script gets the facts as JSON, reads the last message, and makes one decision: let it stop, or send it back to work with a reason. That is the door. Claude can still walk through it, but only after the code has looked at what it wrote.

Two safety rails come straight from the docs. Your script receives stop_hook_active, which is true when this stop is already the result of a stop hook; check it, or your hook loops. And Claude Code caps consecutive stop-hook continuations at eight, so even a buggy hook can't trap a session forever.

The config

Hooks live in your settings files. Two useful spots: ~/.claude/settings.json applies to all your projects, and .claude/settings.json in a repo applies to that project and can be committed so your whole team gets it. The Stop event has no matcher; it always fires.

~/.claude/settings.json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/keep-going.sh",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

The timeout is in seconds; if your script hangs, Claude Code cancels it rather than waiting forever. You can inspect and debug everything in Claude Code with the /hooks menu, which shows what loaded from which file.

The script

The script reads one JSON blob on stdin. The two fields that matter: last_assistant_message, the text Claude just wrote, and stop_hook_active, your loop guard. Exit 0 lets the turn end. Exit 2 blocks the stop, and whatever you print to stderr becomes the reason Claude gets for continuing.

This is a generic starting point, not my production script. It allows any stop that touches money, deletion or mail, and it pushes back on the lazy patterns: a question, or a "still open" note that hands the next step back to you.

~/.claude/hooks/keep-going.sh
#!/usr/bin/env bash
# Stop hook: block lazy stops, allow real handoffs.
# Exit 0 = Claude may stop. Exit 2 = Claude continues; stderr is the reason.
input=$(cat)

# Loop guard: if this stop is already the result of a stop hook, let it through.
if [ "$(printf '%s' "$input" | jq -r '.stop_hook_active // false')" = "true" ]; then
  exit 0
fi

last=$(printf '%s' "$input" | jq -r '.last_assistant_message // ""')

# Anything that spends money, deletes something or mails a person must stop.
if printf '%s' "$last" | grep -qiE 'purchase|payment|refund|delete|rm -rf|send (a )?mail|email (the|him|her|them)'; then
  exit 0
fi

# The lazy patterns: a question or an open-items note that hands work back.
if printf '%s' "$last" | grep -qiE 'shall i |want me to |should i |still open:|remaining:|next step is '; then
  echo "You ended with a question or a still-open note. Decide the next step yourself and finish it. Only stop to ask when the answer costs money, deletes something or mails a person." >&2
  exit 2
fi

exit 0

Make it executable and it runs: chmod +x ~/.claude/hooks/keep-going.sh. The script needs jq on your PATH, same as the examples in Claude Code's own docs.

Two honest limits of this pattern. It works on the message text, so a genuinely finished answer phrased as a question gets pushed once, and a lazy stop phrased as a statement slides through. And the pattern list is yours to tune: mine grew from the exact sentences I kept correcting.

Replay it on last week before you switch it on

This is the step I'd keep if I had to throw away the rest. Before the hook went anywhere near a live session, I ran it against the transcripts of the week before and asked one question: of the turns where I had to answer myself, which ones would the hook have caught?

Of the 122 questions I had answered yes, the hook would have sent 76 straight back to work. Of the 34 times I had said no, it would have pushed 7. I read all seven. Every one was me saying "no, do more" or "no, do it differently". None of them deleted anything, paid anything or mailed anyone. The replay took ten minutes. It's the difference between guessing and knowing, and it also gives you the pattern list: every sentence the hook missed, your script should learn.

237
Turns that ended with a question to me, in one week of my own sessions.
98 : 12
My answers: yes, do it, versus no. Eight to one.
76 of 122
What the replay said the hook would have caught, from the yes-pile alone.

One person, one week, and the counting is done on keywords in my own transcripts, not by reading every message. Treat the numbers as a shape, not a benchmark.

What changed the week after

Sessions where I had to type "keep going" fell from 28 to 11 the week after the hook went live, with fewer interactive sessions overall. I'll say exactly what I said in the video: that is a correlation. Fewer sessions happened too, so the hook doesn't get all the credit. What I can say is that the correction I typed most often stopped dominating my week, and the replay told me beforehand that it would.

Your version will have its own scoreboard. Count one week before, replay the hook on it, switch it on, count one week after. If "keep going" doesn't drop, the patterns are wrong, not the idea.

// the video

Watch the Stop hook episode

The video that goes with this guide, including the two silent failures from the same week: an agent that loaded a whole week of transcripts into memory at once, and a model list that came back short under load.

Sources, checked 27 October 2026

  • Anthropic, Claude Code docs, "Hooks reference", code.claude.com/docs/en/hooks: the Stop event fires when Claude finishes responding; hooks are defined in ~/.claude/settings.json or .claude/settings.json; Stop has no matcher; JSON input on stdin with stop_hook_active and last_assistant_message; exit 2 blocks the stop with stderr as the reason; timeout field on command hooks; the 8-consecutive-continuation cap; the /hooks menu.
  • My own numbers: counts from my own agent transcripts, one week, keyword-based, one person. The 237, 98 : 12, and 76-of-122 replay figures are from that week; the 28-to-11 drop is the week after the hook went live, correlation not proof.

Want the hook, tuned and installed?

The Stop hook, the limit guard and the agents, set up on your own server, with the patterns I run every day.