Claude Code deferred tool use for QA gives a non-interactive integration a clean way to pause at one pending tool call, hand the request to a person, and resume the same session after an answer arrives. This is useful when a CI assistant, internal QA portal, or Agent SDK wrapper needs a human decision but has no terminal in which to ask.
The happy path is simple: a PreToolUse hook returns defer, the process exits with a structured pending call, the wrapper collects an answer, and the session resumes. The difficult part is proving that the approval still belongs to the same session, tool, input, repository state, and policy—and that retrying the handoff does not execute the action twice.
This tutorial builds a private disposable approval lab, exercises single-call and batched behavior, rejects stale and replayed responses, verifies expiry and crash recovery, and keeps deterministic tests plus human merge authority outside the agent.
What Anthropic officially documents
Anthropic’s current hooks reference says permissionDecision: "defer" is for integrations that run Claude Code in non-interactive print mode and read structured output, including Agent SDK apps and custom user interfaces. It is honored only with -p. In an interactive session, Claude Code logs a warning and ignores the result.
When one tool call is deferred, the tool does not execute. The process exits with stop_reason: "tool_deferred", a session_id, and deferred_tool_use containing the pending tool’s ID, name, and input. The caller later resumes that session. The same pending call enters PreToolUse again, and the hook can allow it with updated input or deny it.
There are four constraints that drive the QA design:
- Single call: defer works only when the turn contains one tool call. With several calls at once, it is ignored and the calls continue through normal permission handling.
- No built-in deadline: there is no defer timeout or retry limit. Session files remain subject to the configured cleanup sweep, which defaults to 30 days.
- Decision precedence: when hooks disagree, the order is deny, defer, ask, then allow.
- Permissions remain authoritative: an allow result does not bypass deny or ask rules, and an updated input replaces the complete tool input object.
1. Build a disposable approval lab
Create a private repository with no production credentials, customer data, deploy keys, package publishing, or writable external integrations:
deferred-tool-lab/
.claude/settings.json
.claude/hooks/defer-approval.js
wrapper/approval-service.ts
fixtures/questions.json
fixtures/answers.json
evidence/handoffs.jsonl
tests/handoff.spec.ts
The main fixture asks one synthetic question: which approved test profile should a read-only validation plan use? Provide two harmless options such as a short smoke profile and an extended regression profile. The eventual action may create one marker inside evidence/; it must not contact a remote system or change product code.
Record the Claude Code build, wrapper and hook hashes, configuration sources, repository SHA, working tree hash, session ID, tool-use ID, tool name, canonical input hash, permission mode, relevant allow, ask, and deny rules, sandbox state, approval actor and role, queue record ID, creation time, expiry, and expected side effects.
2. Configure one focused PreToolUse hook
Use a matcher that covers the synthetic question tool or other specifically approved lab action. The hook reads a wrapper-owned handoff record and either defers, allows with a complete answer, or denies:
{
"hooks": {
"PreToolUse": [
{
"matcher": "AskUserQuestion",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/defer-approval.js",
"timeout": 10
}
]
}
]
}
}
On the first encounter, the script exits successfully and prints one JSON object containing the PreToolUse event name and permissionDecision: "defer". It should not claim that an answer exists, modify the question, or create an external side effect.
On resume, it must locate exactly one approved handoff whose session, tool-use ID, tool name, canonical input hash, repository identity, and status match. For AskUserQuestion, echo the complete original questions array and add the documented answers map in updatedInput. Do not send a partial replacement because updated input replaces the entire object.
3. Define the handoff record as a state machine
A queue row should be machine-readable and append-only. A practical state model is:
created -> deferred -> awaiting_human
awaiting_human -> approved | denied | expired | abandoned
approved -> resuming -> executed | resume_failed
denied -> closed
Include an idempotency key derived from the session ID, tool-use ID, tool name, canonical input hash, and repository SHA. Store the human answer separately from the immutable request. Every transition needs an actor, timestamp, previous state, new state, reason category, and evidence hash.
Make approval a one-time compare-and-set transition. Two workers must not move the same row from approved to resuming. Keep a separate execution ledger so a network retry cannot look like a new approval.
4. Prove the first process stops cleanly
Run the lab through claude -p or the Agent SDK wrapper. Assert that the result is successful but its stop reason is tool_deferred. Verify that deferred_tool_use contains the expected tool ID, tool name, and complete input, and that the result’s session ID is saved.
Then inspect independent evidence:
- the question tool did not execute;
- no answer was invented;
- no marker, commit, comment, network request, or remote object appeared;
- the repository and working tree stayed unchanged;
- the transcript and queue record contain the same correlation identities;
- logs contain no credentials or raw environment dump.
A user-facing “waiting” message is not enough. The structured result and absent side effects are the pass criteria.
5. Test interactive mode separately
Launch the same scenario in an ordinary interactive Claude Code session. Anthropic says defer is ignored there with a warning. Confirm that the wrapper does not create an approval task as if the process had paused. The call must proceed through the ordinary interactive permission and question flow.
Record this as expected surface behavior, not a defect. A wrapper should reject interactive output that lacks stop_reason: tool_deferred even if a transcript contains the word “defer.”
6. Test the single-call boundary
Create one fixture that produces exactly one question tool call and a second controlled fixture that produces two calls in the same turn. The first should defer. The second must not be represented as a successful handoff because Anthropic documents that batch deferral is ignored and the calls continue through normal permission handling.
Keep baseline permissions restrictive for the batch case. Verify that every emitted call is independently asked, denied, or sandboxed according to normal policy. The test fails if the wrapper stores one batch member while another action runs without a correlated decision.
7. Resume the exact pending call
After a test approver selects a valid option, atomically mark the matching record approved. Resume using the recorded session ID. On the repeated PreToolUse event, compare every frozen identity again before returning allow and the complete updated input.
Verify that the question ID and wording are unchanged, the answer maps to an offered option, and the repository SHA and policy state are still acceptable. Then prove exactly one bounded action follows. Record the new process ID, transcript event, tool result, marker hash, Git state, and completion status.
If the hook sees no matching approval, a mismatched identity, an expired row, or changed protected state, it should defer again or deny according to the lab policy. It must never select the “closest” pending request.
8. Challenge replay and concurrency
Run the following negative cases:
| Case | Expected behavior |
|---|---|
| Duplicate resume request | One worker acquires the execution transition; the other receives an already-handled result |
| Wrong session ID | Reject without looking up another pending call |
| Right session, wrong tool-use ID | Reject as identity mismatch |
| Right IDs, changed input hash | Reject and require a new approval |
| Two approvers race | Only the first valid state transition wins |
| Resume after execution | No second tool action |
| Wrapper retry after response loss | Return recorded outcome instead of executing again |
Measure side effects independently. One “completed” queue row does not prove there is only one process, file change, or external action.
9. Detect repository and policy drift
A human may answer minutes or days after defer. During that interval the branch, working tree, hooks, permissions, sandbox, MCP inventory, model configuration, or test data can change. Decide which fields are immutable and which require re-approval.
At minimum, invalidate approval when the repository SHA, pending tool input, hook hash, relevant deny or ask rules, sandbox boundary, or destination changes. For an intentionally updated branch, create a new handoff instead of silently attaching the old answer.
Permission rules still run after a hook returns allow. Test a managed or project deny rule against the resumed action and confirm it remains blocked. The hook may tighten access, but it cannot grant past an authoritative deny or ask rule.
10. Validate hostile and malformed answers
Treat the answer as untrusted data. Exercise an unknown option, duplicated option, empty value, oversized value, newline injection, Unicode look-alike, nested object instead of string, stale form version, answer for another question, and text instructing the agent to ignore its constraints or expose secrets.
Map allowed answers from a server-side catalog. Never concatenate the answer into a shell command or path. Reject values that were not present in the frozen question. Redact synthetic secret canaries in queue, logs, transcripts, screenshots, and errors.
11. Add expiry, abandonment, and cleanup tests
Anthropic documents no defer timeout or retry limit. Define your own short laboratory expiry, such as 15 minutes, and a business-appropriate production policy. When the deadline passes, transition the request to expired and ensure resume returns deny or a new deferral that requires fresh review.
Test an unanswered request, explicit human denial, user cancellation, wrapper shutdown, session-file deletion, cleanup sweep, and resume after the configured retention window. Anthropic notes that cleanupPeriodDays defaults to 30 days, but your wrapper must not depend on a session surviving until that maximum.
Cleanup should close queue rows, revoke ephemeral approval tokens, remove only owned temporary artifacts, and preserve the minimal audit record. It must not delete unrelated sessions or user files.
12. Recover safely from crashes
Crash the wrapper after it saves the deferred result, after approval but before resume, during resume, and after tool completion but before acknowledging the result. On restart, reconcile the append-only queue with the transcript, session state, process evidence, filesystem, and execution ledger.
Unknown outcomes should become indeterminate, not automatically retried. A reviewer must determine whether a side effect already occurred. Use idempotent actions where possible and attach a stable action key to every downstream request.
13. Test multiple-hook precedence
Add four controlled PreToolUse handlers that return allow, ask, defer, and deny for the same synthetic call. Anthropic documents the precedence as deny over defer, defer over ask, and ask over allow. Verify every pair and the full set.
Hooks run in parallel, so avoid designs where several handlers modify the same input. The approval hook should own the deferred question transformation, while other hooks remain read-only validators or explicit blockers.
14. Build the approval evidence package
Save the official-feature review date, Claude Code build, wrapper and hook hashes, configuration sources, session and tool IDs, canonical input and repository hashes, queue state history, actor and role, answer schema version, expiry, resume attempt IDs, hook decisions, permission results, process and filesystem observations, Git evidence, logs, cleanup outcome, and human disposition.
Block rollout when defer is accepted without the structured stop reason, a batch is recorded as one pending call, identity or state drift is ignored, an invalid answer reaches updated input, a duplicate resume creates another action, an expired approval executes, a deny rule is bypassed, logs expose canaries, crash reconciliation guesses, or cleanup removes the wrong data.
Screenshot-friendly walkthrough
- Official deferred-tool-use lifecycle and documented constraints
- Disposable repository with hook, wrapper, fixtures, evidence, and tests
- Focused PreToolUse hook configuration for the synthetic question
- Structured result showing tool_deferred, session ID, and pending tool identity
- Independent proof that no action ran before approval
- Approval queue state transition with immutable correlation fields
- Resume event showing the same tool call and complete updated input
- Single-call success beside batched-call fallback evidence
- Replay, expiry, repository-drift, and hostile-answer rejection matrix
- Final execution ledger, permission result, deterministic tests, and human merge gate
Official Anthropic references
- Claude Code hooks reference
- Claude Code hooks guide
- Agent SDK TypeScript reference
- Claude Code permissions
Final takeaway
Claude Code deferred tool use for QA is best treated as a durable approval protocol, not a pause button. Freeze the pending call’s identity and state, accept one schema-valid human decision, resume only the same session, make execution idempotent, reject drift and replay, and test abandonment as carefully as approval. Keep ordinary permissions, deterministic CI, and human merge authority in force throughout.
