Per-session scratchpad UUID in the system prompt is the only cross-session prompt diff, invalidating the cached prefix on every new session
Summary
The system prompt embeds the scratchpad directory path, which contains a
per-session UUID. That UUID is the only thing that differs between two
otherwise byte-identical requests from consecutive sessions in the same project —
and because it sits inside the cached prefix, everything after it is invalidated
on every new session.
I captured two real request bodies from two consecutive claude -p "hi" runs in
the same directory and diffed them:
| block | size | across sessions |
|---|---|---|
| tools (137 MCP tools) | 223,596 chars | byte-identical, same order |
| messages | 50,534 chars | byte-identical |
| system | 7,483 chars | differs at exactly one place |
The single difference:
...-scratchpad/fc9aab5b-da05-4cc7-bbe2-410a1c472151/scratchpad
...-scratchpad/88966ec9-2cb4-4872-bc6b-624b66257462/scratchpad
from this block of the system prompt:
IMPORTANT: Always use this scratchpad directory for temporary files ...
`/private/tmp/claude-501/<project>/<SESSION-UUID>/scratchpad`
The same UUID also appears in metadata.user_id, but that field is not part of
the prompt and is harmless.
Measured cost
Against a local model (so I can read usage.input_tokens directly and control
the cache), 65,303-token prompt, warm model, idle M5 Max:
| request | wall | input_tokens |
|---|---|---|
| cold | 104.3 s | 65,303 |
| identical prompt again | 0.3 s | 5 |
| next session — only the UUID differs | 30.6 s | 16,157 |
| same request with the UUID normalised | 0.4 s | 5 |
Normalising that one UUID in a proxy turns a 30.6 s re-prefill into 0.4 s.
End-to-end with MCP enabled, three consecutive real sessions went from
131.7 s → 17.3 s → 8.3 s.
Why this likely matters for hosted use too
I measured locally because that is where I can see the cache directly. But the
mechanism is not local-specific: the volatile UUID sits in system, ahead of the
tool definitions and the conversation, so it is inside whatever prefix acache_control breakpoint covers. If that is how the hosted path is laid out,
every new session in a project pays a full system+tools cache write instead of a
read, for a value that has no bearing on the model's output. I have not measured
the hosted side and am not claiming a number for it — but it seems worth checking,
since the fix is cheap either way.
Suggested fixes, cheapest first
- Move the scratchpad path out of the cached prefix — put it in the last
user turn (or a system-reminder near the end) rather than in the stable system
block. Zero behaviour change, and the prefix becomes session-invariant.
- Make the path derivable and stable — e.g. key it on the project path plus a
stable session ordinal, or reuse one scratchpad per project. The directory is
temporary storage; its name does not need to be unique per session for the
model to use it correctly.
- If a unique path per session is genuinely required, **place the volatile
segment as late as possible** in the prompt so that the tool definitions
(223 KB here, by far the largest block) stay in the shared prefix.
Related
- #92033 — mid-conversation tool/MCP list changes invalidating the prefix. Same
cache, different trigger; that one is within a session, this one is across
sessions.
- #90953 — a "why did this cache miss" diagnostic would have found this in
minutes instead of a byte-level diff of two captured request bodies.
Environment
Claude Code 2.1.260, macOS 26.5, M5 Max. Reproduced by pointingANTHROPIC_BASE_URL at a local capture server and diffing the two request
bodies; no special configuration needed to see the UUID differ.