Repository navigation
Native cross-session peer messaging between opencode-skein and Claude Code #81
Description
Proposal
Status: unblocked — claude-peer-protocol-spike reported GO on 2026-09-05
The load-bearing spike (a fake, disposable peer registered against a real, unmodified Claude Code
installation) passed outright: ListAgents listed it, SendMessage delivered real, decodable
wire frames to it. Full protocol details — registry schema, key-file hashing, socket path/perms,
NDJSON frame shapes, the attribution envelope, the return-address mechanism — are verified,
not assumed, in claude-peer-protocol-spike/findings.md. Read it before touching this change;
it is the source of truth this proposal is built on.
Why
opencode-skein sessions are invisible to Claude Code's own ListAgents, and Claude sessions are
invisible to opencode-skein's peers/send_peer_message (peer-messaging) unless someone opens
two terminals and manually relays. The spike proved the fix does not require Claude to install
anything, run an MCP server, or run a bridge daemon: opencode-skein can become a real peer on
Claude's own local mechanism.
What Changes
1. Process identity: a per-session sidecar (spike's Option B, not Option A)
Real Claude Code registers exactly one PID per session (confirmed). opencode-skein's own
architecture does not guarantee that — one opencode serve process can host several
independently-active sessions across directories (fleet-instance-presence,
session-peer-awareness). Collapsing all of a server's sessions into one Claude-visible peer
would misrepresent them. Each opencode-skein session that opts into Claude visibility gets its
own small, genuinely separate sidecar process — spawned when the session starts (or lazily on
first use of the peer tools), registering under its own real PID, and proxying frames to/from the
real opencode session over the opencode HTTP API it already exposes (the same mechanism
peer-messaging uses internally). Killed and unregistered when the session ends or the server
shuts down; a crash-recovery pass removes any sidecar registration whose PID is no longer running,
on the same principle real Claude Code's own registry must tolerate (per the spike's confirmed
"reachability is actively probed, not registry-trusted" finding).
2. Compatibility boundary — an adapter, not a rewrite
A peer/claude/ module owns everything Claude-protocol-shaped: registry read/write, key-file
hashing (sha256(messagingSocketPath), confirmed), the UDS client and server, the NDJSON codec,
and attribution-envelope construction/parsing. The rest of opencode-skein — specifically
peer-messaging's Peer/PeerMessage model and send_peer_message tool — gains
"claude-code" as a second reachable harness through this adapter. No new tool, no parallel
list_agents/send_message pair.
3. Outbound: opencode-skein → Claude
Resolve a Claude peer from its registry entry (read-only, same-user file permissions — confirmed
sufficient, no additional secret exchange needed), probe reachability the same way ListAgents
does (connect-and-close) before claiming a target is available, then send the real two-frame
shape: {"type":"auth","token":"<their peerToken>"} then the message frame with a fresh msg_id,
priority, from set to the sidecar's own socket URI, and message.content wrapping the text in
the confirmed <cross-session-message from="..." from-name="..." from-mode="..."> envelope —
with the message text sanitized first: a literal </cross-session-message> or a
from="..."-shaped substring in the payload must not be able to forge envelope structure or
sender identity, a risk the spike's findings flagged but did not need to solve (Claude's own
sender controls both ends of what it sends; opencode-skein's sender does not get that luxury when
the text originates from model output).
4. Inbound: Claude → opencode-skein
The sidecar's socket server validates the presented auth token against its own key file, decodes
the message frame, strips and parses the attribution envelope, and — critically — never treats
the parsed from/from-name as ground truth beyond "which authenticated socket connection sent
this": envelope attributes are for display/context, transport identity (the connection that
presented a valid token) is what provenance is actually keyed on. The resulting message is
injected into the real opencode session the same way peer-messaging injects same-machine peer
messages — same provenance discipline, same "this is agent context, not a permission grant"
posture, extended to a message that happens to have arrived from a different harness.
5. Priority and busy/idle
priority (now/next/later) is decoded and preserved but affects scheduling only, never
authorization — restated explicitly because it is the one field in this whole protocol capable
of being misread as an escalation mechanism. A session mid-turn is not interrupted; a message
arriving then is queued the same way peer-messaging already refuses same-directory delivery to
a literally busy target rather than risking corruption — Claude-sourced messages get the identical
treatment, not a more permissive one.
6. Diagnostics
Extend whatever peer-messaging already exposes (or add alongside it) with Claude-specific
detail on request: sidecar PID/socket/registration state per session, last reachability probe
result, and the specific reasons findings.md's error catalogue calls for ("peer protocol version
2 is unsupported", "registered but socket unreachable", "runtime directory has insecure
permissions", etc.) rather than a generic "send failed".
Conformance and security testing
Per the original request's detailed scope, still binding: unit tests for registry parsing/writing,
the NDJSON codec, key-file hashing, envelope construction and sanitization, name/PID resolution;
a manual/optional conformance suite runnable against a real stock Claude Code install (recording
the tested version, mirroring how findings.md records 2.1.261); security tests for a forged
token, a stale/reused PID, a procStart mismatch, a world-writable runtime directory, a symlinked
socket or registry path, an oversized frame, malformed NDJSON, and an attempted envelope-injection
message. None of this is optional polish — the spike's findings make clear the entire security
model rests on filesystem permissions and live reachability checks, both of which this
implementation must uphold at least as strictly as real Claude Code does.
Non-Goals
- Everything the original request explicitly excluded: remote/cross-machine federation, cloud
ChatGPT, MCP messaging, A2A, a generic distributed agent protocol, persistent message history
service, team/task orchestration, shared memory, file transfer/attachments, broadcast channels,
a central broker, llama-skein integration, specsync integration. - Not a mandatory daemon — matches the spike's constraint.
- Not replacing
fleet-instance-presence/agent-coordination-busfor opencode-to-opencode
presence unless the spike's Slice 4 findings and a follow-up reconciliation decision say so.
Dependencies
claude-peer-protocol-spike— satisfied; findings and a GO recommendation exist.peer-messaging— satisfied; shipped and live-verified (same-directorysend_peer_message
reachable and delivering). This change extends its domain model, not a placeholder.
Impact
- New:
packages/opencode/src/peer/claude/registry.ts(read/write registry JSON + key file),
socket.ts(UDS client + server),codec.ts(NDJSON frame encode/decode + envelope
construct/parse/sanitize),auth.ts(token read/present/verify),sidecar.ts(per-session
sidecar process entry point and lifecycle),packages/opencode/src/peer/adapter.ts
("claude-code"harness onpeer-messaging'sPeer/PeerMessagemodel). - Modified:
packages/opencode/src/session/peers.ts(harnessfield onPeer, now genuinely
used),packages/opencode/src/tool/send-peer-message.ts(resolve a Claude-Code target through
the adapter), session lifecycle hooks (spawn/kill the sidecar with the session). - New tests:
packages/opencode/test/peer/claude/*(unit) plus an optional, manually-run
conformance suite against a real stock Claude Code install.
Disposition (2026-09-18)
Archived — superseded/obsolete. Superseded by claude-code-peer-source (#84) and claude-peer-messaging (#85), which shipped the sidecar peer end to end.
Tasks
Every field/path/permission cited below is confirmed in
openspec/changes/claude-peer-protocol-spike/findings.md, not assumed — read it first.
Slice 1: Codec and registry (pure, no process/socket yet)
- 1.1
packages/opencode/src/peer/claude/codec.ts: encode/decode the two confirmed NDJSON
frame shapes ({"type":"auth","token":...}and the message frame withmsgV,msg_id,
type,message.content,priority,from). Reject malformed/oversized frames cleanly.- Validation: unit tests — round-trip both frame shapes; malformed JSON rejected; oversized
frame rejected
- Validation: unit tests — round-trip both frame shapes; malformed JSON rejected; oversized
- 1.2
codec.ts: attribution envelope construct (<cross-session-message from="..." from-name="..." from-mode="...">...</cross-session-message>) and parse. Construction
SHALL sanitize message text so it cannot forge a closing tag or afrom="..."-shaped
attribute; parsing SHALL treat envelope attributes as display context only, never as
authoritative sender identity.- Validation: unit test — a message containing a literal
</cross-session-message>or
from="forged"is rendered inert in the constructed envelope
- Validation: unit test — a message containing a literal
- 1.3
packages/opencode/src/peer/claude/registry.ts: read a registry entry
(<dir>/sessions/<pid>.json) and its key file (<dir>/sessions/<pid>.<sha256( messagingSocketPath)>.key), matching the exact schema infindings.md. Read-only for now.- Validation: unit tests against fixture files matching the confirmed schema
- 1.4
registry.ts: write a registry entry + key file for a given PID/socket path, with the
confirmed permissions (dir0700, registry/key files0600) andprocStartcomputed as
TZ=UTC LC_ALL=C ps -o lstart= -p <pid>(confirmed format). Refuse to write if the target
runtime directory has insecure ownership/mode — fail closed with a specific diagnostic.- Validation: unit test — written file permissions match; a world-writable parent directory is
refused with a named error, not silently accepted
- Validation: unit test — written file permissions match; a world-writable parent directory is
Slice 2: Socket client and server
- 2.1
packages/opencode/src/peer/claude/socket.ts: UDS client — connect, send the
confirmed auth-then-message frame pair, handle connection-refused (target unreachable) and
timeout distinctly.- Validation: unit/integration test against a local test listener
- 2.2
socket.ts: UDS server — bind at the registeredmessagingSocketPath, accept
connections, validate the presented auth token against the local key file, decode frames via
codec.ts, reject on token mismatch without leaking whether the target session exists.- Validation: integration test — correct token accepted, wrong token rejected, malformed frame
rejected, all without crashing the listener
- Validation: integration test — correct token accepted, wrong token rejected, malformed frame
- 2.3
socket.ts: a liveness probe (bare connect-and-close), matching the confirmed behavior
of realListAgents/SendMessage— used before claiming a registered peer is reachable.- Validation: unit test — a registered-but-not-listening socket is reported unreachable, not
merely "registered"
- Validation: unit test — a registered-but-not-listening socket is reported unreachable, not
Slice 3: Sidecar process and lifecycle
- 3.1
packages/opencode/src/peer/claude/sidecar.ts: a minimal, standalone entry point — on
start, registers itself (Slice 1.4) under its own real PID and a fresh peer token, binds
the socket (Slice 2.2), and proxies decoded inbound messages to the real opencode session
via the existing opencode HTTP API (same injection pathpeer-messagingalready uses).- Validation: manual — a sidecar process started standalone registers correctly and can be
found byregistry.ts's own reader
- Validation: manual — a sidecar process started standalone registers correctly and can be
- 3.2 Session lifecycle hook: spawn a sidecar when a session opts into Claude visibility
(lazily, on first relevant peer-tool use — not for every session unconditionally); kill and
unregister it when the session ends or the server shuts down.- File: session lifecycle integration point (exact hook TBD by current session lifecycle API —
inspect before wiring, per repo convention of verifying current APIs rather than assuming) - Validation: integration test — sidecar process exits and its registry/key/socket files are
removed when the owning session ends
- File: session lifecycle integration point (exact hook TBD by current session lifecycle API —
- 3.3 Startup crash-recovery pass: on opencode server start, remove any sidecar registration
under~/.claude/sessions/(or$CLAUDE_CONFIG_DIR/sessions/) whose PID is no longer a
running process — mirrors the tolerance real Claude Code's own registry must have for dead
entries.- Validation: unit test — a registration with a dead PID is removed; a live one is untouched
Slice 4: Adapter — extend peer-messaging, not a parallel tool
- 4.1 Add
harness: "opencode-skein" | "claude-code"toPeerin
packages/opencode/src/session/peers.ts(currently implicit-only, perpeer-messaging
Slice 1.1's note that this was deferred until a second harness existed).- Validation:
bun run typecheck
- Validation:
- 4.2
packages/opencode/src/peer/adapter.ts: resolve Claude-Code targets by reading the
registry (Slice 1.3), probing reachability (Slice 2.3), and projecting them into the same
PeershaperesolveMessageTargetsproduces — sosend_peer_message's resolution logic
does not need to know which harness a target belongs to.- Validation: unit test — a Claude registry entry projects into a valid
Peer
- Validation: unit test — a Claude registry entry projects into a valid
- 4.3
send_peer_message(packages/opencode/src/tool/send-peer-message.ts): route delivery
through the Claude socket client (Slice 2.1) when the resolved peer's harness is
claude-code, instead of the localpromptOpsinjection path.- Validation:
bun test test/session/prompt.test.tsstill green; new test for a Claude-Code
target delivery path (mocked socket, not a real Claude process, for the unit-test tier)
- Validation:
Slice 5: Priority and busy/idle
- 5.1 Decode and preserve
priority(now/next/later) through the adapter; confirm no
code path letspriorityaffect permission evaluation — this must be checkable by reading
the permission-evaluation call sites, not just by absence of a bug report.- Validation: unit test / static check — priority value has no branch reaching
Permission.evaluate
- Validation: unit test / static check — priority value has no branch reaching
Slice 6: Diagnostics
- 6.1 Extend
peer-messaging's (or add alongside it) diagnostic surface with Claude-specific
detail: sidecar registration state, last probe result, and the specific named error
conditions fromfindings.md's catalogue rather than a generic failure message.- Validation: manual — each named error condition produces its own distinct message
Slice 7: Conformance and security tests
- 7.1 Unit tests: registry parsing/writing, codec round-trip, envelope sanitize/parse,
key-file hashing, name/PID resolution — per proposal.md's testing section. - 7.2 Security tests: forged token, stale/reused PID,
procStartmismatch, world-writable
runtime directory, symlinked socket/registry path, oversized frame, malformed NDJSON,
attempted envelope-injection message. - 7.3 Optional, manually-run conformance suite against a real stock Claude Code install —
record the tested version (mirrorsfindings.md's "Claude Code version tested: 2.1.261").
Slice 8: Verification
- 8.1
bun run typecheckandbun test test/peer/ test/tool/ test/session/green. - 8.2 Live check, following the same pattern as this spike but as a real, shipped feature
(not a throwaway): a real opencode-skein session'ssend_peer_messagereaches a real
Claude Code session viaListAgents, and a real Claude CodeSendMessagereaches a real
opencode-skein session — both directions, cleaned up afterward if any test artifacts remain.
Unchecked items above: see Disposition in proposal.md (2026-09-18).
Activity
[-]Native cross-session peer messaging between opencode-skein and Claude Code (gated)[/-][+]Native cross-session peer messaging between opencode-skein and Claude Code[/+]decebal commentedon Sep 8, 2026
androidand — opencode-skein #81 proved real Claude wire delivery, then chose one sidecar process per independently active session so peer identity stays honest. I build AllSource, an event-sourced agent-memory and provenance layer. I can help test durable send, acknowledgement, sidecar death, re-registration, and process-generation events so recovered peers never claim false liveness or repeat accepted work. That complements your verified adapter boundary without changing Claude protocol. Useful for one crash-recovery scenario? If not, reply “pass” and I will not follow up.
androidand commentedon Sep 17, 2026
Archived 2026-09-18 — see the change's Disposition section.