[BUG] Desktop (Linux ARM64): session registry saves fail with EINVAL — sessions vanish from sidebar on restart

Status Open
Reported on v2.1.237
Maintainer reply None cached
Activity 4 comments · opened Sep 2, 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 the Claude Desktop app for Linux ARM64, every attempt to persist a session's sidebar entry fails with EINVAL. Sessions work normally while the app is running, but exist only in memory — after an app restart they vanish from the sidebar entirely (they do not appear under the Archived filter either). The failure is silent; nothing is surfaced to the user, so it presents as unexplained loss of session history. I lost a full overnight working session from the sidebar this way.

Root cause (verified on the affected machine): the storage-dir verification in the app bundle ($r / mkdirPrivate in index.chunk-Cv8AY_mc.js, called from ensureStorageDir → writeSessionToDisk in index2.chunk-BXE3SynL.js) opens the directory with O_RDONLY | O_DIRECTORY | O_NOFOLLOW, but the flag values used are x86-64 values, not aarch64 values. On aarch64, x86-64's O_DIRECTORY bit (0o200000) means O_DIRECT, and open() with O_DIRECT on a directory returns exactly EINVAL (errno -22).

Verified empirically against the actual storage directory:

  • os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) → native aarch64 flags: succeeds
  • os.open(path, os.O_RDONLY | 0o200000 | 0o400000) → x86-64 flag values: [Errno 22] Invalid argument

The claude-desktop binary is a native aarch64 ELF and system Node reports correct aarch64 fs.constants, so the bundle's constants object is presumably a bundler shim (constants-browserify-style) hardcoding x86-64 Linux values instead of runtime fs.constants.

The filesystem itself is ext4, healthy, and writable (verified by creating a file in the same directory).

Suggested fix: use runtime require('fs').constants (per-platform correct) in the mkdirPrivate verification, or skip unsupported bits.

What Should Happen?

Session sidebar entries should be written to ~/.config/Claude/claude-code-sessions/<accountId>/<orgId>/local_<id>.json on every save, so sessions persist in the sidebar across app restarts — as they did before the update. If a save fails repeatedly, the user should be warned instead of silently losing session history.

Error Messages/Logs

From ~/.config/Claude/logs/main.log — repeated on every save (73 occurrences since the update):

[error] Failed to save session local_a8741449-3800-4faf-b18d-ebed017d1993: EINVAL: invalid argument, open '/home/parallels/.config/Claude/claude-code-sessions/<accountId>/<orgId>' { errno: -22, code: 'EINVAL', syscall: 'open',
  stack: "Error: EINVAL: invalid argument, open '...'
    at async open (node:internal/fs/promises:640:25)
    at async Object.$r (/usr/lib/claude-desktop/resources/app.asar/.vite/build/index.chunk-Cv8AY_mc.js:1:265681)
    at async e.ensureStorageDir (/usr/lib/claude-desktop/resources/app.asar/.vite/build/index2.chunk-BXE3SynL.js:1:310044)
    at async e.writeSessionToDisk (/usr/lib/claude-desktop/resources/app.asar/.vite/build/index2.chunk-BXE3SynL.js:1:318136)" }

Last successful write of a local_*.json entry in that directory: 2026-09-01 08:26. All writes fail from 2026-09-01 09:07 onward — the exact minute the embedded Claude Code runtime updated from 2.1.237 to 2.1.247.

Steps to Reproduce

  1. Run the Claude Desktop app (deb, arm64) on an aarch64 Linux machine (mine: Ubuntu VM under Parallels on Apple Silicon).
  2. Start a new Claude Code session in any project and exchange a few messages.
  3. Observe ~/.config/Claude/logs/main.log: every save logs "Failed to save session ... EINVAL: invalid argument, open '.../claude-code-sessions/<accountId>/<orgId>'".
  4. Quit and reopen the desktop app.
  5. The session is gone from the sidebar (also absent from the Archived filter). The CLI transcript still exists under ~/.claude/projects/.

Minimal reproduction of the underlying syscall bug on any aarch64 Linux box:
python3 -c "import os; os.open(os.path.expanduser('~'), os.O_RDONLY | 0o200000 | 0o400000)"
→ EINVAL, because 0o200000 (x86-64 O_DIRECTORY) is O_DIRECT on aarch64.

Claude Model

Other

Is this a regression?

Yes, this worked in a previous version

Last Working Version

Embedded Claude Code runtime 2.1.237 (saves worked until the in-place update to 2.1.247 on 2026-09-01)

Claude Code Version

2.1.247 (embedded in Claude Desktop 1.40609.0, deb arm64)

Platform

Other

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Other

Additional Information

  • Data is recoverable: transcripts survive in ~/.claude/projects/<project>/<cliSessionId>.jsonl and resume fine with claude --resume <cliSessionId>. Only the desktop sidebar registry entries are lost.
  • Sidebar entries can be reconstructed by hand-writing local_<id>.json files (sessionId + cliSessionId + cwd + timestamps) into ~/.config/Claude/claude-code-sessions/<accountId>/<orgId>/ — the app then shows the session again after restart, confirming only the write path is broken.
  • Because writes fail before touching existing files, previously saved sessions from before the update still list correctly — only sessions created or modified after the update are affected.

View original on GitHub ↗

4 Comments

dhruvkej9 · 1 month ago

Confirming this on a second, independent machine — and with one correction to the "last working version", because the trigger looks like the desktop app upgrade rather than the embedded CLI update.

Environment: claude-desktop 1.40609.0, arm64 .deb, Ubuntu on Linux 6.17.0-1020-oracle (bare-metal Ampere, not a Parallels VM), $HOME on ext4 (rw,relatime,discard,errors=remount-ro,commit=30). Identical stack, same silent failure, same result: two multi-hour sessions (97 and 73 turns) gone from the sidebar.

[error] Failed to save session local_39ea8704-…: EINVAL: invalid argument,
open '/home/ubuntu/.config/Claude/claude-code-sessions/<acct>/<org>'
  { errno: -22, code: 'EINVAL', syscall: 'open' }
    at async open (node:internal/fs/promises:640:25)
    at async Object.$r (app.asar/.vite/build/index.chunk-Cv8AY_mc.js:1:265681)
    at async e.ensureStorageDir (app.asar/.vite/build/index2.chunk-BXE3SynL.js:1:310044)
    at async e.writeSessionToDisk (app.asar/.vite/build/index2.chunk-BXE3SynL.js:1:318136)
    at async zs.flush (app.asar/.vite/build/index2.chunk-BXE3SynL.js:1:233000)

163 EINVALs in one day, 84 of them Failed to save session. Every launch since logs Loaded 1 persisted sessions — that one entry is from 2026-08-21, i.e. everything after it is unreachable from the UI.

The 2.1.247 update is not the trigger

My box never completed the 2.1.247 update and is still running 2.1.237, yet saves are broken anyway:

  • ~/.config/Claude/claude-code/2.1.247/ was created at 03:05 and is empty — [CCD] Download attempt 1/3 … 2/3 … 3/3 failed, [CCD] All download attempts failed, all with the same EINVAL on the version directory.
  • ~/.config/Claude/claude-code/2.1.237/ still holds the real .payload / .verified, and every transcript written since records "version":"2.1.237".

What did change at that moment is the app itself. main.log is unrotated back to 2026-08-21 and contains zero EINVALs before this line:

2026-09-03 03:05:21 [info] [updater] Version changed since last launch: 1.34493.1 → 1.40609.0
2026-09-03 03:05:27 [warn] [RemotePluginManager] Periodic sync failed: EINVAL …   ← first EINVAL, 6s after app-ready

apt upgraded the package on 2026-08-29 08:33 (upgrade claude-desktop:arm64 1.34493.1 1.40609.0); 03:05 on 09-03 was simply the first launch on it. So the failing constants arrived with claude-desktop 1.40609.0, and the [CCD] Initialized with version 2.1.247 line is the app intending to use 2.1.247, not evidence that it did. Worth adjusting "Last Working Version" to the app version — the CLI is a red herring, and chasing the 2.1.237→2.1.247 diff would miss it.

This is consistent with your aarch64 flag analysis, which I think is the correct explanation. I arrived at O_DIRECT independently by probing the failing directory before finding this issue, and your reading of why O_DIRECT is being passed subsumes mine — I had no explanation for why the app would deliberately set it on a directory:

| flags | result |
|---|---|
| O_RDONLY \| O_DIRECTORY (native aarch64) | OK |
| O_RDONLY \| O_SYNC | OK |
| O_TMPFILE \| O_RDONLY | OK |
| O_WRONLY \| O_DIRECTORY | EISDIR |
| O_RDWR \| O_DIRECTORY | EISDIR |
| O_RDONLY \| O_CREAT | EISDIR |
| O_TMPFILE \| O_RDWR | EISDIR |
| O_RDONLY \| O_DIRECT | EINVAL |

mkdir(recursive) and writeFile() into the same directory both succeed, so the directory is fine — only that open fails. Aligns with #90359's strace (O_RDONLY|O_DIRECT|O_LARGEFILE|O_CLOEXEC → EINVAL) and with #90703's Electron-constants report; the three look like one root cause surfacing in different subsystems.

Reads are unaffected, so the lost sessions are recoverable

Worth noting for anyone hitting this, since the data-loss label is what makes it urgent: nothing is actually lost. The CLI transcripts under ~/.claude/projects/<slug>/*.jsonl are complete, and only the desktop index is missing. Rebuilding local_<uuid>.json from a transcript restores the sidebar entry and resume works — cliSessionId is the field that matters, custom-title / cwd / timestamps all come out of the transcript, and the app's own Mapping internal session local_X to CLI session Y log lines recover the original pairing. Load path never touches ensureStorageDir, so hand-written entries survive.

That also suggests a cheap mitigation independent of the flag fix: when an index entry is missing but a transcript exists, reconstruct it on load instead of dropping the session — plus surface a warning when persistence has been failing, rather than discovering it at restart.

BasedGPT · 1 month ago

The evidence separates the layers cleanly: EINVAL is in Desktop's registry write path, while the JSONL transcript survives and claude --resume remains usable. That is a missing durable session-list record, not a deleted transcript.

I built a toolkit for this file-layer distinction: diagnose.py in BasedGPT/claude-code-session-recovery reads the platform's metadata and ~/.claude/projects/ transcript layers and prints a route when the state matches a supported case. Run it read-only first on the ARM64 machine and review its platform paths; do not apply a generated metadata command until it confirms the exact Linux account/organisation directory.

Keep copies of the JSONL files and main.log. If diagnosis prints a repair route, fully quit Desktop and review the dry-run before any apply; if it stays audit-only, leave the state unchanged. The EINVAL fix itself belongs upstream: use runtime fs.constants rather than hard-coded x86 flags.

Hope this helps, if my tools are able to help you, would appreciate a ⭐ :)

BasedGPT · 1 month ago

Hey dhruvkej9,

The second machine is useful because it separates the Desktop app upgrade from the embedded CLI version: EINVAL still appears while the older CLI remains in use, and the transcripts remain resumable. That leaves the Desktop registry write path as the file-layer boundary.

I built a toolkit for this distinction: diagnose.py in BasedGPT/claude-code-session-recovery inventories metadata and transcript files and prints the exact route for orphaned sessions. Run it read-only on the affected Linux machine, review what it finds, and preserve the JSONL files before considering any manual metadata rebuild. The diagnosis should establish whether each missing sidebar entry has a matching transcript; it cannot correct the aarch64 flag constants inside Desktop.

Hope this helps, if my tools are able to help you, would appreciate a ⭐ :)

dhruvkej9 · 1 month ago

@bcherny This is a critical bug, can we expect an fix for this?