# 🤖 Shared remote Claude Code sessions — an Eleven group fronting a workstation

**Date:** 2026-08-05 · **Status:** accepted — open questions decided by Paul
2026-08-28 (see § Decisions) · **Scope:** design only — no implementation yet

## What this is

A group in Eleven becomes a shared front-end to **Claude Code sessions running
on a remote workstation** (the motivating box: docs-t1). Multiple people share
the sessions: the chat on the left is where you steer — **@claude is an
ordinary member of the chat** — and the right-hand pane is a live view of the
remote sessions. Agent activity is translated into Eleven primitives (typed
events, cards, views, action lines, notices) rather than mirrored as a
terminal.

The design uses almost entirely existing machinery — no new group type, no new
E2EE exceptions, one new sidecar-shaped process and one new mini-app.

```
┌────────────────────────────┬──────────────────────────────┐
│  Chat                      │  agents app (Interface pane) │
│                            │                              │
│  Paul: @claude fix the     │  ▸ fix-login   📁 messenger  │
│        flaky reminder test │    running · turn 3          │
│  @claude [fix-login]:      │    ⚙ go test ./internal/bot  │
│    Found it — the ticker…  │  ▸ ops-poke    🖥 workstation │
│  Ana: @claude also check…  │    waiting for approval      │
│                            │    [Allow] [Deny]            │
└────────────────────────────┴──────────────────────────────┘
   the shipped Chat | Split | widget-bar layout, nothing new
```

## Trust model (read this first)

- **The E2EE rule holds untouched.** The server relays opaque ciphertext;
  every `session.*` event, prompt, approval, and view is encrypted under the
  thread key like any message. The server never learns what the sessions do,
  which repos exist, or that a box called docs-t1 is involved.
- **The bridge is a bot-model process** (docs/bots.md): an E2EE member whose
  "device" is the workstation. It runs ON the box, beside the sessions, holds
  its member private key + the Anthropic access there, and is a **pure
  outbound client** — dials the instance and the Anthropic API, no inbound
  ports. Bot code never runs inside the lchat server process.
- **Group membership is the consent moment.** Everyone in the group can steer
  sessions, which means everyone in the group has a lever on the box —
  filesystem, git credentials, whatever the bridge's unix user reaches. This
  is the operator-only dev-bot tier (docs/bots.md tier 4): chat input is
  untrusted input to a machine with authority. The bridge's config (below)
  enumerates exactly what that authority is; adding a member is granting it.
- **Flat authority for v1.** Any member can prompt, approve, and interrupt.
  Roles (driver/observer) are layerable later as ordinary fold state
  (`agents.role` events) without schema changes.

## The pieces

### 1. The bridge — `lchat agents run`

A sibling subcommand of `lchat bot run`, reusing the Go sidecar runtime
(WebSocket client, key unwrap, reconnect, trigger plumbing — all existing). It
runs on the workstation and:

- joins the instance as the **@claude member** (one keypair, one roster row —
  the standard bot-member shape, not a charter persona);
- owns the Claude Code sessions: spawns them **headless** (`claude -p
  --output-format stream-json`, resumable via `--resume <session-id>`; the
  Agent SDK is the same thing as a library), consumes the **structured event
  stream** — assistant text, tool_use blocks with full inputs, tool results,
  turn boundaries. **No terminal emulator**: nothing scrapes a TTY. The only
  terminal-ish work is stripping/basic-rendering ANSI colours out of Bash tool
  output (~50 lines, no cursor addressing exists in a `-p` session);
- translates the stream into encrypted `session.*` events (vocabulary below)
  posted into the group thread, and reads members' prompt/approval/interrupt
  events back off the same thread;
- serializes turns per session (a session is one-turn-at-a-time), queues
  prompts that arrive mid-turn, and **attributes prompts by speaker** —
  "Paul: fix the flaky test" — so the model knows it's talking to a room.

Config, per group the bridge serves:

```
groups:
  <thread-id>:
    roots:  ["~/github.com/elevenmessenger/messenger"]  # scoped sessions live here
    plain:  false                                       # whole-workstation sessions allowed?
```

The config is the blast radius, written down in one place on the box.

### 2. Sessions — two flavors, chosen at spawn

- **Scoped** (default): bound to a directory — an allowed root, normally a
  **fresh git worktree under it** so parallel sessions don't trample each
  other. A scoped session gets that project's own `.claude/` settings,
  CLAUDE.md, hooks, and permission config exactly as a human dev would — the
  repo's existing guardrails govern the remote session with zero new
  machinery.
- **Plain** (opt-in via `plain: true`): no pinning — cwd = home, the whole
  workstation at the bridge's unix user's reach. The "shared computer" mode
  for ops-ish, cross-repo work. Off by default because it hands every member
  the entire box.

Flavor is **visible everywhere**: carried on `session.spawn`, badged on the
session's view ("📁 messenger @ fix-login" vs "🖥 full workstation") and on its
chat card. Nobody wonders which kind of hands are on the box.

Birth is conversational: "@claude fix X" with zero live sessions spawns a
scoped session in the group's default root; `/claude new <name>` names one;
`/claude new --plain <name>` when the config allows. Session state persists on
disk on the box (Claude Code's own session files), so a human on the
workstation can `claude --resume <id>` from a real terminal after the bridge
lets go — continuity is preserved; concurrent dual-driving is not pretended.

### 3. Chat — @claude plus focus-based routing

One bot member; the mental model is "a colleague in the chat who happens to be
several sessions deep."

- **Routing: the target is the active view.** Each member's client knows which
  session view they have focused in the right pane; a message to @claude
  carries `target: <session-id>` **inside the encrypted payload**. Focus is
  per-member — two people can steer two sessions from the same chat
  simultaneously.
- **Routing is visible at send time.** When more than one session is live, the
  composer shows a chip — "→ fix-login" — tappable to retarget without
  touching the pane.
- **Replies pin the target.** Every @claude bubble carries a subtle session
  tag; replying to a bubble routes to that session regardless of pane focus —
  this is also the mobile/chat-only answer. The tag works the other way too:
  tapping it focuses that session's view.
- **Degenerate cases collapse.** One live session: everything routes there, no
  chip. Zero sessions: @claude answers as the workstation ("no sessions
  running — want one?") and a prompt spawns one.

Output posts as @claude with the session tag on each bubble — the feed reads
as one assistant working several desks, which is the truth.

### 4. The right pane — one app, one view per session

- **One `agents` mini-app** (`web/static/apps/agents.js`), chat-placed
  (`places: ["chat"]`), added to the group like any app — one glyph in the
  widget bar, rendering in the standard Interface pane. The shipped
  Chat | Split | widget-bar toggle IS the "chat left, session right" layout;
  nothing new is built for it.
- Its Interface is the **fleet board**: one card per session (name, flavor
  badge, status, current activity line, waiting-on-approval chips), folded
  from durable `session.*` events.
- **Per-session status rides the views engine** (`views.js`,
  `view.create`/`view.update` from the bridge): one engine view per session.
  Views are declarative SCHEMA + DATA interpreted by **both web and the Swift
  twin engine**, so session boards render on iOS/Mac with no per-feature
  Swift. Chips on @claude's replies open the session's view; `/views` lists
  them.
- **Fidelity is "board + drill-in" (option c).** Tap a session to focus it:
  the durable board plus a **live tail** — tool-call chips ("Ran `go test
  ./...`"), collapsed monospace output, file-edit lines, assistant prose —
  streamed as **ephemeral events** (relay-only frames, the existing
  primitive): live viewers see the chatter; nobody replays it; the encrypted
  log stores only what deserves permanence. The views engine cannot host the
  tail (views are durable documents; streaming through `view.update` would
  bloat the log) — the tail is the app's own render over the ephemeral
  stream, web-first.

### 5. Event vocabulary

Durable (thread log; the fold state every member converges on):

| event | payload sketch | why durable |
|---|---|---|
| `session.spawn` | id, name, flavor, root/worktree, by | fleet board exists offline |
| `session.status` | id, state (running/idle/waiting/dead), activity line | board truth |
| `session.prompt` | id=target, text, by | the steering history IS the log |
| `session.turn` | id, assistant text (final, per turn) | the conversation with the agent |
| `session.result` | id, summary card (files changed, tests, links) | milestones as cards |
| `session.approval` | id, request (tool, input digest), state, decided-by | audit trail of authority used |
| `session.end` | id, reason, by | lifecycle honesty |

Ephemeral (relay-only; live tail + presence):

- `session.tool` — tool_use started/finished, truncated output chunks
- `session.stream` — assistant text deltas while a turn is in flight
- typing-style "session is thinking" indicators

Streaming into *chat* has no partial-message primitive, so @claude's chat
bubbles are **post-per-turn** (the durable `session.turn`), with the live
delta visible in the drill-in tail instead — no post-then-edit churn in the
feed.

### 6. Approvals

Claude Code's permission gate surfaces as a chip row — on the session's board
and as a @claude chat card: "fix-login wants to run `git push` — [Allow]
[Deny]". A tap emits an encrypted `session.approval` decision event; the
bridge reads it and answers the gate. Flat authority: first decision wins,
attributed in the event and shown in the feed ("Ana allowed `git push`").
Notices/push ride the existing E2EE notify path, so "waiting for approval"
reaches pockets.

### 7. Liveness

Derived, never declared (the docs/bots.md pattern): the bridge heartbeats
(ephemeral presence + a periodic durable status touch); a session whose bridge
is gone must **read as dead/stale** in the board — "last seen 4m ago" — not
linger as "running". A dead bridge silently eating prompts is the worst
failure mode; staleness is always rendered from event age, client-side.

## What the server gains

Nothing. Every piece rides existing surfaces: bot membership, thread events,
ephemeral frames, the views engine, the mini-app framework, notify/push. The
bridge is a new subcommand of the existing binary; the app is one new file
under `web/static/apps/`.

## Hard parts, named

- **The approval round-trip** — latency between the gate firing and a human
  tapping Allow; the session blocks meanwhile. Mitigations: push notices,
  per-session auto-allow lists (later; config-side, never chat-grantable).
- **Log discipline** — a busy session emits a lot; the durable/ephemeral split
  above is the design's load-bearing wall. Anything high-churn must be
  ephemeral or it bloats every member's encrypted log forever.
- **Prompt-injection containment** — session output is model output over
  untrusted repo/web content; it lands in chat as content, never as commands.
  The bridge only ever acts on typed events from members.

## Deferred (deliberately)

- **Driver/observer roles** — layer later as `agents.role` fold state.
- **Fleet-across-groups overview** — a "you"-placed app aggregating every
  workstation group; needs nothing new from this design.
- **Attaching to a human's interactive TUI session** — ruled out for v1
  (tmux-scraping + keystroke injection fights the one-writer TUI; the honest
  handoff is `--resume` after the bridge lets go).
- **Auto-allow permission profiles** per session flavor.
- **Native drill-in tail** — boards are native-free via the views engine;
  the live tail ships web-first.

## Decisions (Paul, 2026-08-28)

1. **Naming: `agents` stands.** `lchat agents run`, `apps/agents.js`,
   `session.*` events, and the member reads as @claude.
2. **The prompt IS the chat message.** One `session.prompt` event, two
   projections — the member's visible bubble in the feed and the bridge's
   steering input, tagged with the target. No separate `chat.msg`.
3. **Closing a session closes its worktree.** Sessions are closable from
   within Eleven, and `session.end` is the cleanup moment: the bridge removes
   a scoped session's worktree then. The branch (any committed work) survives
   worktree removal as an ordinary git branch; only the checkout goes.
