[BUG] Registry sweep deletes other hosts' live session records when hosts share $HOME and /etc/machine-id (HPC clusters, cloned images)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
On a cluster where compute nodes share a network $HOME and were provisioned from one image, Claude Code on one node deletes the ~/.claude/sessions/<pid>.json records of sessions that are alive on other nodes. Those sessions then drop out of ListAgents and can no longer be reached with SendMessage, and they lose their names. The record is not written back while the process keeps running.
The cause is how the registry scopes pids. pidDomain is linux:<machine-id>:<pid namespace>, and the sweep deletes a record when the pid does not exist on the sweeping host and the record's pidDomain equals the sweeper's. Both halves are the same on every node here:
/etc/machine-idis identical across nodes. I checked two of them and both hold the same value, which is common for HPC images, VM templates and cloned cloud images.- The pid namespace is the root namespace,
pid:[4026531836]. That is the kernel's fixed inode for the initial pid namespace (PROC_PID_INIT_INO), so it is the same on every Linux host and never tells hosts apart.
So every node considers every other node's pids "local", finds them dead, and removes the records, including the peer-auth .key files alongside them.
What I observed: six interactive sessions were running in tmux on one node. Five of them, started while Claude was also running on another node, had no registry record about 30 hours later, although all five processes were alive and each still had its messaging socket in /tmp/cc-socks/. The one session started after the other node's Claude processes had exited kept its record. The five records created after that point have survived since.
What Should Happen?
A host should only sweep records it can prove are its own dead processes. A record written by a live process on another host must never be deleted.
Error Messages/Logs
There is no error; the records just disappear. With --debug the sweep logs its domain in this form (format taken from the bundle). The value is identical on every node I checked:
[DEBUG] [sessionRegistry] sweep permitted (domain linux:<MACHINE_ID>:pid:[4026531836])
Steps to Reproduce
On a single Linux host this shows the sweep rule. On a real cluster, the "fake" record is simply another node's live session.
D=~/.claude/sessions
DOM="linux:$(cat /etc/machine-id):$(readlink /proc/self/ns/pid)"
mk() { # <pid> <pidDomain fragment or empty>
printf '{"pid":%s,"sessionId":"00000000-0000-4000-8000-%012d","cwd":"/tmp/probe","startedAt":%s000,"procStart":"1","kind":"interactive","entrypoint":"cli",%s"messagingSocketPath":"/tmp/cc-socks/%s.sock","name":"probe-%s","nameSource":"user","status":"idle"}\n' \
"$1" "$1" "$(date +%s)" "$2" "$1" "$1" > "$D/$1.json"
}
# pids that do not exist on this host (kill -0 must say "No such process")
mk 4190001 "\"pidDomain\":\"$DOM\"," # same domain as this host
mk 4190002 '"pidDomain":"linux:ffffffffffffffffffffffffffffffff:pid:[4026531836]",' # a different machine-id
mk 4190003 "" # no pidDomain
Then, in any running interactive session on that host, ask it to list peer sessions (the ListAgents tool), and check:
for p in 4190001 4190002 4190003; do [ -e ~/.claude/sessions/$p.json ] && echo "$p kept" || echo "$p DELETED"; done
Result on 2.1.294, the build of the session that ran the sweep:
4190001 DELETED
4190002 kept
4190003 DELETED
Records for live local pids were untouched. I have not run the two-host version deliberately, since it destroys live records. Given the rule above, starting sessions on two hosts that share $HOME and /etc/machine-id and then listing peers from one of them should remove the other host's records.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.296 (Claude Code); the repro sweep ran on 2.1.294, and both builds contain the rule
Platform
Anthropic API
Operating System
Other Linux
Terminal/Shell
Other (tmux)
Additional Information
The rule, paraphrased from the 2.1.296 bundle (function names are the minified ones; 2.1.294 contains the same h1n check):
// pidDomain
return `linux:${machineId}:${await readlink("/proc/self/ns/pid")}`
// Nm(pid): true when the pid does not exist on THIS host
try { process.kill(pid, 0); return false } catch (e) { return code(e) === "ESRCH" }
// h1n(record, ownDomain): is the record in this host's domain?
if (record.pidDomain !== undefined) return record.pidDomain === ownDomain;
// ...on Linux, a record with no pidDomain falls through to `return true`
// sweep (pOt) and peer listing (_Q, zio) then unlink <pid>.json and <pid>.<hash>.key
The bundle already has a host-aware identifier: the OAuth refresh lock records pidSpace = hostname + "#" + readlink("/proc/self/ns/pid") beside pidDomain. The session registry uses pidDomain alone.
Suggested fix, any one of these would do:
- Include the hostname, or
/proc/sys/kernel/random/boot_id, inpidDomain(or requirepidSpaceto match as well) before a record can be swept. - Record the writer's hostname in each session record, and never delete a record written by another host.
Note that the boot id is unique per boot even on cloned images, unlike the machine-id. Using a hash of it would also address #98201, which is about the raw machine-id showing up in debug logs.
Workaround: none that I found from user space. /etc/machine-id is root-owned, and running each session in its own pid namespace changes too much else.
Related:
- #98201 shows the same domain string in its debug-log excerpt.
- #95807 also has peers losing a session's name, but after a restart and for a different reason.
- #69364 is about duplicate resumes, which records being swept across hosts makes harder to detect.