Problem
There is no first-class way to ask "what agent sessions are live, and which one am I?" — pid, socket path, session id, status. Today that gets hand-rolled with ps/env incantations at the call site, and the obvious commands are wrong in non-obvious ways (see Traps).
All the data already exists on disk for Claude sessions. This is a read-only formatting command, not new machinery.
Why now
This is the lookup both phases of #667 need:
- Phase 1 (liveness) — read
status / waitingFor per session instead of scraping panes.
- Phase 2 (delivery) — resolve a crew to its
messagingSocketPath before posting an NDJSON frame.
Right now that resolution is ad-hoc and re-derived by hand every time.
Recommended scoping: generic command + thin seam
Per CLAUDE.md — "does this work for non-Claude agents too?" — as scoped this is Claude-only, so it needs a migration path from the start.
Recommendation: ship a generic squadrant sessions backed by one introspection method on the existing AgentDriver seam, with Claude as the first and only implementation. codex and opencode return "unsupported" until someone implements them.
This is deliberately not a speculative abstraction (Karpathy principle 2). The returned shape — id, pid, cwd, status, address — is one squadrant already models today in its task and liveness records. The seam names a boundary where we know agents diverge, rather than inventing one for hypothetical future needs. The implementation behind it stays a thin file reader.
The alternative — a permanent squadrant claude sessions top-level verb — was considered and is not recommended. It adds Claude-only public surface with no migration path, which is exactly what the multi-agent direction statement warns against, and it is harder to walk back once shipped. We already know how that story ends: the v0.13.3 Claude-only delivery fix never got a cross-agent path and became #514/#657.
Proposed surface
squadrant sessions [--json] [--project <name>] [--live-only]
squadrant whoami [--json]
sessions — the table, with a derived health column reconciling three independent signals (pid alive? socket present? status fresh?). They can disagree; the command should say so rather than silently pick one.
whoami — resolve the calling session via $CLAUDE_CODE_MESSAGING_SOCKET, falling back to a parent-tree walk. Must not use pgrep (Trap 1).
--json is the one that matters for the daemon; the table is for humans.
Available data
Verified against Claude Code 2.1.227, macOS. One file per live session at ~/.claude/sessions/<pid>.json:
| field |
example |
note |
pid |
20523 |
also the filename, also the socket name |
sessionId |
09ba6f18-… |
the stable key — survives process restart |
messagingSocketPath |
/tmp/cc-socks/20523.sock |
the peer inbox |
name |
squadrant-59 |
mutable; auto-derived unless set |
nameSource |
derived |
absent once renamed |
status |
idle | busy | shell | waiting |
self-reported by the agent |
waitingFor |
permission prompt |
only when status: waiting |
cwd |
/Users/…/me/squadrant |
how you map a session → project |
kind |
interactive | bg | daemon | daemon-worker |
|
entrypoint |
cli | sdk-cli | … |
|
version |
2.1.227 |
gate capability checks on this |
peerProtocol |
1 |
envelope version |
procStart |
Tue Aug 11 05:53:51 2026 |
pins pid → process instance |
startedAt / updatedAt / statusUpdatedAt |
epoch ms |
|
Two fields not in the registry that must be derived:
- uid —
stat -f %u <socket> (macOS) or ps -o uid= -p <pid>. Always the current user in practice since the socket dir is 0700, but worth surfacing.
- socket mode —
stat -f %OLp <socket>. Expect 600. Surfacing it makes a misconfigured umask visible instead of silent.
Traps — the reason a shared command is worth having
These were each found the hard way. They are the test matrix.
pgrep omits the session you're calling from. Measured: pgrep -x claude returned 8 pids while the registry held 9 — the missing one was the calling session itself. macOS pgrep skips the invoking process and its ancestors, and your session's claude is an ancestor of your shell. Anything using pgrep to identify self is silently wrong.
pgrep -fl is unusable here — squadrant passes a multi-KB --settings JSON blob on the command line, so -f output is unreadable. -x stays clean.
- pid is not stable;
sessionId is. One observed session moved pid 47716/squadrant-bb → 20523/squadrant-59 across a version upgrade, same sessionId. Any cache keyed on pid or name goes stale across a restart or upgrade.
- A crashed session leaves a fresh-looking file. It can't unlink on SIGKILL, and its last
status is frozen forever. kill(pid, 0) is mandatory; statusUpdatedAt alone is not enough.
sdk-cli sessions have no status field at all. Absence must render as unknown, never idle — otherwise we report an unknown session as ready for work.
$CMUX_CLAUDE_PID is cmux-only. $CLAUDE_CODE_MESSAGING_SOCKET is set by Claude Code itself and is the portable self-identifier (basename … .sock). Fully generic fallback is a walk up the process tree from $$ until a claude comm matches.
Prior art — do not rewrite
packages/agents already owns the per-agent driver seam.
- A reference reader is already written and tested:
readRegistry() in squadrant-hub/spokes/squadrant/findings/2026-08-08-cc-peer-messaging-snippets.js (the peers and watch commands). It already does liveness reconciliation and stale handling, and it runs correctly against live sessions on 2.1.227. Lift it rather than rewriting.
Scope
Read-only. No delivery behaviour, no daemon changes, no state-machine changes. Landing this before #667 Phase 1 means the LifecycleSource work gets a tested reader instead of growing its own.
Related: #667 (peer-messaging transport), #618, #567.
Source: research side-session, 2026-08-11 — spokes/squadrant/side-handoffs/claude-session-introspection-cli.md.
Problem
There is no first-class way to ask "what agent sessions are live, and which one am I?" — pid, socket path, session id, status. Today that gets hand-rolled with
ps/envincantations at the call site, and the obvious commands are wrong in non-obvious ways (see Traps).All the data already exists on disk for Claude sessions. This is a read-only formatting command, not new machinery.
Why now
This is the lookup both phases of #667 need:
status/waitingForper session instead of scraping panes.messagingSocketPathbefore posting an NDJSON frame.Right now that resolution is ad-hoc and re-derived by hand every time.
Recommended scoping: generic command + thin seam
Per
CLAUDE.md— "does this work for non-Claude agents too?" — as scoped this is Claude-only, so it needs a migration path from the start.Recommendation: ship a generic
squadrant sessionsbacked by one introspection method on the existingAgentDriverseam, with Claude as the first and only implementation. codex and opencode return "unsupported" until someone implements them.This is deliberately not a speculative abstraction (Karpathy principle 2). The returned shape —
id,pid,cwd,status,address— is one squadrant already models today in its task and liveness records. The seam names a boundary where we know agents diverge, rather than inventing one for hypothetical future needs. The implementation behind it stays a thin file reader.The alternative — a permanent
squadrant claude sessionstop-level verb — was considered and is not recommended. It adds Claude-only public surface with no migration path, which is exactly what the multi-agent direction statement warns against, and it is harder to walk back once shipped. We already know how that story ends: the v0.13.3 Claude-only delivery fix never got a cross-agent path and became #514/#657.Proposed surface
sessions— the table, with a derivedhealthcolumn reconciling three independent signals (pid alive? socket present? status fresh?). They can disagree; the command should say so rather than silently pick one.whoami— resolve the calling session via$CLAUDE_CODE_MESSAGING_SOCKET, falling back to a parent-tree walk. Must not usepgrep(Trap 1).--jsonis the one that matters for the daemon; the table is for humans.Available data
Verified against Claude Code 2.1.227, macOS. One file per live session at
~/.claude/sessions/<pid>.json:pid20523sessionId09ba6f18-…messagingSocketPath/tmp/cc-socks/20523.socknamesquadrant-59nameSourcederivedstatusidle | busy | shell | waitingwaitingForpermission promptstatus: waitingcwd/Users/…/me/squadrantkindinteractive | bg | daemon | daemon-workerentrypointcli | sdk-cli | …version2.1.227peerProtocol1procStartTue Aug 11 05:53:51 2026startedAt/updatedAt/statusUpdatedAtTwo fields not in the registry that must be derived:
stat -f %u <socket>(macOS) orps -o uid= -p <pid>. Always the current user in practice since the socket dir is0700, but worth surfacing.stat -f %OLp <socket>. Expect600. Surfacing it makes a misconfigured umask visible instead of silent.Traps — the reason a shared command is worth having
These were each found the hard way. They are the test matrix.
pgrepomits the session you're calling from. Measured:pgrep -x claudereturned 8 pids while the registry held 9 — the missing one was the calling session itself. macOSpgrepskips the invoking process and its ancestors, and your session'sclaudeis an ancestor of your shell. Anything usingpgrepto identify self is silently wrong.pgrep -flis unusable here — squadrant passes a multi-KB--settingsJSON blob on the command line, so-foutput is unreadable.-xstays clean.sessionIdis. One observed session moved pid47716/squadrant-bb→20523/squadrant-59across a version upgrade, samesessionId. Any cache keyed on pid or name goes stale across a restart or upgrade.statusis frozen forever.kill(pid, 0)is mandatory;statusUpdatedAtalone is not enough.sdk-clisessions have nostatusfield at all. Absence must render asunknown, neveridle— otherwise we report an unknown session as ready for work.$CMUX_CLAUDE_PIDis cmux-only.$CLAUDE_CODE_MESSAGING_SOCKETis set by Claude Code itself and is the portable self-identifier (basename … .sock). Fully generic fallback is a walk up the process tree from$$until aclaudecomm matches.Prior art — do not rewrite
packages/agentsalready owns the per-agent driver seam.readRegistry()insquadrant-hub/spokes/squadrant/findings/2026-08-08-cc-peer-messaging-snippets.js(thepeersandwatchcommands). It already does liveness reconciliation and stale handling, and it runs correctly against live sessions on 2.1.227. Lift it rather than rewriting.Scope
Read-only. No delivery behaviour, no daemon changes, no state-machine changes. Landing this before #667 Phase 1 means the
LifecycleSourcework gets a tested reader instead of growing its own.Related: #667 (peer-messaging transport), #618, #567.
Source: research side-session, 2026-08-11 —
spokes/squadrant/side-handoffs/claude-session-introspection-cli.md.