Agent SDK: prompt cache invalidated every query() — random UUID in Bash tool description
Package: @anthropic-ai/claude-agent-sdk
Severity: Cost — 12x input token pricing penalty per turn
Affects: Any multi-turn agent using the SDK's query() API
Summary
The SDK embeds a random UUID in the Bash tool description that changes between query() calls, breaking Anthropic's prompt cache prefix match. Since tool definitions sit inside the cached prefix (~17K tokens), any byte change forces a full cache write ($3.75/Mtok) instead of a cache read ($0.30/Mtok) — a 12x cost penalty per turn.
Root cause
cli.js line ~12092 — Cf1("claude-settings", ".json") calls crypto.randomUUID() to generate a temp settings file path like:
/tmp/claude-settings-20fc829c-5ec4-43a6-93f9-62663f5517b1.json
This path is embedded in the Bash tool's denyWithinAllow sandbox restriction list in the tool description. Since the SDK spawns a new subprocess per query() call, a new UUID is generated each time, changing the tool description and busting the cache.
Evidence from captured API traffic
Call 1→2: claude-settings-20fc829c-5ec4-43a6-93f9-62663f5517b1.json
Call 2→3: claude-settings-90a7db64-23ee-477b-b8f3-e97eb1ba1d1f.json
Call 3→4: claude-settings-d6d53ae3-d79a-4df0-adde-80d87aa3e471.json
Each change causes cache_creation_input_tokens: ~16,800 with cache_read_input_tokens: 0 — a full miss every turn.
Cost impact
In a captured 8-turn session (before our proxy-level workaround), 6 of 8 Sonnet calls were full cache misses due to this bug alone. Observed waste:
- Cache writes (wasted): 95,352 tokens at $3.75/Mtok = $0.36
- Cache reads (saved): 87,267 tokens at $0.30/Mtok = $0.03
- Per-turn penalty: ~$0.06 per cache miss vs ~$0.005 for a hit
For a 10-turn session, this bug alone adds ~$0.50 in unnecessary cost.
Suggested fix
Use a deterministic path derived from the session or process, or accept a settings path via the query() options. The model never references this path — it's purely a sandbox restriction.
Workaround
We normalize the UUID to a zeroed stable value (00000000-0000-0000-0000-000000000000) in an HTTP proxy between the SDK and Anthropic's API. Same byte length, no content-length issues.
const SETTINGS_UUID_RE =
/claude-settings-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.json/g;
const STABLE_SETTINGS_FILENAME =
'claude-settings-00000000-0000-0000-0000-000000000000.json';
function normalizeRequestBody(body: string): string {
return body.replace(SETTINGS_UUID_RE, STABLE_SETTINGS_FILENAME);
}
Reproduction
- Run
query()twice with any prompt - Capture the request bodies (set
ANTHROPIC_BASE_URLto a logging proxy) - Diff the Bash tool descriptions — the
claude-settings-*.jsonpath will differ - Observe
cache_creation_input_tokens > 0andcache_read_input_tokens: 0on the second call
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗