Agent SDK: prompt cache invalidated every query() — random UUID in Bash tool description

Resolved 💬 4 comments Opened Feb 23, 2026 by RobGruhl Closed Mar 23, 2026

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

  1. Run query() twice with any prompt
  2. Capture the request bodies (set ANTHROPIC_BASE_URL to a logging proxy)
  3. Diff the Bash tool descriptions — the claude-settings-*.json path will differ
  4. Observe cache_creation_input_tokens > 0 and cache_read_input_tokens: 0 on the second call

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗