Skip to content

Native cross-session peer messaging between opencode-skein and Claude Code #81

Closed

Description

@androidand
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-bus for 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-directory send_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 on peer-messaging's Peer/PeerMessage model).
  • Modified: packages/opencode/src/session/peers.ts (harness field on Peer, 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 with msgV, 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
  • 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 a from="..."-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
  • 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 in findings.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 (dir 0700, registry/key files 0600) and procStart computed 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

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 registered messagingSocketPath, 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
  • 2.3 socket.ts: a liveness probe (bare connect-and-close), matching the confirmed behavior
    of real ListAgents/SendMessage — used before claiming a registered peer is reachable.
    • Validation: unit test — a registered-but-not-listening socket is reported unreachable, not
      merely "registered"

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 path peer-messaging already uses).
    • Validation: manual — a sidecar process started standalone registers correctly and can be
      found by registry.ts's own reader
  • 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
  • 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" to Peer in
    packages/opencode/src/session/peers.ts (currently implicit-only, per peer-messaging
    Slice 1.1's note that this was deferred until a second harness existed).
    • Validation: bun run typecheck
  • 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
    Peer shape resolveMessageTargets produces — so send_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
  • 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 local promptOps injection path.
    • Validation: bun test test/session/prompt.test.ts still green; new test for a Claude-Code
      target delivery path (mocked socket, not a real Claude process, for the unit-test tier)

Slice 5: Priority and busy/idle

  • 5.1 Decode and preserve priority (now/next/later) through the adapter; confirm no
    code path lets priority affect 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

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 from findings.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, procStart mismatch, 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 (mirrors findings.md's "Claude Code version tested: 2.1.261").

Slice 8: Verification

  • 8.1 bun run typecheck and bun 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's send_peer_message reaches a real
    Claude Code session via ListAgents, and a real Claude Code SendMessage reaches 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

  1. changed the title [-]Native cross-session peer messaging between opencode-skein and Claude Code (gated)[/-] [+]Native cross-session peer messaging between opencode-skein and Claude Code[/+] on Sep 5, 2026
  2. decebal commented on Sep 8, 2026

    @decebal

    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.

  3. androidand commented on Sep 17, 2026

    @androidand
    OwnerAuthor

    Archived 2026-09-18 — see the change's Disposition section.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions