Interrupting a tool mid-execution is stamped non_execution_kind: "user-rejected" — the model is told the tool did not run, though the engine enum has a dedicated "interrupted" value

Status Open
Reported on v2.1.227
Maintainer reply None cached
Activity 2 comments · opened Aug 12, 2026

Summary

When a tool call that is already executing is interrupted, the harness hands the model a tool_result whose text asserts the tool never ran:

The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.

accompanied by tool_use_result: "User rejected tool use" and tool_result_meta: [{"non_execution_kind": "user-rejected"}].

This is the permission-denial message being reused for the mid-execution interrupt path. The two are not equivalent: on a permission denial the tool genuinely never executes; on a mid-execution interrupt it ran and was killed.

The engine is not missing the information. At the same instant it emits, for the same tool_use_id:

  • system/task_notification"status": "stopped" (preceded by task_started for that same id)
  • final result event → "terminal_reason": "aborted_tools"

And its own non_execution_kind enum already contains a value for exactly this case:

["user-rejected","permission-rule","automode-blocked","automode-unavailable",
 "automode-parsing-error","interrupted","cancelled"]

with the schema documenting it as:

non_execution_kind is the harness-stamped reason an is_error:true result did not carry the tool's own execution output (user-rejected / permission-rule / automode-* / interrupted / cancelled); absent means the tool ran to completion.

So the mid-execution-interrupt path stamps user-rejected when a documented interrupted value exists.

Reproduction (no GUI required)

A tick-probe makes execution duration observable: the log line count proves how long it actually ran, which distinguishes "never executed" from "executed then killed".

probe.sh:

#!/bin/bash
L="$1"; rm -f "$L"
for i in $(seq 1 40); do
  printf '%s tick=%02d\n' "$(date '+%H:%M:%S')" "$i" >> "$L"
  sleep 1
done
echo "COMPLETED_ALL_40_TICKS"

Drive the engine over the documented streaming-input channel:

claude -p --verbose --input-format stream-json --output-format stream-json --allowed-tools Bash
  1. On stdin, send a user message asking it to run bash probe.sh <logpath> with the Bash tool.
  2. Poll the log until it has accumulated several ticks — this confirms the tool is genuinely executing, not sitting at a permission prompt.
  3. Then send on stdin:

``json
{"type":"control_request","request_id":"req_1","request":{"subtype":"interrupt"}}
``

  1. Keep polling the log for 10+ seconds afterwards.

Observed

Three independent runs (interrupt injected at tick 6, tick 6, and tick 15 — the last specifically to rule out a start-up race):

| run | ticks at interrupt | ticks at +2s / +5s / +10s / +15s | real execution before kill |
|---|---|---|---|
| 1 | 6 | 6 / 6 / 6 / 6 | ~6s |
| 2 | 6 | 6 / 6 / 6 / 6 | ~6s |
| 3 | 15 | 15 / 15 / 15 / 15 | ~15s |

COMPLETED_ALL_40_TICKS never printed; the probe process was gone (pgrep empty). So the tool executed and was killed mid-flight.

Event sequence from the stream (run 1, abridged):

system/task_started        {task_id: ..., tool_use_id: toolu_01Ww..., task_type: local_bash}
control_response           {subtype: success, request_id: req_int_1, response: {still_queued: []}}
system/task_notification   {tool_use_id: toolu_01Ww..., status: "stopped"}
user tool_result           "The user doesn't want to proceed with this tool use. The tool use
                            was rejected (eg. if it was a file edit, the new_string was NOT
                            written to the file). STOP what you are doing..."   is_error: true
                           tool_use_result:  "User rejected tool use"
                           tool_result_meta: [{non_execution_kind: "user-rejected"}]
user text                  "[Request interrupted by user for tool use]"
result/error_during_execution  {terminal_reason: "aborted_tools", stop_reason: "tool_use"}

The tool_result text was byte-identical across all runs, in both the --output-format stream-json output and the persisted session JSONL.

Why this matters

The model is told, in the same breath, two contradictory things:

  • tool_result: the tool was rejected, and if it was an edit nothing was written
  • an adjacent text block: [Request interrupted by user for tool use]

The interrupt marker is the weaker of the two and is directly contradicted by the tool_result next to it. The failure mode is a model that, after interrupting a long Edit, a git operation, or a migration script, concludes the target is untouched and proceeds on that assumption — skipping the state check it would have made had it known the tool ran.

Note the narrow version of this claim, which is the defensible one: the harness cannot know what side effects a killed command left behind, and it does not need to. It does know the tool started executing — it emitted task_started and then stopped for that tool_use_id. Asserting non-execution is wrong on that basis alone.

Also worth flagging: the field is literally named non_execution_kind, and its schema says "absent means the tool ran to completion". There is no stamp for "started, did not complete" other than the interrupted / cancelled values that this path declines to use.

Affected entry points

The misleading wording is confirmed on three entry points, all with the same probe method and byte-identical tool_result text:

| entry point | engine | real execution before kill | tool_result wording | toolUseResult |
|---|---|---|---|---|
| Agent SDK / stream-json, control_request {subtype: interrupt} | 2.1.227 | 6s, 6s, 15s (3 runs) | identical | User rejected tool use |
| Claude Desktop Code window, Interrupt Claude button | 2.1.227 | 19s | identical | — |
| CLI / TUI, ESC | 2.1.195 | 55s | identical | User rejected tool use |

The non_execution_kind: "user-rejected" mis-stamp is confirmed on 2.1.227 (SDK/stream-json and Desktop). On 2.1.195 the tool_result_meta / non_execution_kind field does not exist at all (zero occurrences in that binary), so the TUI run above cannot speak to it either way.

Worth noting what that implies: the wording problem is long-standing — #47282 reported it in April 2026 — while non_execution_kind is a newer, machine-readable field that arrived with a dedicated interrupted value already in its enum, and this path stamps user-rejected anyway. The structured field reproduced the old mistake rather than fixing it.

VS Code extension — not tested.

The permission-prompt-stage interrupt is a correct use of this wording and is a useful control: in that case the probe log is never created (zero execution) and rejected is accurate.

Environment

  • Engine 2.1.227 (the build Claude Desktop ships/downloads on macOS: ~/Library/Application Support/Claude/claude-code/2.1.227/claude.app/Contents/MacOS/claude) — SDK/stream-json and Desktop runs
  • Engine 2.1.195 (CLI install at ~/.local/share/claude/versions/2.1.195) — TUI/ESC run
  • Desktop app 1.28929.0, macOS 26 (arm64)
  • Reproduced from clean working directories; five runs total across the three entry points, with two independently written harness scripts for the SDK path

Related

  • #47282 — "ESC during tool execution is reported to the assistant as \"rejected\"" — the same defect, reported 2026-04-13, closed by the stale bot 2026-05-25 (not_planned) and now locked, so it cannot be commented on. Filing fresh per the bot's own instruction. That report focused on the wording being untrue and on STOP what you are doing over-halting the model; it did not cover the non_execution_kind mis-stamp, the existence of an interrupted enum value, or the SDK / Desktop entry points.
  • anthropics/claude-agent-sdk-typescript#405 — the same scenario (interrupt during an in-flight tool call) surfacing as a misclassified error_during_execution to SDK consumers. Consistent with the result/error_during_execution + terminal_reason: aborted_tools seen here.
  • #84621 — the structurally identical problem for interrupted subagent dispatches: the parent cannot distinguish "never ran" from "ran and stopped mid-action".

Suggested fix

Stamp non_execution_kind: "interrupted" on this path and give it text that does not assert non-execution — something to the effect of the tool was interrupted while running; its effects, if any, are unknown; verify state before continuing — instead of reusing the permission-denial string.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗