Windows: background agent .output files always 0 bytes (symlink fallback)

Resolved 💬 3 comments Opened Apr 12, 2026 by Meme-Theory Closed Apr 12, 2026

Bug Description

On Windows, background agent output files (.output JSONL transcripts) remain 0 bytes for the entire agent lifecycle and after completion. The completion notification returns results correctly, but the output file is never populated.

This is NOT the Git 2.53.0 bash stdout issue (though that's related). This is specific to background agent transcript files.

Environment

  • Windows 11 Pro 10.0.26200
  • Claude Code CLI (latest, also reproduced on prior versions)
  • PowerShell AND Git Bash (MINGW64) — both affected
  • CLAUDE_CODE_TMPDIR remapped to ~/.claude/tmp — no change (rules out temp dir permissions)

Root Cause (traced through source)

The bug is in utils/task/diskOutput.ts:

  1. initTaskOutputAsSymlink() (line ~438) tries to create a symlink from the .output file to the agent's .jsonl transcript
  2. Windows symlinks require admin/developer privileges — the fs.symlink() call fails
  3. The fallback (line ~447) calls initTaskOutput() which creates an empty 0-byte file
  4. The agent's conversation is written to the .jsonl transcript via the project backend (recordSidechainTranscript() in tools/AgentTool/runAgent.ts)
  5. The .output file has no connection to the .jsonl file after the symlink fails — it stays 0 bytes permanently

Why Bash Works But Agents Don't

| Component | Mechanism | Works on Windows? |
|:--|:--|:--|
| Bash tool (utils/Shell.ts) | Opens file with fs.openSync(), passes fd via stdio[1] — child writes directly to file handle | Yes — OS-level fd redirect |
| Background agents (bridge/sessionRunner.ts) | stdio: ['pipe', 'pipe', 'pipe'] — parent reads via readline, writes to project backend separately | No — relies on symlink .output.jsonl |

The Bash tool bypasses the pipe entirely by giving the child process a file descriptor. Background agents use pipes + a symlink, and the symlink is the part that breaks on Windows.

Proposed Fix

In diskOutput.ts, replace the symlink fallback with one of:

  1. Hard link (fs.linkSync) — works on Windows without admin privileges, same semantics as symlink for read purposes
  2. Junction (fs.symlinkSync with 'junction' type) — Windows junctions work without elevation for directories
  3. Periodic copy/tail — write .output from the same source that feeds the .jsonl
  4. Direct fd redirect — match the Bash tool pattern: open the .output file, pass fd to the child process stdio

Option 1 (hard link) is the simplest one-line fix:

// Current (fails on Windows without admin):
fs.symlinkSync(targetPath, outputPath);

// Fix:
try {
  fs.linkSync(targetPath, outputPath);  // hard link works without admin on Windows
} catch {
  fs.symlinkSync(targetPath, outputPath);  // fall back to symlink on Unix
}

Note: hard links require both paths on the same volume, which they are (both under the temp/project directory).

Related Issues

  • #17147 — Background agents output files remain empty
  • #14521 — Background agents cannot write files with run_in_background=true
  • #21352 — Agents complete but output files are empty
  • #32252 — Background tasks report "completed" but output files missing or empty
  • #21915 — Bash tool produces no output on Windows (separate Git 2.53.0 issue)

Reproduction

  1. Run Claude Code on Windows (any shell)
  2. Spawn a background agent: Agent({ prompt: "echo hello", run_in_background: true })
  3. Check the output file path returned in the launch message
  4. File is 0 bytes during execution AND after completion
  5. The completion notification correctly returns the agent's result (proving the agent ran fine)

View original on GitHub ↗

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