Desktop SSH: ccd-cli /tmp/claude-settings-{hash}.json permission collision in multi-user hosts causes exit 1

Resolved 💬 3 comments Opened Apr 26, 2026 by leoncuhk Closed Apr 29, 2026

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 Max plan, separate accounts

Steps to reproduce

  1. Have a Linux host with two non-root user accounts (userA, userB), neither in the other's primary group.
  2. From Claude Desktop, set up two SSH connections — one to userA@host, one to userB@host.
  3. Connect as userA, open any project, send a message → works fine. (/tmp/claude-settings-44136fa355b3678a.json gets created with mode 664, owned by userA.)
  4. 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.

View original on GitHub ↗

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