Claude Code Bug: Message Interruption Misreported as User Refusal

Claude Code 2.1.301 fixed a bug where tool calls interrupted by user messages during permission checks were incorrectly logged as user refusals, conflating explicit decisions with scheduling cancellations; the article argues for a distinct 'not_executed' status with cause and retryable fields to preserve accurate model behavior and audit metrics.

Architecture Development Notes
Architecture Development Notes
Architecture Development Notes
Claude Code Bug: Message Interruption Misreported as User Refusal

Documentation Defines Three Distinct Paths

First, normal queuing: documentation states that when a user sends a message while Claude is working, Claude Code queues the message instead of interrupting the current turn; if queued during tool execution, the message is delivered after those tools finish, still within the same turn. Thus a message does not truncate a running tool call.

Second, genuine refusal: pressing Esc on a permission prompt refuses that action, equivalent to an unannotated "No". An annotated "No" sends the annotation as the refusal reason to Claude, which continues working; an unannotated "No" stops the current turn in the main session. On a background sub-agent, Esc only refuses that single tool call without stopping the sub-agent. Refusal is an explicit decision with a target and follow‑up semantics. Pressing Esc is a decision, not an abort; the scenario under discussion is when approval has not yet returned and a message arrives first.

Third, messages: a message carries new information — additional parameters, scope changes, or even making the original call more appropriate. It is not a verdict on any action. The problem is timing: if a message arrives while permission is still pending, the runtime faces both an undecided approval and a new turn input. It may let the approval continue or void the call, but it must not record "the call did not execute" as "the user refused this call".

The Model Only Sees the Result, Not the UI State

For the model, the tool result is the sole record of the call's outcome. If the result says "user refused", the model reads an authorization fact: this action is not allowed, or the user does not want it to happen. The model may then abandon the path, apologize, switch tools, or re‑ask. Yet the user's actual words might have been "use staging instead", where the correct next step is to retry with new parameters, not treat the action as vetoed.

This error also propagates beyond the model. Some teams use refusal rate as a signal for tool description or default behavior: if a tool is often refused, they rewrite descriptions, parameters, or defaults. If interruptions are counted as refusals, the metric mixes timing issues unrelated to the tool itself. This inference assumes teams actually aggregate reports or statuses this way; the release notes provide no such data. But once the attribution semantics are wrong, any report built on it becomes unreliable.

The reverse boundary must also hold. Fixing this attribution must not turn every abort into "retryable". Genuine refusal remains a decision, especially when unannotated and the current turn has stopped; auto‑retry would bypass the user's explicit choice. The fix is not to weaken refusal's effect, but to stop writing a decision that never happened.

Pending Calls Need a Third Outcome

Viewing permission check as a state machine yields states: request sent, awaiting decision, approved, refused. Events like message interruption, session end, or mode switch can abort a call before any verdict. They differ from pressing Esc: Esc is an explicit refusal decision; here no decision was formed. Such outcomes are neither approval nor refusal.

A schematic result format separating three concerns:

{
  "status": "not_executed",
  "cause": "superseded_by_user_message",
  "retryable": true
}
status

indicates the call did not execute, cause explains why, retryable tells the model whether it may retry with new information. Field names may vary, but all three are essential. Only "not executed" leaves the model unsure whether to retry or give up; only "user refused" feeds the model a stance the user never expressed.

Auditing should split on the same axis. Refusal is an authorization record; cancellation is a scheduling record. If teams aggregate using the tool result's report or status, refusals belong in refusal rate, permission review, and rule tuning; cancellations belong in interruption cause, wait time, and concurrency statistics. Mixing them makes any conclusion from either side unreliable.

Who Made the Decision?

The fix itself occupies one line in the release notes, yet the takeaway check is simple: for every tool result, can you identify who made the decision the result claims? If the answer is "no one", it must not be written as "user refused".

The release notes do not specify the exact post‑fix wording, so verification in a real session is still needed. But the attribution requirement is clear: a call may stop because of a message, but the user's decision must not be fabricated as a result.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

state machineAI agenttool callingAudit Loggingbug fixpermission handlingretry logicClaude Code
Architecture Development Notes
Written by

Architecture Development Notes

Focused on architecture design, technology trend analysis, and practical development experience sharing.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.