How to Stop Claude Code: Interrupt a Turn, Cancel a Command, Quit, or End a Headless Run
📅 October 9, 2026 · ⏱️ 15 min read · ♻️ Updated
Claude Code is halfway through a change you did not want, or running a command that will never finish, and you want it to stop now. There are four different things you might mean by “stop”, and each has its own key or command.
Press Esc. Claude Code stops the current response or tool call mid-turn and keeps the work done so far, so you can redirect it. To leave the session, type /exit, or press Ctrl+D twice within 800 milliseconds, or press Ctrl+C twice when nothing is running.
| You want to | Press or type | What the docs say it does |
|---|---|---|
| Stop the current turn | Esc | ”Stop the current response or tool call mid-turn so you can redirect. Claude keeps the work done so far.” |
| Interrupt a running operation | Ctrl+C | ”Interrupts a running operation. If nothing is running, the first press clears the prompt input and a second press exits Claude Code” |
| Quit | /exit (alias /quit) or Ctrl+D | Ctrl+D: “The first press shows a confirmation hint and a second press within 800ms exits.” |
| Undo what it changed | Esc Esc on an empty prompt, or /rewind | Opens “the rewind menu to restore or summarize code and conversation from a previous point” |
Every key, flag, setting and exit code on this page is taken from Claude Code’s public documentation: the interactive mode reference, the commands list, the tools reference, the CLI reference, the headless guide, the hooks reference, the environment variables list, the /goal page and the checkpointing page, all read on 2026-10-07 and re-checked word for word on 2026-10-13. On the machine this was written on, claude --version printed 2.1.295 (Claude Code). The docs change often; if a key behaves differently for you, check your version against the page.
A few details in that table matter in practice:
Escis the one to reach for. It works mid-turn and keeps what is already done, so you can type a correction and carry on. On a permission prompt,Escdeclines the action. When a dialog is open,Esccloses the dialog.- Typing while Claude works does not interrupt it. The docs say Claude Code “queues the message instead of interrupting the turn”. If you want it to stop and read your message, press
Esc: Claude Code keeps what you queued and sends it right away. - Background subagents have their own stop.
Ctrl+XCtrl+Kwill “Stop all running background subagents in this session”. You “Press twice within 3 seconds to confirm.” /taskslists background work. It is where you “View and manage background work in the current session, including subagents that have finished”.
Stopping a long-running Bash command
When Claude is running a shell command that is taking too long, press Esc to stop the tool call. The alternative is Ctrl+B, which “Backgrounds Bash commands and agents” so you get the prompt back while the command keeps running (inside tmux, press it twice). Once a command is in the background, /tasks is where you see it and stop it.
The surprise is what happens when nobody presses anything. Each command runs under a timeout, but in the tools reference’s words: “When a foreground command reaches its timeout without finishing, Claude Code moves it to the background instead of stopping it, unless the command starts with sleep.” The result Claude gets back says so: Command did not complete within its 120s timeout and was moved to the background. So a dev server or a stuck test run does not die at the timeout: Claude Code moves it to the background, where it keeps running.
Two environment variables control the timeout, both in milliseconds:
BASH_DEFAULT_TIMEOUT_MSis the timeout Claude gets when it does not ask for one. The default is 120000, or 2 minutes.BASH_MAX_TIMEOUT_MSis the most Claude may ask for on one command. The default is 600000, or 10 minutes. The docs add: “The effective ceiling is the larger of this andBASH_DEFAULT_TIMEOUT_MS.”
You never set a per-command timeout yourself; Claude passes one with the call when it wants longer than the default. To give every command five minutes before it moves to the background, start the session with the variable set:
BASH_DEFAULT_TIMEOUT_MS=300000 claude
If you would rather have a command killed at its timeout than moved, turn background tasks off. Setting CLAUDE_CODE_DISABLE_BACKGROUND_TASKS to 1 disables “all background task functionality, including the run_in_background parameter on Bash and subagent tools, auto-backgrounding, and the Ctrl+B shortcut”, and then “a command that reaches its timeout stops instead”. Starting Claude Code with --bare has the same effect on background tasks.
Background commands do not run forever, but the limit depends on how you run Claude Code. In a local session you work in from a terminal, the desktop app or the VS Code extension, background commands “have no time limit”. In an unattended session, such as a -p run, a command that moved to the background “gets 30 minutes from the move”, and a command Claude started in the background gets 30 minutes or the timeout Claude passed, “up to a maximum of 2 hours”. That limit “requires Claude Code v2.1.285 or later”. Either way, “Background tasks are automatically cleaned up when Claude Code exits”, and on macOS and Linux processes that detached from the task’s shell, “such as ones started under setsid or timeout, stop too”.
Ending a headless claude -p run
A claude -p run in a script or CI job has no one to press Esc. Cap it in advance with —max-turns, or stop it from outside with a signal. SIGTERM ends the process with exit code 143 and leaves the turn unfinished; to end the turn cleanly, send SIGINT first.
Cap it in advance. The CLI reference documents --max-turns: “Limit the number of agentic turns (print mode only). Exits with an error when the limit is reached. No limit by default.” Note that --max-turns does not appear in claude --help on 2.1.295, which was checked by searching the captured help text. That is expected: the CLI reference says “claude --help does not list every flag, so a flag’s absence from --help does not mean it is unavailable.” The budget cap is in both places. This is what claude --help prints for it and for -p:
$ claude --help
--max-budget-usd <amount> Maximum dollar amount to spend on API
calls (only works with --print)
-p, --print Print response and exit (useful for
pipes). Note: The workspace trust dialog
is skipped when Claude is run in
non-interactive mode (via -p, or when
stdout is not a TTY, e.g. piped or
redirected output). Only use this in
directories you trust. Settings files
that fail validation are silently
ignored in this mode (no error dialog is
shown).
The CLI reference adds that --max-budget-usd is checked “against its client-side cost estimate, which can differ from your bill”.
Stop it with a signal. The headless guide has a section on exactly this: “If you stop a claude -p run with SIGTERM, for example with kill or from a process supervisor, Claude Code exits with code 143. Claude Code leaves the turn that was in progress unfinished and records no result for it. To end the turn instead, send SIGINT, or call the Agent SDK’s interrupt(), before you stop the process.” On SIGTERM it also “terminates the process tree of any Bash command that is still running”, then runs SessionEnd hooks and exits, starting no new tool call or model request on the way out. The docs do not give an exit code for SIGINT, and this page does not guess one.
If you resume a run that was stopped this way, the interrupted turn stays as it was and your next prompt takes over. To have Claude Code carry on with the interrupted turn instead, the docs give CLAUDE_CODE_RESUME_INTERRUPTED_TURN=1.
Read the exit code. “Claude Code exits with code 0 on success and a non-zero code when the run fails.” A bad flag fails before the run starts. On 2.1.295, claude -p --no-such-flag-xyz "hi" printed error: unknown option '--no-such-flag-xyz' to stderr and exited 1, without sending anything to a model.
A wall-clock cap is the usual pattern in CI: GNU timeout in front of the run and --max-turns inside it. One detail catches people. When timeout kills a command, timeout itself exits with 124, not the 143 that Claude Code returns on SIGTERM. Add --preserve-status if you want the child’s own status. Both numbers were measured with sleep under GNU coreutils 8.32, not with a real Claude run:
timeout 30m claude -p --max-turns 20 "Fix the failing test in tests/test_api.py"
case $? in
0) echo "finished" ;;
124) echo "stopped by timeout after 30 minutes" ;;
*) echo "claude exited non-zero" ;;
esac
Stopping it from carrying on by itself
Sometimes Claude Code keeps going after you expected it to stop: it finishes, then starts another turn without being asked. The usual cause is a Stop hook, or /goal, which the docs call “a built-in shortcut for a session-scoped prompt-based Stop hook”.
A Stop hook runs “when the main Claude Code agent has finished responding. Does not run if the stoppage occurred due to a user interrupt.” That second sentence matters: Esc always wins, because a hook never sees a stop you caused.
A Stop hook can do one of three things when it runs, and the third is the one people set up by accident:
- Let Claude stop. Exit 0 with no
decisionin its output. This is what notification and logging hooks should do. - End everything. The universal
continuefield: “Iffalse, Claude stops processing entirely after the hook runs.”stopReasonis the message shown to you. - Keep Claude working. For a Stop hook,
"decision": "block"“prevents Claude from stopping”, andreason“Tells Claude why it should continue”. Exiting with code 2 does the same thing: the exit-code table says it “Prevents Claude from stopping, continues the conversation”, with your stderr as the reason.
That last point is the accident. To a Stop hook, exit code 2 is not an error code; it is an instruction to keep going. A script that exits 2 because a sound file was missing, or because it copied exit 2 from a PreToolUse guard like the one in Stop Claude Code From Running rm -rf, Force-Pushing or Reading .env, tells Claude to keep working, and its error message becomes Claude’s instructions. A notification hook like the one in Claude Code Notification When Done should end with exit 0 on every path. The hooks reference notes that exit 1 is treated as “a non-blocking error” and Claude Code carries on with the action, which here means it stops normally, unless the hook sets "onFailure": "block", the setting the reference gives “To block the action instead” (the guard post shows it on a PreToolUse hook).
Claude Code has two brakes on this. The stop_hook_active field in the hook’s input “is true when Claude Code is already continuing as a result of a stop hook”, so a hook can check it and let go. There is also a hard cap: “after stop hooks have continued the turn eight times in a row, Claude Code overrides the next block and ends the turn.” The count resets each time Claude calls a tool, so a hook that blocks while Claude keeps running tools is not capped. CLAUDE_CODE_STOP_HOOK_BLOCK_CAP changes the cap, and setting it to 0 disables it. Do not set it to 0 unless you have another way out.
To find the hook responsible, run /hooks to view your hook configurations. They can live in ~/.claude/settings.json, in a project’s .claude/settings.json or .claude/settings.local.json, in managed policy settings, in a plugin or in a skill. To turn hooks off without deleting them, set "disableAllHooks": true. For one run only, pass --settings '{"disableAllHooks": true}', which “takes precedence over project and local settings”. Managed hooks are the exception: only disableAllHooks set in managed settings can turn them off. For /goal, run /goal clear; stop, off, reset, none and cancel are accepted aliases for clear.
If you do want a hook that keeps Claude going, give it two off-switches. This one asks for one more turn, and only while a file named .claude/keep-going exists in the project. It lets Claude stop whenever stop_hook_active is true, and also when the input cannot be parsed, so a broken hook means a normal stop, not a loop:
#!/usr/bin/env bash
# keep-going-once.sh: a Stop hook that asks Claude for ONE more turn, and only
# while the file .claude/keep-going exists in the project. Delete that file and
# Claude stops normally.
input=$(cat)
read -r active cwd < <(printf '%s' "$input" | python3 -c '
import json, sys
d = json.load(sys.stdin)
print(str(d.get("stop_hook_active", False)).lower(), d.get("cwd", ""))
' 2>/dev/null)
# Off-switch 1: this stop is already a continuation caused by a Stop hook. Let it end.
[ "$active" = "false" ] || exit 0
# Off-switch 2: no keep-going file, no extra turn.
[ -n "$cwd" ] && [ -f "$cwd/.claude/keep-going" ] || exit 0
printf '%s\n' '{"decision": "block", "reason": "The .claude/keep-going file exists. Finish the task it names, then delete the file."}'
Save it as ~/.claude/hooks/keep-going-once.sh, make it executable with chmod +x, and register it under Stop:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/keep-going-once.sh" }
]
}
]
}
}
The script was fed four Stop payloads by pipe, built from the documented input fields (cwd, stop_hook_active, hook_event_name and the common fields). Every run exited 0 and printed nothing to stderr:
| Payload | stdout | Claude Code would |
|---|---|---|
| no keep-going file, stop_hook_active false | nothing | stop |
| keep-going file present, stop_hook_active false | the block JSON | take one more turn |
| keep-going file present, stop_hook_active true | nothing | stop |
| not JSON at all | nothing | stop |
Undoing what it did
Stopping Claude does not undo anything. Esc keeps the work done so far. For undo, Claude Code has checkpoints, and you have git.
Checkpoints. “Every prompt you send that starts a turn creates a new checkpoint”, and Claude Code keeps file snapshots for “the 100 most recent checkpoints in a session”. Run /rewind, or press Esc twice with an empty prompt, pick the prompt to go back to, and choose:
- Restore code and conversation: revert both code and conversation to that point
- Restore conversation: rewind to that message while keeping current code
- Restore code: revert file changes while keeping the conversation
Checkpoints survive a resumed session, but snapshots are removed in a retention sweep “by default about 30 days after the session last saved one”; cleanupPeriodDays changes that.
What a rewind does not restore is the important part. Checkpoints undo edits made by Claude’s file editing tools, not files changed by Bash commands, so keep git as the real safety net. The checkpointing page is explicit: “Checkpointing does not track files modified by Bash commands”, with rm file.txt, mv old.txt new.txt and cp source.txt dest.txt as its examples. Edits made by most subagents are not restored either (“Use git to revert them”). Neither are edits made outside Claude Code or by another session, nor symlinked or hard-linked files.
Git. Commit or stash before a long session, and every change Claude makes is a diff against a known point. Afterwards:
git status --short # every changed and new file, one per line
git diff # the edits themselves
git restore path/to/file # discard one file's uncommitted changes
git clean --dry-run # list untracked files Claude created, deleting nothing
git restore throws away uncommitted changes to that file for good, so read the diff first. A status line that shows the current git branch makes it harder to start a session on the wrong one.
What this cannot stop
- Anything already sent off the machine. A
git push, a deploy, an API call or a message Claude sent has happened. No key or rewind brings it back. - Files a shell command changed. As above,
/rewindcovers file-tool edits only. Armrun through Bash is gone unless git or a backup has it. - Processes that belong to something else. Claude Code cleans up its own background tasks at exit. A container started with
docker run -dor a service started throughsystemctlbelongs to Docker or systemd, not to Claude Code’s process tree, and keeps running. - A hard kill. The SIGTERM cleanup described above (killing running Bash commands, then
SessionEndhooks) is what Claude Code does when it gets the chance. SIGKILL (kill -9) cannot be caught by any program, so none of that runs. - Managed hooks. If your organisation sets hooks in managed policy settings,
disableAllHooksin your own settings does not reach them. - Work queued in the same turn. A message you typed while Claude was still working can show up in the rewind menu marked No code restore. The checkpointing page explains: “Checkpoints are taken for prompts that start a turn, so this message has none of its own. The edits Claude made after reading it count toward the prompt that started the turn.” To undo that work, you rewind the whole turn.
What was tested, and what was not
Tested on Linux, with claude --version reporting 2.1.295 (Claude Code):
claude --helpwas captured in full (311 lines). Every help line quoted on this page is checked against that capture by a test in this site’s repository, and the capture is how--max-turnswas confirmed absent from the built-in help.claude -p --no-such-flag-xyz "hi"exited 1 witherror: unknown option '--no-such-flag-xyz', which made no model call.keep-going-once.shwas fed the four payloads in the table above, by pipe, with bash 5.1 and Python 3.10. The test re-runs them against the script as published on this page.- GNU
timeoutexit codes (124, and 143 with--preserve-status) were measured withsleep.
Not tested, and stated only on the docs’ authority: every key press (Esc, Ctrl+C, Ctrl+D, Ctrl+B, Ctrl+X Ctrl+K, double Esc) in a live session, --max-turns and --max-budget-usd on a real run, SIGTERM and SIGINT on a real claude -p process, the 143 exit code from Claude Code itself, the timeout-to-background move, the 8-continuation cap, and /rewind. No model call was made for this page. Each of those statements is quoted from the docs page linked beside it, so if your version behaves differently, the docs page is the place to check.
Follow new posts by RSS: paste dankdev.com/feed.xml into any feed reader. It is a plain XML file — no signup, no email address, no account. Posts land there the same day they go up, and there is nothing to unsubscribe from later.