[BUG] 2.1.154 regression: deep CLAUDE_CODE_TMPDIR overflows macOS UNIX-socket sun_path, breaking bundled playwright-cli daemon (EADDRINUSE)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
Starting in 2.1.154, Claude Code roots the per-uid temp directory under CLAUDE_CODE_TMPDIR and exports that path as TMPDIR into the environment of spawned tool/child processes. When CLAUDE_CODE_TMPDIR is set to a deep path, any child that binds a Unix-domain socket under $TMPDIR constructs a socket path that exceeds the macOS sun_path limit (104 bytes), so bind() fails with EADDRINUSE and leaves orphaned/truncated socket files.
This breaks Claude Code's own bundled @anthropic-ai/playwright-cli: its daemon (playwright ... run-cli-server) places its socket at os.tmpdir()/playwright-cli/<hash>/<session>.sock when PLAYWRIGHT_DAEMON_SOCKETS_DIR is unset, and os.tmpdir() now resolves to the deep CLAUDE_CODE_TMPDIR-derived TMPDIR. On ≤ 2.1.153 the child saw a short TMPDIR (/tmp/claude-<uid>) and the socket bound fine.
In our case this silently broke a Claude Agent SDK harness that sets CLAUDE_CODE_TMPDIR to a per-workspace directory: 12 of 13 parallel browser-scraping jobs failed with repeated EADDRINUSE, with no obvious indication that a temp-dir change was the cause.
What Should Happen?
A deep CLAUDE_CODE_TMPDIR should not break Unix-socket-binding tools. Any of:
- The bundled playwright-cli should set its own short
PLAYWRIGHT_DAEMON_SOCKETS_DIRrather than inheriting a long$TMPDIR. - Claude Code should keep the per-uid temp/socket segment short enough to stay under
sun_path(e.g. hash a long root), or warn when a derived socket path would exceed the OS limit. - At minimum, document that
CLAUDE_CODE_TMPDIRis exported as the childTMPDIRand can break Unix sockets if set to a deep path.
Error Messages/Logs
listen EADDRINUSE: address already in use <CLAUDE_CODE_TMPDIR>/claude-<uid>/playwright-cli/<hash>/<session>.sock
sun_path budget (macOS 104-byte limit), real depth from our harness:
- short (≤2.1.153):
/tmp/claude-501/playwright-cli/<hash>/<session>.sock= 69 bytes → binds - deep (≥2.1.154):
<deep CLAUDE_CODE_TMPDIR>/claude-501/playwright-cli/<hash>/<session>.sock= 134 bytes → exceeds 104 →bind()fails →EADDRINUSE
Steps to Reproduce
Minimal probe showing the TMPDIR propagation change (no browser tooling needed):
export CLAUDE_CODE_TMPDIR=/tmp/deep-probe/aaaaaaaa/bbbbbbbb/cccccccc/dddddddd/eeeeeeee/tmp
mkdir -p "$CLAUDE_CODE_TMPDIR"
claude -p 'Run this exact shell command and report its raw output: printf "TMPDIR=%s\n" "$TMPDIR"'
Observed child $TMPDIR:
- 2.1.153:
TMPDIR=/tmp/claude-501(short — socket binds) - 2.1.154+:
TMPDIR=/tmp/deep-probe/aaaaaaaa/bbbbbbbb/cccccccc/dddddddd/eeeeeeee/tmp/claude-501(deep)
Real-world consequence: with a deep CLAUDE_CODE_TMPDIR, invoking the bundled playwright-cli (which starts a run-cli-server daemon binding a Unix socket under $TMPDIR) fails repeatedly with EADDRINUSE on macOS.
Standalone confirmation of the socket consequence (no Claude Code or playwright needed — just Node, run on macOS). This isolates the OS behavior: a single overlong AF_UNIX path silently binds at a truncated path, so two distinct deep paths sharing the first ~104 bytes (e.g. two daemon launches/retries) collide:
const net = require('net'), fs = require('fs'), os = require('os'), path = require('path');
const deep = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'sunpath-')),
'a'.repeat(60), 'b'.repeat(60));
fs.mkdirSync(deep, { recursive: true });
const p1 = path.join(deep, 'oasb_scraper_AAAAAA.sock'); // 177 bytes
const p2 = path.join(deep, 'oasb_scraper_BBBBBB.sock'); // 177 bytes; differs only after byte ~104
console.log('socket path length:', Buffer.byteLength(p1), 'bytes (macOS sun_path limit = 104)');
net.createServer().listen(p1, () => { // binds OK at the silently-truncated path
console.log('first daemon: listening');
net.createServer().on('error', e => console.log('second daemon:', e.code))
.listen(p2); // EADDRINUSE: truncates to the same path as p1
});
Output on macOS:
socket path length: 177 bytes (macOS sun_path limit = 104)
first daemon: listening
second daemon: EADDRINUSE
Is this a regression?
Yes
Last Working Version
2.1.153
Claude Code Version
2.1.159
Platform
Claude API (OAuth)
Operating System
macOS
Terminal/Shell
zsh
Additional Information
Bisect — each version was fed the same deep CLAUDE_CODE_TMPDIR and printed the child $TMPDIR:
| Version | child $TMPDIR | class |
|---|---|---|
| 2.1.142, .143, .148, .149, .150, .152, .153 | /tmp/claude-501 | short (last good: 2.1.153) |
| 2.1.154, .156, .159 | <deep>/…/tmp/claude-501 | deep (first bad: 2.1.154) |
(2.1.151 and 2.1.155 were never published, so 2.1.154 is the immediate successor of 2.1.153.)
Where the change is: the child-env builder that sets TMPDIR/CLAUDE_CODE_TMPDIR/TMPPREFIX for spawned tools exists in builds as far back as 2.1.142; the 2.1.154 change is in the caller that computes which temp dir gets passed in — it began rooting the per-uid dir under CLAUDE_CODE_TMPDIR instead of the system default. The 2.1.156 changelog entry ("Fixed $TMPDIR resolving to different directories in sandboxed vs unsandboxed Bash commands within the same session") appears to be a follow-on consistency fix on top of the 2.1.154 change, not the origin — there is no changelog entry for the 2.1.154 behavior change itself.
Related: #41817 (path-scoped Unix socket bind in sandbox) describes a similar socket failure mode but is framed as a sandbox feature request; this report is a distinct regression via the documented CLAUDE_CODE_TMPDIR env var, with no sandbox required.
Workaround: set PLAYWRIGHT_DAEMON_SOCKETS_DIR (playwright checks it before os.tmpdir()) — or TMPDIR — to a short path in the affected child's environment.
Environment: Claude Code installed via the native installer under ~/.local/share/claude/versions/ (~/.local/bin/claude → active version); macOS (Darwin 25.x). Verbose --debug logs from the probe and the original EADDRINUSE artifacts from the affected run are available on request.