Summary
Setting maxThinkingTokens to any value in agentQuery() options breaks all hook dispatch — zero SubagentStart, SubagentStop, Notification, PreCompact, and all other hook events fire for custom agents. This is not a threshold issue; values of 1000, 4096, 10000, 16000, and 100000 all reproduce the identical failure. The parameter must be entirely absent from the options object.
Tracked upstream: anthropics/claude-agent-sdk-typescript#25
Technical Root Cause — Full Trace
1. Parameter Flow Through the SDK
When maxThinkingTokens is passed to agentQuery(), the SDK processes it through this chain:
agentQuery({ options: { maxThinkingTokens: N } })
→ query() in sdk.mjs:15087
→ destructured at sdk.mjs:15127: maxThinkingTokens
→ passed to ProcessTransport at sdk.mjs:15181: maxThinkingTokens
→ ProcessTransport.spawn() at sdk.mjs:6534-6536:
if (maxThinkingTokens !== undefined) {
args.push("--max-thinking-tokens", maxThinkingTokens.toString());
}
→ spawns: node cli.js --output-format stream-json --verbose
--input-format stream-json
--max-thinking-tokens 4096 ← THIS FLAG
--max-turns 500
--model claude-sonnet-4-5-20250929
--betas context-1m-...,interleaved-thinking-...
--permission-mode bypassPermissions
...
The SDK spawns cli.js (the bundled Claude Code executable) as a child process. The --max-thinking-tokens flag is passed as a CLI argument.
2. Hook Registration (Works Correctly)
Hooks are registered during Query.initialize() (sdk.mjs:7891-7925):
async initialize() {
let hooks;
if (this.hooks) {
hooks = {};
for (const [event, matchers] of Object.entries(this.hooks)) {
if (matchers.length > 0) {
hooks[event] = matchers.map((matcher) => {
const callbackIds = [];
for (const callback of matcher.hooks) {
const callbackId = `hook_${this.nextCallbackId++}`;
this.hookCallbacks.set(callbackId, callback); // Stored in Map
callbackIds.push(callbackId);
}
return { matcher: matcher.matcher, hookCallbackIds: callbackIds, timeout: matcher.timeout };
});
}
}
}
const initRequest = {
subtype: "initialize",
hooks, // Sent to CLI process
sdkMcpServers,
agents: this.initConfig?.agents
};
const response = await this.request(initRequest);
}
The hooks are correctly serialized and sent to the CLI process via the initialize control request. The callback IDs are stored in this.hookCallbacks Map. This part works — the hooks are registered.
3. Hook Dispatch (Broken by --max-thinking-tokens)
When the CLI process needs to fire a hook, it sends a control_request with subtype hook_callback back to the SDK:
// sdk.mjs:7865-7866 — inside processControlRequest()
} else if (request.request.subtype === "hook_callback") {
const result = await this.handleHookCallbacks(
request.request.callback_id, request.request.input, request.request.tool_use_id, signal
);
// sdk.mjs:8045-8052
handleHookCallbacks(callbackId, input, toolUseID, abortSignal) {
const callback = this.hookCallbacks.get(callbackId);
if (!callback) {
throw new Error(`No hook callback found for ID: ${callbackId}`);
}
return callback(input, toolUseID, { signal: abortSignal });
}
The CLI process never sends these hook_callback control requests when --max-thinking-tokens is active. The CLI's internal event loop/streaming pipeline is disrupted by the thinking flag, causing it to suppress all lifecycle events while still producing correct final output.
4. What the CLI Sends vs. Doesn't Send
Without --max-thinking-tokens (WORKING):
CLI → SDK: control_request { subtype: "hook_callback", callback_id: "hook_0", input: { agent_type: "Explore" } }
CLI → SDK: control_request { subtype: "hook_callback", callback_id: "hook_0", input: { agent_type: "Plan" } }
CLI → SDK: { type: "system", subtype: "init", session_id: "..." }
CLI → SDK: { type: "stream_event", event: { type: "content_block_delta", ... } } ← streaming works
CLI → SDK: control_request { subtype: "hook_callback", callback_id: "hook_0", input: { agent_type: "securities-researcher" } }
CLI → SDK: control_request { subtype: "hook_callback", callback_id: "hook_0", input: { agent_type: "case-law-analyst" } }
... (all 40+ subagent hooks fire)
With --max-thinking-tokens (BROKEN):
CLI → SDK: control_request { subtype: "hook_callback", callback_id: "hook_0", input: { agent_type: "Explore" } }
CLI → SDK: control_request { subtype: "hook_callback", callback_id: "hook_0", input: { agent_type: "Plan" } }
CLI → SDK: { type: "system", subtype: "init", session_id: "..." }
CLI → SDK: { type: "stream_event", event: { type: "content_block_delta", ... } } ← streaming partially works
← ZERO further hook_callbacks
← custom agent hooks NEVER fire
Built-in agents (Explore, Plan) fire at initialization time — before the thinking pipeline engages. Custom agents fire during execution when the Task tool dispatches them, by which point the thinking pipeline has suppressed all event dispatch.
Affected Hook Types
All hook types registered in HOOK_EVENTS (sdk.mjs:6381-6394) are affected:
| Hook Type |
Status with maxThinkingTokens |
Status without |
SubagentStart |
BROKEN — zero events for custom agents |
Working |
SubagentStop |
BROKEN — zero events |
Working |
Notification |
BROKEN — zero events |
Working |
PreCompact |
BROKEN — zero events |
Working |
PreToolUse |
BROKEN — zero events |
Working |
PostToolUse |
BROKEN — zero events |
Working |
PostToolUseFailure |
BROKEN — untested, assumed broken |
Untested |
SessionStart |
Fires (init-time) |
Fires |
SessionEnd |
Untested |
Untested |
Stop |
Untested |
Untested |
The pattern: hooks that fire at initialization time (before the first API call) still work. Hooks that fire during execution (after the thinking pipeline starts) are completely suppressed.
Empirical Evidence
Broken Run (Feb 17 — maxThinkingTokens: 16000)
From a 3,759-line SSE capture with maxThinkingTokens: 16000, SDK 0.2.44:
| SSE Event Type |
Count |
system_info |
1 |
system_init |
1 |
thinking_start / thinking |
~495 |
tool_call (Task dispatching subagents) |
8 |
hook_event (SubagentStart) for custom agents |
0 |
hook_event (SubagentStop) |
0 |
hook_event (Notification) |
0 |
hook_event (PreCompact) |
0 |
The orchestrator spawned 8 subagents via Task tool calls but zero hook callbacks were received.
Broken Run (Feb 18 — maxThinkingTokens: 4096)
Same behavior with lower threshold. SDK 0.1.61:
- Hooks registered correctly (hookCallbacks Map populated)
- Explore/Plan SubagentStart fired at init (before thinking)
- Custom agent SubagentStart: 0 events
Working Run (Feb 18 — maxThinkingTokens omitted)
From 5,606-line SSE capture, SDK 0.1.61:
| SSE Event Type |
Count |
hook_event (SubagentStart) — Explore |
1 |
hook_event (SubagentStart) — Plan |
1 |
hook_event (SubagentStart) — custom agents |
9+ |
hook_event (SubagentStop) — custom agents |
9+ |
hook_event (PreCompact) |
1 |
| Text streaming (delta events) |
Hundreds |
Custom agent hooks fired correctly:
employment-labor-analyst (timestamp 1771438545610)
commercial-contracts-analyst (1771438545611)
tax-structure-analyst (1771438545612)
case-law-analyst (1771438545613)
patent-analyst (1771438545615)
regulatory-rulemaking-analyst (1771438545616)
Working Run (Feb 16 — maxThinkingTokens absent, Opus 4.6)
From session manifest 2026-02-16-1771206424/session-manifest.log:
- 67+ SubagentStart events for custom agents
- All hooks firing correctly
- Model was
claude-opus-4-6, no maxThinkingTokens, only context-1m beta
Commit History — When maxThinkingTokens Was Introduced
| Commit |
Date |
Change |
Hook Status |
f7af3e7 |
Feb 16 |
No maxThinkingTokens in agentQuery |
Working — 67+ custom agent hooks |
b5f47bc |
Feb 17 |
Added maxThinkingTokens: 16000 |
Broken — 0 custom agent hooks |
46be06c |
Feb 17 |
SDK upgrade 0.39→0.74, 0.1.61→0.2.44 (kept maxThinkingTokens) |
Broken — 0 custom agent hooks |
| Current |
Feb 18 |
Commented out maxThinkingTokens, reverted SDK |
Working — 9+ custom agent hooks |
The Feb 16 working state (f7af3e7) had these agentQuery options:
{
model: 'claude-opus-4-6',
maxTurns: 500,
// NO maxThinkingTokens
betas: ['context-1m-2025-08-07'], // No interleaved-thinking, no effort
hooks: manifestHooksConfig,
agents: getLegalSubagents()
}
Reproduction
// BROKEN — ANY value breaks hooks (tested: 1000, 4096, 10000, 16000)
for await (const message of agentQuery({
prompt,
options: {
model: 'claude-sonnet-4-5-20250929',
maxThinkingTokens: 4096, // any value
hooks: manifestHooksConfig,
agents: getLegalSubagents(),
includePartialMessages: true,
betas: ['interleaved-thinking-2025-05-14', 'context-1m-2025-08-07']
}
}))
// WORKING — parameter must be entirely absent (not null, not 0, absent)
for await (const message of agentQuery({
prompt,
options: {
model: 'claude-sonnet-4-5-20250929',
// maxThinkingTokens: OMITTED
hooks: manifestHooksConfig,
agents: getLegalSubagents(),
includePartialMessages: true,
betas: ['interleaved-thinking-2025-05-14', 'context-1m-2025-08-07']
}
}))
Impact on Our System
- Frontend dashboard: Phase pipeline UI (SubagentStart/SubagentStop drive
hookSSEBridge.js → classifyAgent() → SSE subagent_start/subagent_stop events) shows zero agent activity
- Session manifest: NDJSON event log (
session-manifest.log) contains zero custom agent lifecycle events, making post-run analysis impossible
- Tool usage tracking:
SubagentStop hooks parse transcripts for tool usage counters (subagentToolUsage) — all counters stay at 0
- Gate checks: SubagentStop hook runs report verification against expected file paths — silently skipped
- Auto-recovery: PreCompact hook injects recovery context before compaction — never fires, so compaction loses critical state
- Silent failure: No errors, warnings, or degraded output quality — the orchestrator still produces correct final output, hooks are just silently dead
Upstream Confirmation
anthropics/claude-agent-sdk-typescript#25 confirms this is a known issue:
- @InsanePrototyper (Jan 6, 2026): "When maxThinkingTokens is set, StreamEvent stops emitting entirely... 0 StreamEvents — nothing comes through. No tool start, no input deltas, no text deltas. Completely silent until the final AssistantMessage."
- @ashwin-ant (Anthropic, Jan 6): Acknowledged, requested repro details
- @sccorby (Jan 12): "Can confirm I am encountering this issue with the Python SDK also."
- Our comment (Feb 18): Confirmed zero hook events with raw SSE log evidence across SDK versions 0.1.61 and 0.2.44
The issue affects both TypeScript and Python Agent SDKs and has been open since October 2025.
Workaround
Remove maxThinkingTokens entirely from agentQuery() options. The bundled CLI process (cli.js) manages thinking internally per model using its own default budget. Thinking still occurs (confirmed by output quality analysis) — just without an explicit external budget constraint.
// In claude-sdk-server.js line 938:
// maxThinkingTokens: Number(process.env.SDK_MAX_THINKING_TOKENS || 4096),
// ↑ Disabled — breaks SubagentStart/Stop hooks (Agent SDK Issue #25)
Environment
@anthropic-ai/claude-agent-sdk: 0.1.61 (also reproduced on 0.2.44)
@anthropic-ai/sdk: 0.39.0 (also reproduced on 0.74.0)
- Model:
claude-sonnet-4-5-20250929 (also reproduced on claude-opus-4-6)
- Betas:
interleaved-thinking-2025-05-14, context-1m-2025-08-07
- Node: v22.x, macOS Darwin 23.5.0
- 40 registered custom subagents, 134+ MCP tools
Related Issues
Summary
Setting
maxThinkingTokensto any value inagentQuery()options breaks all hook dispatch — zeroSubagentStart,SubagentStop,Notification,PreCompact, and all other hook events fire for custom agents. This is not a threshold issue; values of 1000, 4096, 10000, 16000, and 100000 all reproduce the identical failure. The parameter must be entirely absent from the options object.Tracked upstream: anthropics/claude-agent-sdk-typescript#25
Technical Root Cause — Full Trace
1. Parameter Flow Through the SDK
When
maxThinkingTokensis passed toagentQuery(), the SDK processes it through this chain:The SDK spawns
cli.js(the bundled Claude Code executable) as a child process. The--max-thinking-tokensflag is passed as a CLI argument.2. Hook Registration (Works Correctly)
Hooks are registered during
Query.initialize()(sdk.mjs:7891-7925):The hooks are correctly serialized and sent to the CLI process via the
initializecontrol request. The callback IDs are stored inthis.hookCallbacksMap. This part works — the hooks are registered.3. Hook Dispatch (Broken by
--max-thinking-tokens)When the CLI process needs to fire a hook, it sends a
control_requestwith subtypehook_callbackback to the SDK:The CLI process never sends these
hook_callbackcontrol requests when--max-thinking-tokensis active. The CLI's internal event loop/streaming pipeline is disrupted by the thinking flag, causing it to suppress all lifecycle events while still producing correct final output.4. What the CLI Sends vs. Doesn't Send
Without
--max-thinking-tokens(WORKING):With
--max-thinking-tokens(BROKEN):Built-in agents (Explore, Plan) fire at initialization time — before the thinking pipeline engages. Custom agents fire during execution when the
Tasktool dispatches them, by which point the thinking pipeline has suppressed all event dispatch.Affected Hook Types
All hook types registered in
HOOK_EVENTS(sdk.mjs:6381-6394) are affected:maxThinkingTokensSubagentStartSubagentStopNotificationPreCompactPreToolUsePostToolUsePostToolUseFailureSessionStartSessionEndStopThe pattern: hooks that fire at initialization time (before the first API call) still work. Hooks that fire during execution (after the thinking pipeline starts) are completely suppressed.
Empirical Evidence
Broken Run (Feb 17 —
maxThinkingTokens: 16000)From a 3,759-line SSE capture with
maxThinkingTokens: 16000, SDK 0.2.44:system_infosystem_initthinking_start/thinkingtool_call(Task dispatching subagents)hook_event(SubagentStart) for custom agentshook_event(SubagentStop)hook_event(Notification)hook_event(PreCompact)The orchestrator spawned 8 subagents via Task tool calls but zero hook callbacks were received.
Broken Run (Feb 18 —
maxThinkingTokens: 4096)Same behavior with lower threshold. SDK 0.1.61:
Working Run (Feb 18 —
maxThinkingTokensomitted)From 5,606-line SSE capture, SDK 0.1.61:
hook_event(SubagentStart) — Explorehook_event(SubagentStart) — Planhook_event(SubagentStart) — custom agentshook_event(SubagentStop) — custom agentshook_event(PreCompact)Custom agent hooks fired correctly:
employment-labor-analyst(timestamp 1771438545610)commercial-contracts-analyst(1771438545611)tax-structure-analyst(1771438545612)case-law-analyst(1771438545613)patent-analyst(1771438545615)regulatory-rulemaking-analyst(1771438545616)Working Run (Feb 16 —
maxThinkingTokensabsent, Opus 4.6)From session manifest
2026-02-16-1771206424/session-manifest.log:claude-opus-4-6, nomaxThinkingTokens, onlycontext-1mbetaCommit History — When
maxThinkingTokensWas Introducedf7af3e7maxThinkingTokensin agentQueryb5f47bcmaxThinkingTokens: 1600046be06cmaxThinkingTokens)maxThinkingTokens, reverted SDKThe Feb 16 working state (
f7af3e7) had these agentQuery options:Reproduction
Impact on Our System
hookSSEBridge.js→classifyAgent()→ SSEsubagent_start/subagent_stopevents) shows zero agent activitysession-manifest.log) contains zero custom agent lifecycle events, making post-run analysis impossibleSubagentStophooks parse transcripts for tool usage counters (subagentToolUsage) — all counters stay at 0Upstream Confirmation
anthropics/claude-agent-sdk-typescript#25 confirms this is a known issue:
The issue affects both TypeScript and Python Agent SDKs and has been open since October 2025.
Workaround
Remove
maxThinkingTokensentirely fromagentQuery()options. The bundled CLI process (cli.js) manages thinking internally per model using its own default budget. Thinking still occurs (confirmed by output quality analysis) — just without an explicit external budget constraint.Environment
@anthropic-ai/claude-agent-sdk: 0.1.61 (also reproduced on 0.2.44)@anthropic-ai/sdk: 0.39.0 (also reproduced on 0.74.0)claude-sonnet-4-5-20250929(also reproduced onclaude-opus-4-6)interleaved-thinking-2025-05-14,context-1m-2025-08-07Related Issues