[Bug] REPL cat() returns empty string ("") on repeat reads instead of a sentinel — downstream scripts truncate/clobber real files
Environment
- Claude Code 2.1.201, macOS (darwin 25.5.0), model claude-fable-5
- REPL tool enabled via
"env": { "CLAUDE_CODE_REPL": "true" }in settings.json
Summary
The experimental REPL tool's cat(path) shorthand dedupes against content it already returned earlier in the session: the first read returns the file content, but any subsequent cat() that would return the same content again returns an empty string — indistinguishable from a genuinely empty file. No error text, no sentinel. This holds across REPL calls and even within a single script.
Presumably this is a context-token-saving elision ("the model already has this content"), which is a reasonable goal — but signaling it with '' is destructive, because the REPL's documented contract is "shorthands never throw — they return the error text on failure". An elided read returns success-shaped nothing.
Reproduction (deterministic)
One REPL script:
o.reads = []
for (let i = 0; i < 5; i++) {
const t = await cat('/path/to/any/existing/file.md', 1, 2)
o.reads.push(t ? t.length : 'EMPTY')
}
o
// → [12, "EMPTY", "EMPTY", "EMPTY", "EMPTY"]
Cross-call variant: cat(f) in one REPL invocation returns the content; cat(f) in a later invocation returns '' while sh('wc -c f') confirms the file is unchanged on disk (reproduced with a 1444-byte file). If the file's content changes between reads (e.g. via the Write tool), the next cat() returns the new content in full — the elision keys on repeated identical content, not on the path alone.
Why this is worse than a display quirk
The empty string flows into whatever the script does next, and the model has no way to distinguish "elided" from "empty file":
- File truncation:
const t = await cat(template); await put(target, t)— if the template was cat'd earlier in the session,t === ''andput()faithfully writes a 0-byte file. We hit exactly this in production use (a tracked rule file truncated to 0 bytes). For weeks this was misdiagnosed as "put() truncates files". - Document clobbering: a session re-reads its own memory-index file, gets
'', concludes the index is empty, and "rebuilds" it with only one line — destroying ~109 index entries. (We had precisely this failure; the elision is the most plausible trigger.) - Any read-modify-write, diff, or compose pattern silently degrades the same way.
The failure surfaces far downstream of the read, so each incident tends to get blamed on a different layer (put(), sandboxing, external tools), which is why it kept recurring for us.
Expected behavior
Any of these would be fine — just not bare '':
- Return an explicit sentinel consistent with the REPL's error convention, e.g.
"[elided] unchanged since previous read of <path> — content already in context", so a guard likeif (!t || t.startsWith('[elided]'))(or even naive string use) fails loud instead of silent; - or return the content again (correctness over token savings for a tool whose output feeds writes);
- or make elision opt-in per call.
Notes
put()itself behaved correctly in all our experiments: it returns"[error] File has not been read yet…"for unread targets and writes faithfully otherwise — the "truncation" was it writing an elided empty read.- Couldn't find this in docs or existing issues (REPL appears undocumented; closest is #52175, unrelated).
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗