[BUG] Windows: all shell output capture fails on VMware App Volumes — `stat` by path and `fstat` by handle report different `dev` for every local path

Status Open
Reported on v2.1.247
Maintainer reply None cached
Activity 2 comments · opened Sep 3, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

On a Windows VDI with a VMware App Volumes writable volume, every shell command loses its output. The tool result is:

<bash output unavailable: output file .../tasks/<id>.output could not be read (ENOENT).
This usually means another Claude Code process in the same project deleted it during startup cleanup.>

No other Claude Code process exists. Commands running longer than a few seconds are also killed. One user measured roughly 340 failures against 68 successes across their projects; nothing on 2.1.247 or earlier ever failed. It is not intermittent, it is the normal case, and it makes Claude Code unusable on this platform.

The cause is the task-output identity check. Claude Code verifies that an output file has the same identity by path as through the open handle. The App Volumes filter driver returns a different volume serial for path-based stat than for handle-based fstat, so the check can never pass. ino matches in every case; only dev differs, and by the same constant pair for every path on the drive.

No local directory passes, so CLAUDE_CODE_TMPDIR cannot be pointed anywhere on the machine that works. %TEMP%, C:\Temp, C:\ProgramData, C:\Users\Public and C:\Windows\Temp all fail, and the last three carry no per-user redirection at all. The only route that passes is the same directory reached over the SMB loopback, which brings its own problem (see Additional Information).

What Should Happen?

Shell commands should return their output. Where a file's inode matches and only its device id differs, that is the signature of a filesystem filter driver rather than of a swapped or replaced file, and the identity check should tolerate it in the same way 2.1.252 relaxed the equivalent macOS case.

Error Messages/Logs

<bash output unavailable: output file C:\Users\<user>\AppData\Local\Temp\claude-<id>\tasks\<id>.output could not be read (ENOENT). This usually means another Claude Code process in the same project deleted it during startup cleanup.>


Probe output showing the underlying condition. Both rows are the same directory, reached two ways (`X:` is `\\localhost\C$` mapped to a drive letter):


MISMATCH  C:\Temp    path=1849253077/48976645947699736  handle=1828635740/48976645947699736
OK        X:\Temp    path=1828635740/48695170970989080  handle=1828635740/48695170970989080


Relevant strings in the 2.1.259 binary, which is how the check was identified:


output file identity changed
lstat refused a swapped path
open refused a swapped path
file changed between lstat and open
by realpath; same directory (device and inode), treated as an alias
tasks dir moved or linked

Steps to Reproduce

Reproducing needs a Windows VDI with a VMware App Volumes writable volume attached, where the user profile is redirected through C:\SnapVolumesTemp\MountPoints\{GUID}\SVROOT. Steps 4 and 5 demonstrate the underlying filesystem condition and need only Node, so they can be run by anyone with that platform to confirm the cause without installing Claude Code.

  1. On such a machine, install Claude Code 2.1.259 (native install, or let the desktop app update itself).
  2. Start a session in any project and run any shell command that writes to stdout, for example Get-Date or ls.
  3. The command's side effects happen but the output is lost, with the error above. Repeat a dozen times: failure is the normal result, not an occasional one.
  4. Save this as probe.js:

``js
const fs = require('fs'), path = require('path');
const dir = process.argv[2];
const p = path.join(dir, 'probe.tmp');
fs.writeFileSync(p, 'x');
const fd = fs.openSync(p, 'r');
const a = fs.statSync(p), b = fs.fstatSync(fd);
console.log((a.dev === b.dev && a.ino === b.ino ? 'OK' : 'MISMATCH'), dir,
'path=' + a.dev + '/' + a.ino, 'handle=' + b.dev + '/' + b.ino);
fs.closeSync(fd); fs.unlinkSync(p);
``

  1. Run it against any local directory. Every one reports MISMATCH on dev, including directories with no per-user redirection:

``
node probe.js "C:\Temp"
node probe.js "C:\Windows\Temp"
node probe.js "C:\ProgramData"
``

  1. Reach the same directory over the SMB loopback and probe again. It reports OK, which confirms the two views disagree rather than either being wrong about the file:

``
net use X: \\localhost\C$
node probe.js "X:\Temp"
``

  1. Set CLAUDE_CODE_TMPDIR to X:/Temp in user settings (~/.claude/settings.json), fully quit and relaunch, and shell commands work normally. That confirms the identity check is what fails, not the filesystem.

Claude Model

Not sure / Multiple models

Is this a regression?

Yes, this worked in a previous version

Last Working Version

2.1.247

Claude Code Version

2.1.259 (Claude code), 2.1.255 (desktop app auto-update)

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

VS Code integrated terminal

Additional Information

Windows 11 x64, VDI with a VMware App Volumes writable volume.

PowerShell 7.6.4 and Git Bash (MINGW64). Both fail identically, so it is not shell-specific.

The SMB loopback workaround is not a solution. Two problems with it:

  1. It requires the C$ administrative share to be reachable by the user. Disabling admin shares is a routine hardening step, so a managed estate can lose this without warning and every affected user breaks at once.
  2. Directory enumeration through the loopback is unreliable. Listing the temp directory returns zero items while the same directory listed locally shows the session folder, and Test-Path reports a parent directory as absent while reporting a file inside it as present. Opening files by full path works, so sessions run normally, but startup cleanup enumerates that directory and therefore removes nothing, so old session folders accumulate indefinitely. This persisted across a logoff and a move to a different pool machine, so it is not a cache expiring.

So on this platform every route available to a user is either impossible or degraded.

Why this is not a duplicate of #65067:

| | #65067 | This report |
| --- | --- | --- |
| Platform | macOS, APFS | Windows, VMware App Volumes |
| Pattern | Intermittent, correlated with output size | Deterministic, every command |
| CLAUDE_CODE_TMPDIR relocation | Does not help | Fixes it, over SMB only |

Suggested fix. 2.1.252 fixed the same class of failure on macOS ("task output swap refused (tasks dir moved or linked)"). The binary already carries an alias-tolerance path that treats two renderings of a directory as the same when device and inode agree. The equivalent relaxation is needed where ino matches and only dev differs. Failing that, a documented way to opt out of the identity check would let affected users stay on current versions.

View original on GitHub ↗

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