Desktop SSH: ccd-cli /tmp/claude-settings-{hash}.json permission collision in multi-user hosts causes exit 1
Summary
When multiple Linux users on the same host connect to that host via Claude Desktop's SSH workspace feature, the second user's ccd-cli daemon exits with code 1 on every initialize. The Desktop UI shows Claude Code process exited with code 1. The first user works fine.
Root cause: ccd-cli writes its settings cache to /tmp/claude-settings-{sha256(settings)[:16]}.json. When two users receive identical --managed-settings/--settings from Desktop (the common case — both {}), the SHA256 hash is identical, so they collide on the same file in the world-shared /tmp/ directory.
Environment
- ccd-cli: 2.1.119 (deployed by Desktop, not user-installed)
- Claude Desktop: 1.4758.0 (macOS)
- Remote host OS: Ubuntu 22.04.5 LTS, x86_64
- Remote claude CLI: 2.1.119 (native installer at
~/.local/share/claude/versions/) - Both users authenticated via
Claude Maxplan, separate accounts
Steps to reproduce
- Have a Linux host with two non-root user accounts (
userA,userB), neither in the other's primary group. - From Claude Desktop, set up two SSH connections — one to
userA@host, one touserB@host. - Connect as
userA, open any project, send a message → works fine. (/tmp/claude-settings-44136fa355b3678a.jsongets created with mode 664, owned byuserA.) - Connect as
userB, open any project, send a message →Claude Code process exited with code 1.
Evidence
The exact stderr from the failing ccd-cli (captured via wrapper)
Error processing settings: EACCES: permission denied, open '/tmp/claude-settings-44136fa355b3678a.json'
Hash is SHA256 of settings content, truncated to 16 hex chars
>>> import hashlib
>>> hashlib.sha256(b'{}').hexdigest()
'44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a'
^^^^^^^^^^^^^^^^
matches the filename in /tmp/
Both users' Desktop spawns ccd-cli with --managed-settings {} (and the second user additionally with --settings {}). Because the actual settings JSON is {} for both, the SHA256 prefix is identical → identical filename → guaranteed collision.
File permission state
$ ls -la /tmp/claude-settings-44136fa355b3678a.json
-rw-rw-r-- 1 userA userA 2 Apr 26 01:52 /tmp/claude-settings-44136fa355b3678a.json
userB is not in userA's group → no write permission. ccd-cli appears to open this file with O_RDWR (likely an atomic write/refresh pattern, even when the desired content is unchanged) → EACCES.
Captured initialize payload that triggers it
{
"request_id": "ai5p9jybpt",
"type": "control_request",
"request": {
"subtype": "initialize",
"hooks": {
"PreToolUse": [
{"matcher": "Edit|Write|MultiEdit|NotebookEdit", "hookCallbackIds": ["hook_0"]},
{"matcher": "mcp__ccd_directory__request_directory", "hookCallbackIds": ["hook_1"]},
{"matcher": "mcp__.*", "hookCallbackIds": ["hook_2"]}
],
"Stop": [{"hookCallbackIds": ["hook_3"]}]
},
"sdkMcpServers": ["Claude in Chrome", "mcp-registry", "ccd_session", "scheduled-tasks"]
}
}
The exit happens regardless of hooks content (verified by feeding ccd-cli minimal initialize payloads with no hooks — also exit 1 if the cache file is unwritable).
Workaround (per-user)
Wrap ccd-cli with a script that redirects TMPDIR to a private directory:
#!/bin/bash
export TMPDIR="$HOME/.claude/tmp"
mkdir -p "$TMPDIR"
exec "$HOME/.claude/remote/ccd-cli/<version>.real" "\$@"
(Rename the real binary to .real and put the wrapper in its place.) This makes ccd-cli write the cache to ~/.claude/tmp/ instead of /tmp/, avoiding the collision permanently.
Caveat: Desktop may overwrite the wrapper on next deployment if it does a content/size check on the binary.
Suggested fix
Move the settings cache out of the world-shared /tmp/ and into a per-user location:
$XDG_CACHE_HOME/claude/(defaulting to~/.cache/claude/), or~/.claude/cache/, or- include
getuid()in the filename:claude-settings-{uid}-{hash}.json
Any of these would eliminate the collision entirely without behavioral change for single-user setups.
Impact
Affects any team / lab environment where multiple developers share a single SSH host (cloud VM, dev server, on-prem workstation). The first user to connect "wins" the cache; everyone else is silently broken until the cache file is manually chmod 666'd or deleted.
Related
- Initially misdiagnosed this as #50698 (hookCallbackId-related daemon exit) due to similar surface symptom, but the actual mechanism is filesystem permission, not callback protocol.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗