Sockets directory is still first-come after 2.1.248, and a local account can squat both /tmp/cc-socks and the per-uid fallback

Status Open
Reported on v2.1.248
Maintainer reply None cached
Activity 0 comments · opened Sep 1, 2026

Summary

Version 2.1.248 fixed the symptom reported in #89401. A second account on a shared host now keeps cross-session messaging, because it falls back to /tmp/cc-socks-<uid>. Two problems remain.

  1. The sockets directory is still first-come. /tmp/cc-socks belongs to whichever account starts Claude Code first after each /tmp clear. The directory an account uses therefore changes from boot to boot.
  2. Any unprivileged local account can take both names. The fallback has no fallback, so messaging goes off again.

This report also documents a workaround, because the current one in the error text does not survive case 2.

Environment

  • Claude Code 2.1.252, native install, macOS 25.6.0 (arm64)
  • Two local accounts, uid 501 and uid 502
  • XDG_RUNTIME_DIR unset, CLAUDE_CODE_TMPDIR unset

1. First-come ownership

The primary path keeps the shared name:

var Y = 103;
function Yur(){
  let e = a.XDG_RUNTIME_DIR || ly(),
      t = ue(F(e, "cc-socks", `${process.pid}.sock`));
  if (Buffer.byteLength(t) <= Y) return t;
  return nNn();
}

/tmp is world-writable and sticky. The first account to start Claude Code creates /tmp/cc-socks at mode 0700. That account keeps it. Every other account fails the owner check and takes the per-uid path. macOS clears /tmp, so the winner can change after a reboot.

Peer discovery does not break. Each session writes its absolute socket path into ~/.claude/sessions/<pid>.json:

"pid": "45417",
"messagingSocketPath": "/tmp/claude-501/cc-socks/45417.sock",
"peerProtocol": "1"

Peers read that file, so they never resolve the directory by convention. The cost is diagnostic, not functional. An account cannot predict its own socket directory, and support answers now depend on login order.

The uid suffix costs 4 bytes out of the 103-byte budget. Make cc-socks-<uid> the default. This removes the shared name, the boot-order dependency, and the first squat target below.

2. A local account can squat both names

The refusal path tries the per-uid directory once, then stops:

try { await Re(o) }
catch (m) {
  let k = un(m) ? await XGe() : void 0,
      u = k === void 0 ? void 0 : ue(nNn(k)),
      _ = u === void 0 ? void 0 : F(u, "..");
  if (u === void 0 || _ === void 0 || _ === o) return $e(o, m, De(m));
  n(`[uds-messaging] sockets directory ${o} refused (...); trying the per-uid fallback ${_}`, {level:"warn"});
  try { await Re(_) } catch (f) { return $e(_, f, De(f)) }
  e = u; we("primary_dir_refused_fell_back")
}

nNn() builds the fallback from a fixed uid, so the target name is public and predictable:

function nNn(e = process.getuid?.() ?? 0){
  let t = a.TERMUX_VERSION ? a.PREFIX : void 0,
      i = t ? F(t, "tmp") : "/tmp";
  return F(i, `cc-socks-${e}`, `${process.pid}.sock`);
}

Steps to reproduce

  1. As any local account, and before the target account starts Claude Code, run these two commands. Replace 501 with the target uid.
mkdir -m 700 -p /tmp/cc-socks
mkdir -m 700 -p /tmp/cc-socks-501
  1. As uid 501, start Claude Code.
  2. Read the debug log.

Result

The primary directory fails the owner check. The per-uid directory fails the same check. The code calls $e() and cross-session messaging turns off for that account.

Both directories survive a /tmp clear if the other account recreates them from a login item or a launchd agent.

Scope

This is denial of service only. The owner check and the peerToken file still stop the squatting account from receiving anyone else's messages. The attacker needs a local login on the same host. The realistic targets are shared Macs, build hosts, and CI runners with several service accounts.

Suggested fix

Try a third location before giving up. A directory under the user's own home works, because another account cannot create it. Report the refusal in /status in both cases.

3. Workaround

Set XDG_RUNTIME_DIR to a private directory that you own:

XDG_RUNTIME_DIR=/tmp/claude-501

Claude Code already creates /tmp/claude-<uid> at mode 0700 for its own temp files, and validates it. The directory exists and passes the checks. The resulting socket path is 35 bytes, which is well inside the 103-byte limit.

This also defeats the squat in case 2, because the primary path no longer resolves to /tmp/cc-socks.

Two notes on the workaround:

  • Set it in your shell profile or in user settings. Version 2.1.251 stopped project-level .claude/settings.json from setting CLAUDE_CODE_TMPDIR, TMPDIR, TMP, and TEMP.
  • The fallback ignores both XDG_RUNTIME_DIR and CLAUDE_CODE_TMPDIR, as nNn() above shows. If your own directory is ever refused, the code sends you to /tmp anyway. The error text asks the user to point these variables at a private directory. The fallback should honor the same base.

View original on GitHub ↗