[BUG] /export produces 0-byte files and empty clipboard — rendering pipeline returns empty string
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
/export consistently produces empty output. Both the file and clipboard paths generate 0 bytes of content. The command appears to succeed — the "Conversation exported to: ..." message is displayed — but the resulting file is 0 bytes. Clipboard export similarly reports success while the clipboard is empty. This occurs regardless of conversation length, including long sessions with hundreds of messages.
What Should Happen?
/export test-output should produce a non-empty file containing the conversation transcript. /export → "Copy to clipboard" should place the conversation text on the system clipboard.
Error Messages/Logs
No error is displayed. The success message shows normally:
"Conversation exported to: test-output.txt"
But:
$ ls -la test-output.txt
-rw-r--r-- 1 user staff 0 Apr 9 19:30 test-output.txt
Steps to Reproduce
- Start a Claude Code session (CLI, Homebrew cask binary)
- Have any conversation (even a single exchange works)
- Run
/export test-output - Check file size:
ls -la test-output.txt→ 0 bytes - Or: run
/export, select "Copy to clipboard", paste → empty
Claude Model
Opus
Is this a regression?
Yes; /export worked in previous versions.
Claude Code Version
2.1.98 (Claude Code)
Platform
Anthropic API
Operating System
Multiple: macOS, also confirmed on Fedora Linux by @Cianidos
Terminal/Shell
iTerm2
Additional Information
I traced through the minified source in the npm package (cli.js from @anthropic-ai/claude-code@2.1.98). The rendering pipeline for /export is:
/export → agY() → ogY() → zi8() → igY() → c58()
Root cause: c58 (render-to-string) returns empty, and igY has no recovery path.
c58 renders the transcript via Ink to a PassThrough stream. It captures only the first data event:
async function c58(q, K) {
let _ = "", z = false, Y = new PassThrough();
if (K !== undefined) Y.columns = K;
return Y.on("data", (O) => {
if (z) return;
z = true;
_ = O.toString();
}),
await (await XB(createElement(FkY, null, q),
{ stdout: Y, patchConsole: false })).waitUntilExit(),
_;
}
The PassThrough has no columns/rows/isTTY properties (the Ink renderer defaults to 80×24, so dimensions aren't the direct cause). The FkY wrapper exits via setTimeout(exit, 0) after one render frame.
Then igY breaks on the first empty chunk with no fallback:
for (let j = 0; j < $; j += A) {
let H = await w([j, j + A]);
if (MO(H).trim() === "") break; // all-or-nothing exit
await _(H);
}
MO is stripAnsi. If the rendered frame is whitespace/ANSI-only, trim() yields "" and the entire loop exits immediately — zero content is collected.
Both export paths then silently write the empty string:
- File:
_i8(filename, "")→ 0-byte file + success message - Clipboard:
WP("")→pbcopywith empty stdin + success message
Three possible triggers (couldn't narrow further without debugger access):
A. Stream timing — The Homebrew cask binary runs on Bun (Bun.FFI / Bun.stripANSI confirmed in binary strings). Bun's PassThrough may not synchronously emit the data event on .write(). If it fires after waitUntilExit() resolves, _ stays "".
B. Empty screen render — renderFullFrame (the non-TTY path, since PassThrough.isTTY is undefined) iterates screen cells. If Yoga layout produces zero-height content, the output is only newlines → stripAnsi().trim() → "".
C. Render context — c58 creates a fresh Ink instance with a bare AppStateProvider (no initialState). If the transcript component reads any state from context rather than its direct props, it sees an empty default state.
Suggested fixes:
- Collect all
datachunks instead of only the first event - Use
continueinstead ofbreakinigY, or remove the early exit - Report empty export as a failure — don't show a success message for 0-byte output
This issue has 13 comments on GitHub. Read the full discussion on GitHub ↗