[BUG] 2.1.232 socket-dir hardening silently disables cross-session messaging in user-namespace/chroot envs (ancestors owned by unmapped uid 65534)
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 a65534-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:
- 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/500and below are0700and owned by the user — the actual attack surface is fine). - Provide an explicit override (setting or env var, e.g.
CLAUDE_CODE_SOCKETS_DIR) for hosts where the heuristic cannot work. - At minimum, surface the failure: a warning in
claude doctor/ListAgentsoutput ("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
- Run Claude Code 2.1.232 in an environment where an ancestor of
$XDG_RUNTIME_DIRis owned by neither the user nor root. Easiest faithful repro is a user-namespace chroot (e.g. nix-portable), where/runand/run/userappear as uid 65534; the key condition is juststat -c %u /run/userreturning something other than0or your uid. - Start two interactive sessions.
- Run
/list-agentsin one: the other session does not appear (andss -xl | grep cc-socksshows no socket for either pid). - Same setup on 2.1.226/2.1.228 (the vet does not exist there — the
socket_dir_refusedmarker is absent from those binaries): sockets bind at/run/user/500/cc-socks/<pid>.sockand 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)
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
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_DIRto/mnt/wslg/runtime-dir, and provides that directory on tmpfs with mode0777. It belongs to the user, but every user can write to it and it has no sticky bit. The check refuses it, socc-socksis never created and the session binds no inbox socket.Debug log:
The string
private-or-stickyis 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
$XDG_RUNTIME_DIRis/mnt/wslg/runtime-dirat mode777./list-agentsin one. The other session is missing, and/statusshows noPeer addressrow.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_DIRat a private directory you own:Restart the sessions. Each then binds
.../run/cc-socks/<pid>.sockwith mode0600, and/list-agentslists the peers.Caveat: this also changes
XDG_RUNTIME_DIRfor 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/tmpis1777 root:root, so it passes the same check.Strong agreement with proposal 3. Nothing surfaces the failure:
/statusomits thePeer addressrow, and/list-agentsreportsNo 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
Reproduced on Linux, and pinned to 2.1.232.
Verified with released builds in a fresh config dir:
unshare -Ur): cross-session messaging is silently disabled. The only trace is a--debug-fileline: "Failed to set up sockets directory … (refusing to bind — cross-session messaging is OFF for this session)".The hardening is working as designed against directories genuinely owned by another user, but in uid-mapped environments it misfires: ancestor directories like
/and/homeappear owned by "nobody" (65534), so the check always fails — and the workaround suggested in the debug message (pointingXDG_RUNTIME_DIR/CLAUDE_CODE_TMPDIRat 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
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:
unshare -U, nix-portable style chroots): the host's/,/run,/run/user,/tmpare 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.XDG_RUNTIME_DIRpoints 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
/tmpinstead of giving up, which covers the WSLg default without touchingXDG_RUNTIME_DIR; (3) if messaging still can't be set up, you now get a visible notice at startup and aPeer address: unavailable — <reason>row in/statusinstead of a debug-only log line.I'll follow up here with the version that ships it. Until then, on WSLg the
XDG_RUNTIME_DIRoverride @sitawit described works; on user-namespace hosts there is unfortunately no clean workaround on 2.1.232–2.1.235.