[Bug] REPL cat() returns empty string ("") on repeat reads instead of a sentinel — downstream scripts truncate/clobber real files

Open 💬 1 comment Opened Jul 4, 2026 by dbl-0

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":

  1. File truncation: const t = await cat(template); await put(target, t) — if the template was cat'd earlier in the session, t === '' and put() 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".
  2. 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.)
  3. 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 like if (!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).

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗