Arnav Daultani

← work

typescript
2026
live

Beacon, local mission control for Claude Code agents

Open source, MIT licensed, published on npm.

problem

Claude Code sessions pile up fast: one per terminal tab, one per project, background agents nobody remembers starting. There is no single place to see them all, no way to check what a background agent is doing without hunting down its transcript, and no shared backlog when several agents are working on related things across different repositories.

approach

A Fastify server discovers every session on the machine by watching Claude Code's own on-disk state with fs.watch, cross-checked periodically against `claude agents --json --all` rather than polled as the fast path. A single WebSocket hub pushes the live session list and agent events to a React 19 and Vite frontend.

Agents split into two classes. Discovered agents, started normally in a terminal, are read-only: live status, transcript, token usage. Owned agents, launched by Beacon itself through the Agent SDK, get real control: streamed output, follow-up prompts, interrupt, permission approval, kill.

A cross-project ticket board sits behind both classes of agent. It is backed by SQLite and exposed over MCP, both a standalone HTTP endpoint for external sessions and an in-process server for owned ones, so agents in different repositories can read and write the same backlog.

tradeoffs

  • choice Never speak Claude Code's private daemon control-socket protocol. Take over an external session through the public Agent SDK instead, by stopping it and resuming the same session id under an SDK-owned process.
    cost Taking control of a session someone else started is a visible, deliberate action that interrupts whatever it was doing mid-flight, rather than a silent handoff. The private protocol would have made that seamless, and it was also undocumented and could change on any Claude Code update.
  • choice Bind to 127.0.0.1 by default. Binding elsewhere requires a bearer token, generated fresh and printed once when the server starts, never persisted to disk.
    cost Reaching the dashboard from another device on the network takes an explicit extra step instead of just working, which is the point: transcripts can contain raw shell output and full file contents, and that is not something to expose by default.
  • choice Session state is read via fs.watch and cross-checked periodically against `claude agents --json --all`, never polled as the fast path.
    cost Reconciling two sources of truth about what is running is more logic than trusting one poll loop, and disagreements between them have to be resolved deliberately rather than just picked up on the next tick.

what broke

The first version of adopt reached straight into a session someone else had started: find the process by pid, attach a controller to it, done. That meant anything hitting the API could seize a live session out from under whoever was running it, with no seam where consent belonged, and a reused pid from a long-dead process was indistinguishable from a live one.

The fix was adopt-via-resume. Beacon never attaches to a process it did not launch. It stops the external session, verifies the pid's start time against the live process first so a recycled pid can never be signalled by mistake, then resumes that same session id under its own Agent SDK owned process. Taking control is a visible, one-way action that interrupts the session, not a silent handoff.

what I learned

Control over a process you did not start has to be a deliberate, visible action, never a seamless attach. Making it interrupt the session was the fix, not a compromise on top of one.

A pid alone is not an identity. Verifying it against the process's actual start time before signalling anything is the only way to be sure you are killing what you think you are.