[BUG] [Claude Desktop / Linux] Cowork & Dispatch hang on infinite spinner — app opens its own storage directories with O_DIRECT, ext4 returns EINVAL
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?
This is the Claude Desktop application (claude-desktop 1.37937.3, arm64, installed
from Anthropic's apt repo), NOT the Claude Code CLI.
The Cowork / Dispatch tab in Claude Desktop shows a loading spinner indefinitely and
never becomes usable. No error message is ever displayed. Restarting the app does not
help. The tab spins forever with no timeout and no error state.
Root cause: the app opens its own storage directories under ~/.config/Claude with the
O_DIRECT flag. O_DIRECT on a directory is invalid on ext4, so the kernel returns EINVAL,
and every write path under ~/.config/Claude fails.
strace shows the exact call:
openat(AT_FDCWD, "/home/pi/.config/Claude/vm_bundles/claudevm.bundle",
O_RDONLY|O_DIRECT|O_LARGEFILE|O_CLOEXEC) = -1 EINVAL (Invalid argument)
The same call with the same flags and the same errno fails on:
~/.config/Claude/vm_bundles/claudevm.bundle
~/.config/Claude/claude-code/2.1.246
~/.config/Claude/claude-code-sessions/<org>/<uuid>
~/.config/Claude/local-agent-mode-sessions/<org>/<uuid>
~/.config/Claude/local-agent-mode-sessions/<org>/<uuid>/rpm
~/.config/Claude/local-agent-mode-sessions/<org>/<uuid>/agent/local_ditto_<uuid>/outputs
~/.config/Claude/local-agent-mode-sessions/skills-plugin/<uuid>/<org>/skills
All of these are ordinary directories (drwx------ pi:pi). Confirmed on the same path:
python3: os.open(dir, O_RDONLY|O_DIRECT) -> EINVAL [Errno 22] Invalid argument
python3: os.open(dir, O_RDONLY) -> ok
python3: os.open(dir, O_RDONLY|O_DIRECTORY) + fsync -> ok
node: fs.promises.open(dir, 'r') -> ok
Call sites visible in main.log: ensureStorageDir / writeSessionToDisk,
_syncPlugins, ensurePluginStructure, primeSessionDirs, CCD downloader.
The failure cascades: the stale rootfs.img cannot be refreshed, the Claude Code SDK
2.1.246 download fails 3/3 attempts, session initialisation fails, QEMU is therefore
never spawned (pgrep -a qemu is empty while the app is running), and the UI shows a
spinner forever because the session it is waiting on never initialises.
This is NOT a missing-requirement problem. KVM, vhost_vsock, QEMU and the kvm group
are all correctly set up and the app never reports any of the documented
"Cowork requires ..." messages. See Environment below.
Cowork worked correctly on this machine until 2026-08-28 08:59 (clean VM shutdown in
the logs). It broke on the 09:04 restart, when the app first requested a new
rootfs.img version and could not write it.
I searched existing issues for "O_DIRECT" and "EINVAL" and found no report of this.
Related but not duplicates: #35116 (VM download fails because destination dir can't be
written, Windows/ENOENT), #37925 (Linux downloader failure), #24070 (requests that guest
console output be logged on failure — my kernel-console.log is 29 bytes and empty).
What Should Happen?
- The app should not pass O_DIRECT when opening directories. O_DIRECT is meaningless
on a directory and is rejected with EINVAL by ext4 (and other filesystems). A plain
open, or O_DIRECTORY where a directory handle is wanted, works correctly here.
- Cowork / Dispatch should start normally: VM bundle refreshes, SDK installs, QEMU
launches, session initialises.
- If startup does fail, the UI should surface an actionable error rather than an
indefinite spinner. The Cowork tab already has error states for missing QEMU, missing
KVM and missing vhost_vsock; a total storage failure like this should surface one too,
with a timeout rather than spinning forever.
Error Messages/Logs
--- strace (openat calls returning EINVAL) ---
openat(AT_FDCWD, ".../Claude/local-agent-mode-sessions/<org>/<uuid>/rpm", O_RDONLY|O_DIRECT|O_LARGEFILE|O_CLOEXEC) = -1 EINVAL (Invalid argument)
openat(AT_FDCWD, ".../Claude/vm_bundles/claudevm.bundle", O_RDONLY|O_DIRECT|O_LARGEFILE|O_CLOEXEC) = -1 EINVAL (Invalid argument)
openat(AT_FDCWD, ".../Claude/claude-code/2.1.246", O_RDONLY|O_DIRECT|O_LARGEFILE|O_CLOEXEC) = -1 EINVAL (Invalid argument)
openat(AT_FDCWD, ".../Claude/claude-code-sessions/<org>/<uuid>", O_RDONLY|O_DIRECT|O_LARGEFILE|O_CLOEXEC) = -1 EINVAL (Invalid argument)
openat(AT_FDCWD, ".../Claude/local-agent-mode-sessions/skills-plugin/<uuid>/<org>/skills", O_RDONLY|O_DIRECT|O_LARGEFILE|O_CLOEXEC) = -1 EINVAL (Invalid argument)
--- cowork_vm_node.log ---
[Bundle:status] rootfs.img version mismatch: have=6d1538ba…, want=2a762adf…
[setYukonSilverConfig] Stale bundle present; refreshing in background
[setYukonSilverConfig] Stale-bundle refresh failed (non-fatal): EINVAL: invalid argument, open '/home/pi/.config/Claude/vm_bundles/claudevm.bundle'
[linux-vm] Spawning helper: /usr/lib/claude-desktop/resources/cowork-linux-helper
[cowork-linux-helper] [server] listening on /run/user/1000/claude-cowork-vm.sock
[error] [download] VM download failed: EINVAL: invalid argument, open '/home/pi/.config/Claude/vm_bundles/claudevm.bundle'
(last successful run, before the break:)
[VM:shutdown] App quit, stopping VM … [vm] qemu exited: <nil> … Completed in 3644ms
--- main.log ---
[error] [CCD] Download attempt 1/3 failed Error: EINVAL: invalid argument, open '/home/pi/.config/Claude/claude-code/2.1.246'
[error] [CCD] Download attempt 2/3 failed …
[error] [CCD] Download attempt 3/3 failed …
[error] [CCD] All download attempts failed
[error] Failed to save session local_<uuid>: EINVAL: invalid argument, open '.../claude-code-sessions/<org>/<uuid>'
at async open (node:internal/fs/promises:640:25)
at async e.ensureStorageDir (…/index2.chunk-CknQXpFk.js)
at async e.writeSessionToDisk (…/index2.chunk-CknQXpFk.js)
[error] [LocalPlugins] Failed to sync remote plugins: EINVAL … /rpm
[warn] [SkillsPlugin] Periodic sync failed: EINVAL … /skills
[error] [audit] logger init failed — dropping entries { error: "EINVAL … /agent/local_ditto_<uuid>" }
[error] Session
Steps to Reproduce
- Install Claude Desktop 1.37937.3 (arm64) from Anthropic's apt repository on
Debian 13 (trixie), with ~/.config on an ext4 filesystem.
- Sign in and use Cowork/Dispatch successfully (worked here until 2026-08-28 08:59).
- Restart the app after the server-side rootfs.img version changes, so the app has to
refresh a stale VM bundle.
- Open the Cowork / Dispatch tab.
- Observe: spinner forever, no error shown.
pgrep -a qemuis empty while the app runs. - Confirm the cause directly — this reproduces standalone, no app needed:
python3 -c "import os,sys; d=os.path.expanduser('~/.config/Claude'); os.open(d, os.O_RDONLY|os.O_DIRECT)"
-> OSError: [Errno 22] Invalid argument
while a plain open of the same directory succeeds.
- Or capture it from the app:
strace -f -y -e trace=openat -o /tmp/claude-strace.log claude-desktop
grep EINVAL /tmp/claude-strace.log | grep '\.config/Claude'
Claude Model
_No response_
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
2.1.215
Platform
Other
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Other
Additional Information
Product: Claude Desktop app (Linux beta) — not the Claude Code CLI
Version: claude-desktop 1.37937.3, arm64 (apt: downloads.claude.ai stable)
OS: Debian GNU/Linux 13 (trixie)
Kernel: 6.18.34+rpt-rpi-2712, aarch64, page size 16384 (Raspberry Pi 5, 16 GB)
Filesystem: ext4 on NVMe, rw, noatime, 146 GB free, 8% inodes, no I/O errors in dmesg
Virtualisation: /dev/kvm and /dev/vhost-vsock both present; user in kvm group;
vhost_vsock loaded; qemu-system-arm, qemu-efi-aarch64, virtiofsd installed;
qemu-system-aarch64 -accel help lists kvm; systemd-detect-virt = none
The target directory is created but left empty:
~/.config/Claude/claude-code/2.1.215 250M (working, previously installed)
~/.config/Claude/claude-code/2.1.246 4.0K (empty, created 2026-08-28 09:04)
The app successfully creates the directory, then fails on the subsequent
O_DIRECT open of that same directory, so no SDK content is ever written.
3 Comments
Correction — the placeholders in the path list above were stripped as HTML. The
affected directories are:
[error] Session initialization failed for local_ditto_<uuid>: EINVAL: invalid argument,
open '.../local-agent-mode-sessions/<org>/<uuid>/agent/local_ditto_<uuid>/outputs'
at async e.primeSessionDirs (…/index2.chunk-Dkhfb34G.js)
--- kernel-console.log ---
(29 bytes, blank lines only — guest never produced output; last written 2026-07-23)
--- also present, likely separate ---
[error] Electron safeStorage encryption is not available on this system, cannot store allowlist cache
Your strace line is the key to this, but I think it's pointing somewhere other than where it looks.
The app never asked for
O_DIRECT. It asked forO_DIRECTORYand passed the x86_64 value forit. On aarch64 that value happens to mean
O_DIRECT, so strace decoded a flag nobody wrote.The private-directory helper is:
Perfectly reasonable code. The constants it reads are wrong. Measured inside Anthropic's own shipped
arm64 Electron (42.10.0 / Node 24.18.1,
process.arch === "arm64"):| constant | arm64 build reports | correct aarch64 value | |
|---|---|---|---|
|
O_DIRECTORY| 65536 (0o200000) | 16384 (0o40000) | ← x86_64 value ||
O_NOFOLLOW| 131072 (0o400000) | 32768 (0o100000) | ← x86_64 value ||
O_DIRECT| 16384 (0o40000) | 65536 (0o200000) | ← x86_64 value |The other 12
O_*flags I checked (O_RDONLY,O_WRONLY,O_RDWR,O_CREAT,O_EXCL,O_TRUNC,O_APPEND,O_NONBLOCK,O_SYNC,O_DSYNC,O_NOCTTY,O_NOATIME) are all correct. Exactlythe three whose values differ between x86_64 and aarch64 are wrong, and all three carry the x86_64
value — the signature of
node_constants.cccompiled against x86_64fcntl.hwhile targetingaarch64.
These constants rotate between the two architectures in the Linux uapi headers:
| constant | x86_64 | aarch64 |
|---|---|---|
|
O_DIRECT|0o40000|0o200000||
O_DIRECTORY|0o200000|0o40000||
O_NOFOLLOW|0o400000|0o100000||
O_LARGEFILE|0o100000|0o400000|So the intended
O_DIRECTORY(0o200000) isO_DIRECTon aarch64 — andO_DIRECTon a directoryis unconditionally
EINVAL:do_dentry_open()rejects it because directory inodes have nodirect_IOin theiraddress_space_operations. Not ext4-specific, not filesystem-specific, and nomount option avoids it. Your
O_LARGEFILEin that same strace line is the other half of the rotationshowing up.
Minimal repro, no Anthropic software involved
Plain kernel-level check, runs anywhere on aarch64:
Line 3 is the intent and it works. Line 4 is the effect and reproduces the exact error. Line 5
isolates the responsible bit.
To read the constants out of the shipped binary directly (
runAsNodeis fused off, so this needs athrowaway copy): flip the first fuse byte after the sentinel
dL7pKGdnNz796PbbjQWNKmHXBZaB9tsXfrom0to1in a copy ofclaude-desktop, place it beside the realresources/(otherwise it abortswith
Invalid file descriptor to ICU data received), then:This is one bug, not three
I think this also accounts for #89850 and #90354. Same helper, same three constants:
the version directory by accident. The observable is identical —
open()on the directory, EINVAL— but the filename is fine; the directory is opened deliberately, to
chmodit safely, and it'sthe flags that are wrong.
downloadBinaryForTarget()calls the helper before it logsDownloading from …and before itopens a socket, which is why the CLI download dies with zero network I/O and the 3-attempt retry loop
surfaces it as
Download failed. Check your internet connection and try again.That message isactively misleading and worth fixing on its own — it sent all three of us looking at the network
first.
Five subsystems share the helper, matching your directory list:
| subsystem | user-visible effect |
|---|---|
|
[CCD]binary manager | Claude Code sessions cannot start (hard block) ||
[CCDScheduledTasks]| scheduled tasks never persist ||
[ScheduledTasks]| same, agent-mode side ||
[RemotePluginManager]| plugin sync fails every poll ||
[SkillsPlugin]| skills sync fails every poll |Plus
vm_bundles, which is the spinner you're seeing.Fix
Derive the flags from the running platform's
fs.constantsrather than baked-in x86_64 numbers — or,upstream of that, build
node_constants.ccagainst the aarch64 headers sofs.constantsis right forevery consumer, not just this call site.
I patched the asar locally to use the correct aarch64 values and all five subsystems plus the
Cowork/Dispatch bundle load came back green: the CLI downloads, sessions start, scheduled tasks
persist, and plugin/skills sync stops erroring. So the constants really are the whole story here —
nothing else needed changing.
Happy to test a candidate build on Asahi Linux (Apple M1) if that's useful.
Confirming this on a second arm64 machine, and adding two things this thread doesn't have yet: why the flag is
O_DIRECT, and how much user data it destroys.Still present in 1.40609.0
Same failure, newer build —
claude-desktop 1.40609.0, aarch64 Linux, kernel 6.17 (NVIDIA DGX Spark, GB10), ext4 on NVMe. It survived at least two releases past the 1.37937.3 reported here. On this machine 1.18286.0 was fine, so the regression landed between those.Root cause: x86-64
open()constants in the arm64 bundleThe app isn't asking for
O_DIRECT— it's asking forO_DIRECTORY|O_NOFOLLOWusing x86-64 numeric values, which decode to something else entirely on aarch64:| Flag | x86-64 value | What that value means on aarch64 |
|---|---|---|
|
O_DIRECTORY|0o200000(0x10000) |O_DIRECT||
O_NOFOLLOW|0o400000(0x20000) |O_LARGEFILE|(aarch64's own
O_DIRECTORYis0o40000,O_NOFOLLOWis0o100000.)That exactly reproduces the observed
O_RDONLY|O_DIRECT|O_LARGEFILE— the intent was a safe directory open, andO_DIRECTon a directory isEINVAL. It also explains why every affected call site is a directory open, and why nothing else in the app misbehaves.Suggested fix: use
fs.constants.O_DIRECTORY/fs.constants.O_NOFOLLOWinstead of baked-in numeric literals (or gate the literals onprocess.arch). Node already exposes the correct per-architecture values.Severity: this silently destroys sessions, not just Cowork
On this machine the same bug hit
ensureStorageDir → writeSessionToDisk1,241 times over two days, with no error surfaced in the UI. Consequences:Recovery was only possible because the CLI writes its own transcripts to
~/.claude/projects/independently of the broken store — the app's log still hadMapping internal session <local_id> to CLI session <cli_id>lines, so the store records could be reconstructed and joined back to their claude.ai tiles by timestamp. Without that log, the sessions would simply be gone.Worth flagging because a spinner in one tab and silent permanent data loss are very different priorities.
Workaround
An
LD_PRELOADshim that rewrites0x10000|0x20000back toO_DIRECTORY|O_NOFOLLOW, scoped to paths under~/.config/Claude, fully restores session saving and goes inert once the constants are correct.app.asarintegrity is fused, so patching the bundle isn't an option — which leaves ordinary users with no fix at all.