[BUG] 2.1.232 socket-dir hardening silently disables cross-session messaging in user-namespace/chroot envs (ancestors owned by unmapped uid 65534)

Status Fixed / completed
Reported on v2.1.232
Maintainer reply ✓ Yes — bcherny
Activity 5 comments · opened Aug 14, 2026 · closed Aug 27, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet (closest are #85412, #85497, #84945, #85886 — all different causes; this one is the new socket-directory ownership vet)
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code (2.1.232)

What's Wrong?

Since 2.1.232, sessions on my Linux dev host never bind their cross-session messaging socket, so they are invisible to /list-agents / ListAgents in other sessions and unreachable by SendMessage. There is no visible error; the only trace is a debug-level log ("cross-session messaging is OFF for this session").

The cause is the socket-directory hardening added in 2.1.232 ("Hardened the auto-generated cross-session messaging socket directory..."). Before binding $XDG_RUNTIME_DIR/cc-socks/<pid>.sock, the CLI now walks every ancestor of the sockets directory and requires each component to be owned by the current user or root (sticky/world-writable rules are checked only after ownership passes). On hosts where Claude runs inside a user-namespace chroot (nix-portable style: / is a per-process chroot under $HOME/tmp/nix-chroot.XXXXXX, with host dirs bind-mounted in), the host system directories appear owned by the unmapped uid 65534 (nobody):

$ ls -ldn / /run /run/user /run/user/500 /run/user/500/cc-socks /home1 /tmp
drwx------ 20   500   500 4096 ... /
drwxr-xr-x 33 65534 65534 1040 ... /run
drwxr-xr-x  5 65534 65534  100 ... /run/user
drwx------  4   500   500  100 ... /run/user/500
drwx------  2   500   500  160 ... /run/user/500/cc-socks
drwxr-xr-x 10 65534 65534  163 ... /home1
drwxrwxrwt 38 65534 65534 ...    /tmp

So the vet always throws directory_rule / socket_dir_refused and messaging is disabled. This environment is a catch-22:

  • Every shared location (/run/user/500, $HOME/..., /tmp) has a 65534-owned ancestor, so the vet refuses it.
  • Every location that passes the vet is inside the per-process chroot, which other sessions cannot see, so it would be useless for cross-session messaging anyway.

I could not find any env var or setting to override the sockets directory or the vet. Sessions still running 2.1.226 on the same host bind /run/user/500/cc-socks/<pid>.sock fine and remain visible; the failure is one-way (a 2.1.232 session still lists the 2.1.226 peers, so from the user's perspective the asymmetry is very confusing).

What Should Happen?

Cross-session messaging should keep working (or at least fail loudly) in user-namespace/chroot environments where system ancestors map to the unmapped uid. Some options:

  1. Relax the ancestor ownership rule: accept ancestors owned by an unmapped uid (65534) when they are not writable by others, or only enforce strict ownership from the deepest user-owned component down (here /run/user/500 and below are 0700 and owned by the user — the actual attack surface is fine).
  2. Provide an explicit override (setting or env var, e.g. CLAUDE_CODE_SOCKETS_DIR) for hosts where the heuristic cannot work.
  3. At minimum, surface the failure: a warning in claude doctor / ListAgents output ("cross-session messaging disabled: <reason>") instead of a debug-only log line. Diagnosing this required extracting the bundled JS from the binary.

Error Messages/Logs

No user-visible error. The internal log message (from the bundled source) is:

[uds-messaging] Failed to set up sockets directory <dir> (refusing to bind — cross-session messaging is OFF for this session): ...
a sockets-directory component is not a private-or-sticky directory owned by us or root — refusing to use it

Observed state: the session's ~/.claude/sessions/<pid>.json presence file is written correctly (name, tmux pane, status) but has no messagingSocketPath, and /proc/net/unix shows listening cc-socks sockets only for the 2.1.226 sessions.

Steps to Reproduce

  1. Run Claude Code 2.1.232 in an environment where an ancestor of $XDG_RUNTIME_DIR is owned by neither the user nor root. Easiest faithful repro is a user-namespace chroot (e.g. nix-portable), where /run and /run/user appear as uid 65534; the key condition is just stat -c %u /run/user returning something other than 0 or your uid.
  2. Start two interactive sessions.
  3. Run /list-agents in one: the other session does not appear (and ss -xl | grep cc-socks shows no socket for either pid).
  4. Same setup on 2.1.226/2.1.228 (the vet does not exist there — the socket_dir_refused marker is absent from those binaries): sockets bind at /run/user/500/cc-socks/<pid>.sock and both sessions see each other.

Is this a regression?

Yes, this worked in a previous version

Last Working Version

2.1.226 (confirmed working on the same host; 2.1.228 does not contain the vet either)

Claude Code Version

2.1.232 (installed via nix)

Platform

Anthropic API

Operating System

Other Linux (kernel 4.19.62, shared dev server, sessions run under tmux)

View original on GitHub ↗

3 Comments

sitawit · 15 days ago

Same regression, different environment: WSL 2 with WSLg. Here the check fails on the permission rule rather than the ownership rule.

Cause

WSLg sets XDG_RUNTIME_DIR to /mnt/wslg/runtime-dir, and provides that directory on tmpfs with mode 0777. It belongs to the user, but every user can write to it and it has no sticky bit. The check refuses it, so cc-socks is never created and the session binds no inbox socket.

$ stat -c '%a %U:%G' /mnt/wslg/runtime-dir
777 <user>:<user>

Debug log:

[ERROR] [uds-messaging] Failed to set up sockets directory /mnt/wslg/runtime-dir/cc-socks
(refusing to bind — cross-session messaging is OFF for this session): a sockets-directory
component is not a private-or-sticky directory owned by us or root — refusing to use it.
A directory on the sockets path is shared (world- or group-writable without the sticky bit,
e.g. a container volume mounted at /tmp) or not owned by you or root. Point XDG_RUNTIME_DIR
or CLAUDE_CODE_TMPDIR at a private (0700) directory you own to use a different location.

The string private-or-sticky is absent from the 2.1.231 binary and present in 2.1.232 and 2.1.233. Messaging worked on this machine until 2.1.232 installed. Observed on 2.1.233.

Steps to reproduce

  1. Use a default WSL 2 install with WSLg, where $XDG_RUNTIME_DIR is /mnt/wslg/runtime-dir at mode 777.
  2. Start two interactive sessions in two terminals.
  3. Run /list-agents in one. The other session is missing, and /status shows no Peer address row.

Two differences from the original report

1. This is a platform default. No chroot, no unusual setup. The permissions come from WSLg, not from user configuration, so this likely affects any WSL 2 install with WSLg.

2. A workaround exists here, unlike the catch-22 in the original report. Point XDG_RUNTIME_DIR at a private directory you own:

mkdir -m 700 -p "$HOME/.claude/run"
// ~/.claude/settings.json — absolute path required
{
  "env": {
    "XDG_RUNTIME_DIR": "/home/<user>/.claude/run"
  }
}

Restart the sessions. Each then binds .../run/cc-socks/<pid>.sock with mode 0600, and /list-agents lists the peers.

Caveat: this also changes XDG_RUNTIME_DIR for hooks and MCP servers, which then no longer find the WSLg sockets for Wayland and PulseAudio.

Suggested fix

Proposal 1 in the original report does not cover this case: the directory is user-owned and fails the permission rule, not the ownership rule.

A fallback to /tmp/cc-socks-<uid>/ covers both cases. The code already builds that path when the primary path is too long, and /tmp is 1777 root:root, so it passes the same check.

Strong agreement with proposal 3. Nothing surfaces the failure: /status omits the Peer address row, and /list-agents reports No reachable agents, which suggests no other session is running. Only the debug log gives the reason.

Is anyone else on WSL 2 seeing this?

Environment

  • Claude Code 2.1.233 (check also present in 2.1.232); last working version 2.1.231
  • WSL 2 with WSLg, Windows Terminal
  • Anthropic API
bcherny collaborator · 13 days ago

Reproduced on Linux, and pinned to 2.1.232.

Verified with released builds in a fresh config dir:

  • 2.1.233, normal environment: the cross-session messaging socket is created and messaging works.
  • 2.1.233 with the auto socket directory owned by another uid, or inside a single-user user namespace (unshare -Ur): cross-session messaging is silently disabled. The only trace is a --debug-file line: "Failed to set up sockets directory … (refusing to bind — cross-session messaging is OFF for this session)".
  • 2.1.231 binds and listens in the identical environments; 2.1.232 is where the refusal starts, matching its changelog entry about hardening the messaging socket directory on shared /tmp.

The hardening is working as designed against directories genuinely owned by another user, but in uid-mapped environments it misfires: ancestor directories like / and /home appear owned by "nobody" (65534), so the check always fails — and the workaround suggested in the debug message (pointing XDG_RUNTIME_DIR/CLAUDE_CODE_TMPDIR at a private directory you own) can't ever pass there. We're tracking a fix to make this environment work again and to surface the condition instead of failing silently.

🤖 Generated with Claude Code

dhollman contributor · 12 days ago

Thanks for the precise reports — both environments are covered by a fix that is now up for review.

What was going wrong, in short: since 2.1.232 the messaging socket directory is only used after every directory above it checks out as owned by you or root and not writable by others. Two setups can never satisfy that as written:

  • User-namespace / uid-mapped hosts (rootless containers, unshare -U, nix-portable style chroots): the host's /, /run, /run/user, /tmp are reported as uid 65534 because their real owner isn't mapped into the namespace, so every candidate directory was refused — and the workaround the debug message suggested couldn't pass either.
  • WSL2 with WSLg: XDG_RUNTIME_DIR points at /mnt/wslg/runtime-dir, which is yours but world-writable without the sticky bit, so it was refused too.

The change does three things: (1) inside a user namespace, ancestor directories owned by the kernel's overflow uid are treated like root-owned ones (they still must not be writable by others unless sticky) — nobody inside the namespace can act as that owner; the socket directory itself must still be yours; (2) when the runtime directory can't be used, Claude Code falls back to a private per-user directory under /tmp instead of giving up, which covers the WSLg default without touching XDG_RUNTIME_DIR; (3) if messaging still can't be set up, you now get a visible notice at startup and a Peer address: unavailable — <reason> row in /status instead of a debug-only log line.

I'll follow up here with the version that ships it. Until then, on WSLg the XDG_RUNTIME_DIR override @sitawit described works; on user-namespace hosts there is unfortunately no clean workaround on 2.1.232–2.1.235.

Showing cached comments. Read the full discussion on GitHub ↗