Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Summary
When Claude Code withdraws a permission request it sent T3 (control_cancel_request, seen by the adapter as the canUseTool abort signal), the Claude V2 adapter returns but never closes the approval it drew. The request stays pending, its node and approval_request item stay waiting, and Approve fails with No pending Claude runtime request.
Claude Code withdraws a request whenever something else answers it first. It runs PermissionRequest hooks at the same moment it sends can_use_tool, and cancels the T3 request when the hook answers. A hook that approves in bypass mode therefore leaves one stuck card per prompt.
This is the approval half of #15517. #22803 withdraws aborted AskUserQuestion requests and states that approval requests are unchanged. #22023 (Stop) and #22982 (run end, tools that never ran) don't cover it either: here the turn completes normally and the tool ran.
Steps to reproduce
-
Add a
PermissionRequesthook to the Claude home'ssettings.jsonthat allows in bypass mode:{ "hooks": { "PermissionRequest": [ { "hooks": [ { "type": "command", "command": "/path/to/allow.sh" } ] } ] } }#!/usr/bin/env bash # allow.sh [ "$(jq -r .permission_mode)" = bypassPermissions ] && echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
-
In a Full access Claude thread, have Claude run a command that trips one of Claude Code's bypass-immune safety checks, for example
echo victim > dir.txt; mkdir -p victim; touch victim/a; F=$(cat dir.txt); rm -f $F/*("Dangerous rm operation on possibly-empty variable path"). -
The command runs at once, because the hook answered. An approval card for it is still shown.
-
Press Approve on that card.
Expected behavior
When the CLI withdraws a request nobody answered, its card closes as cancelled. The request, node and item settle the way #22803 settles a withdrawn question.
Actual behavior
Observed on the desktop app (0.1.2) with Claude Code 2.1.308:
- The command ran and the turn continued. The hook's log shows the approval at 02:29:57Z.
orchestration_v2_projection_runtime_requests: kindcommand, statuspending,resolvedAtnull. Nodeapproval_requestand itemapproval_requestarewaiting, withfullAccessOverride: "provider_check"and the reason "Dangerous rm operation on possibly-empty variable path: $F/* ...".- It was still pending after the turn finished. The turn-end sweep only closes native
user_inputrequests (ProjectionStore.ts#L4731). - Pressing Approve on two such cards produced 20 failed
ClaudeAdapterV2.respondToRuntimeRequestspans, eachProviderAdapterRuntimeRequestResponseErrorcaused byNo pending Claude runtime request .... - While a request is pending, the thread can't be settled (
Orchestrator.ts#L3015), snoozed, resumed after a usage limit or idle-compacted.
The SDK side, outside T3: with @anthropic-ai/claude-agent-sdk 0.3.276, permissionMode: "bypassPermissions", the hook above and a canUseTool that waits 15 s like a person, Claude Code called canUseTool for the same command and aborted it after 15 ms. The command then ran. Without the hook, the callback's answer decides it: an immediate deny kept the files.
Cause
After awaitClaudeApprovalDecision resolves through the abort signal, the adapter deletes its pendingRuntimeRequests entry and returns. It never emits an update for the request, node or turn item it opened, so nothing makes them terminal until a server restart reconciles them. A respond after that hits the No pending Claude runtime request branch.
The same path should apply to anything else that aborts a pending approval: Auto mode's classifier answering while a card is open, or a steer (#21895).
Possible fix
Mirror #22803 on the approval path: when the decision Deferred is not done after the race (the signal won, not a person's answer), emit cancelled for the request, node and approval item. A person's answer, including Cancel, keeps its own resolution. I have this change with two adapter tests: a withdrawn request closes (fails on current main with the card left pending/waiting), and an answered request keeps its answer (fails if the fix cancels on any cancel decision). I can open a PR if that approach suits you.
Impact
Major degradation or frequent failure
Version or commit
Desktop 0.1.2; same code at main @ 24c80f3
Environment
Linux (Omarchy), T3 Code desktop 0.1.2, Claude Code 2.1.308, Claude Opus 5.5, Full access thread
Workaround
Restarting the T3 server cancels the stuck requests. Writing rm targets as literal paths or guarded variables ("${F:?}"/*) avoids the safety check, so no request is sent.
Activity
guychai commentedon Oct 25, 2026
Also seeing this on Windows 11 with T3 Code desktop 0.1.2 and Claude Code 2.1.310 (native install), in a Full access Claude thread.
Setup was a
PermissionRequesthook (matcherBash|PowerShell) that allows only whenpermission_modeisbypassPermissions. The command was a multi-line Bash script whoserm -rf $OUT/*trips thedangerousRemovalsafety check.What the logs show:
permission_decision: {"decision":"accept","source":"hook","reason_type":"hook"}, and the command ran.server.trace.ndjsonstill has anawaitClaudeApprovalDecisionspan for it, lasting ~116 ms (129 ms on a second repro). That matches the CLI sendingcan_use_tooland withdrawing it once the hook answers.I also tried adding a
PreToolUsehook that returnspermissionDecision: "allow"in bypass mode. It doesn't avoid the request: the safety check still sendscan_use_toolto T3, so the card still appears. Closing withdrawn requests on the T3 side, as you proposed, looks like the only fix.Dropje97 commentedon Nov 2, 2026
Also seen with no
PermissionRequesthook at all. The request is withdrawn when the turn moves on, not when a hook answers.Setup: a self-hosted server on a local build of
0.1.4-nightly.20261028.3841(Linux), and a Full access Claude thread (a delegated child task,bypassPermissions) on Claude Code 2.1.312.~/.claude/settings.jsonhas no hooks and no ask rules.What happened:
$( … )and a backtick inside a quotedgrep -oE/sed -Epattern. One of Claude Code's bypass-immune bash checks asked, and T3 projected acommandruntime request with an approval card."Tool permission request aborted"for thattool_use_id, so the CLI withdrew the request after about 17 minutes.pending(orchestration_v2_projection_runtime_requests,resolved_atnull). A person clicked the card at 07:28Z, about three hours later, believing something was blocked.Since 2026-10-28 this install has had six
commandrequests from Full access Claude threads. Each was closed only by a person's click or a server restart. The cards look like a blocked agent, so people approve them although nothing waits.The fix proposed above, emitting
cancelledwhen the decisionDeferredis still open after the abort wins, would cover this case as well.juliusmarminge commentedon Nov 9, 2026
Thanks for the detailed report and reproductions. #27320 fixes withdrawn Claude approvals on main in 41588d4520.