Control room for AI agents
Agents Control Center
One inbox and control plane for many Claude Code sessions, across accounts and any number of machines, also from a phone.
- Role
- Sole author
- Period
- Oct 2026
- Status
- In daily use
- Code
- Private, built at Enara Health
- commits in 11 days
- 149
- versions, 0.1 to 0.76
- 108
- tests
- 565
- runtime dependencies
- 10
- TypeScript
- Bun
- React
- SQLite
- tmux
- Noise
- WebAuthn
- Web Push
- MCP
- whisper.cpp
01 Context
Why it exists
I run several Claude subscriptions side by side, on more than one machine. Each session asks for permissions and waits for answers in its own terminal tab, so prompts pile up where nobody is looking. Plan limits are visible only in each session's status line, and a session that hits one simply stalls.
Two more costs are easy to miss. A conversation resumed after its prompt cache expires costs about twice its size to rewrite. And the built-in remote control shows one conversation at a time, not the questions waiting across all of them. I wanted one place to see every session, answer what it asks, and keep work moving when an account or a machine runs out, without typing every answer by hand.
02 In use
What it does
Everything here works across sessions, accounts and machines, which a single Claude Code session cannot see. Claude Code is the agent it drives today; the server reaches agents through adapters, so others can be added without touching the UI.
-
One inbox for every session
Permission prompts and questions from every session, account and machine land in one list, with the full command and a risk assessment. Answer with a number key, a quick reply or from the phone. Force pushes, merges and migrations need a second confirmation.
-
Any number of machines
One main machine and as many workers as you pair. Each joins with one pasted command, reconnects by itself after sleep or a dropped network, and shows its CPU and memory. New sessions go to the machine with room.
-
Sessions that move, work included
A session continues on another account when its plan runs out, or on another machine when memory runs short. Its uncommitted work travels with it, privately, and nothing is committed or pushed for the move.
-
Recovery and automatic stops
An optional memory guard pauses the heaviest idle session when memory runs low. Long-idle sessions close after writing a handoff note and restart from it for a few thousand tokens. Closed sessions reopen with the same conversation, and outdated ones restart onto the newest Claude Code without losing it.
-
Secrets that never enter a conversation
Secure paste stores credentials encrypted and writes them only into env files git ignores; the session hears the key names, never the values. An agent that needs a file like
.envfrom another machine asks for it, and the person approves a list of names and sizes. -
A voice loop
Dictate with whisper.cpp on your own machine, switching between Spanish and English as you go. Kokoro reads the agent's replies aloud as they arrive, and spoken commands pause, repeat or skip; saying "reply" records an answer, sends it when you stop talking and reads the next reply.
-
Agents that work together
An MCP server in every session lets agents find each other, send messages, ask the person to start a session, request files and ask for a QA run in a real browser. Sessions started by another one are listed under it, across machines.
-
Plan and prompt-cache awareness
Plan usage for each account, and for each idle session its size and how long its prompt cache stays warm, because coming back after it expires costs about twice the conversation.
-
A live map of what runs
The Activity view draws a session's process tree, with the servers it left listening and a stop button for runaway commands. The network view shows every machine, with each message between agents flying along the links.
-
Accounts and terminals from any device
Add a Claude account and sign it in from the phone, and open a session's live terminal in the browser when it waits on a key the app cannot send.
03 Architecture
How it works
A local Bun server finds every interactive Claude Code session on the machine, reads what it is doing, and drives it through its own terminal. A React app, installable on a phone, shows them all with one inbox for the questions. Any number of worker machines run the same program without a UI and connect to the main one over an encrypted link; the main machine gives the orders and relays everything between them.
-
Adapters for agents and terminals
The server never calls Claude Code directly. An agent adapter only reads the agent's own state: its accounts, sessions, transcripts, subagents, past decisions, launch flags and how to resume a conversation. Pausing, closing and delivering messages do not depend on the agent, and a terminal transport sends keys, reads the screen and opens tabs. Claude Code, tmux and cmux are implemented; Codex CLI, Gemini CLI, opencode or Aider would each be one more adapter.
-
Find and watch sessions without owning them
Every two seconds the server reads the file Claude Code writes for each running process and the end of each transcript, and measures CPU and memory over each agent's whole process tree. Sessions live in tmux, not as children of the server, so they outlive it.
-
One inbox, answered safely
Permission prompts and questions are read off the terminal screen and checked against fifteen risk rules. To answer, the server reads the screen again and types only if the prompt still matches the one you saw. High-risk approvals, such as force pushes, merges and migrations, need a second confirmation.
-
Any number of machines over Noise
Each worker dials the main machine using the Noise IK handshake, after a first pairing with a one-time token, so there is no fixed number of machines: each one is another paired peer. The main machine can only invoke a closed list of calls on it, with no generic shell, file or exec call. Events are numbered and acknowledged, and a repeated call is never run twice.
-
An MCP server in every session
The app installs an
agentsMCP server into every account on every machine. Agents use it to see where they run and how much plan is left, find each other, send messages, start or move sessions, request files and ask for a QA run. Messages arrive marked as not coming from the user. -
Sessions that move
When a session hits its plan limit, the app moves it on its own, preferring the same account group, then the same machine, then plan headroom and free memory. Across machines, uncommitted work travels as a git bundle, binary patches and an archive, and the target checks that
git statusmatches. -
Secrets kept out of conversations
Secure paste keeps credentials encrypted at rest and writes them only into env files git ignores, so a secret is never committed. Agents use the values from the shell without printing them, and every save, write, read and deletion is audited without the value.
-
A phone that is really a remote
Phones pair with a QR code and reach the app over Tailscale. A passkey locks it and push notifications are encrypted end to end. Speech stays on your own machines: whisper.cpp transcribes, Kokoro reads aloud and Vosk listens for spoken commands.
04 Decisions
The calls that shaped it
-
Decision 01
Adapters, so Claude Code is the first agent and not the only one
- Chose
- An agent adapter that only reads the agent's own state, and a terminal transport that sends keys, reads the screen and opens tabs.
- Over
- Calling Claude Code and tmux directly from the server.
- Because
- Another agent or terminal is one new module behind the same inbox, phone and machine link.
- Cost
- With one agent implemented, the interface is shaped by Claude Code, and moves and idle closing still read Claude Code's files directly.
-
Decision 02
Drive the real terminal instead of wrapping the agent
- Chose
- Read Claude Code's own session files and transcripts, parse its terminal screen, and type into tmux.
- Over
- Starting sessions through an SDK, which would hide them from the terminal and lose interactive features.
- Because
- Sessions opened by hand show up too, interactive features keep working, and sessions survive restarts of the app.
- Cost
- None of that is a public interface, so a Claude Code update can break a reading. Several releases fixed exactly that, and the parsers are tested against real screens and transcripts.
-
Decision 03
Write a Noise channel, with the worker dialing out
- Chose
- A Noise IK link implemented in the repository on audited primitives, tested against the published test vectors.
- Over
- SSH, which needs a server and port forwarding inside WSL, and a VPN as the node link, which authenticates machines rather than this app.
- Because
- Mutual authentication in one round trip, it works from behind NAT, and adding a machine is one pasted command.
- Cost
- A protocol to own and keep secure, and a star: everything between workers passes through the main machine.
-
Decision 04
Move work without committing or pushing
- Chose
- Pack uncommitted work as a bundle, patches and an archive, send it over the link, and verify it on arrival.
- Over
- The first version, which had agents commit and push to a work-in-progress branch before moving.
- Because
- Work must never reach a remote just because a session moved; it may contain files that should never leave the machine.
- Cost
- A long tail of edge cases: platform metadata files, a 1.2 GB move that crashed the server, and rollback when a move fails halfway.
-
Decision 05
Bound what agents may do on their own
- Chose
- An agent may move itself without asking only when its plan is used up or its machine is short of memory, and only within its account group. Starting sessions, moving across groups and requesting files wait for the user.
- Over
- Either full autonomy or asking about everything.
- Because
- The common emergencies are handled at once, and anything that spends someone else's plan or reaches into another machine stays a human decision.
- Cost
- More approval cards in the inbox.
-
Decision 06
Keep secrets out of the conversation
- Chose
- An encrypted local store for credentials, a file handoff between machines that the person approves, and a hook that stops, once, a tool call about to read a secret and points to the safe route.
- Over
- Pasting keys into the chat, or letting agents read
.envfiles whenever they need a value. - Because
- Anything that enters a conversation stays in its transcript and is sent again with every later request.
- Cost
- One more step whenever an agent needs a key, and stored secrets stay on the main machine for now.
-
Decision 07
Reach the phone over a private network
- Chose
- Tailscale with its certificate, a passkey lock, and Web Push implemented in the repository.
- Over
- A public tunnel with an access layer in front, or a relay server to run.
- Because
- Nothing is reachable from the internet, and there is no extra server to maintain.
- Cost
- Tailscale on both devices, and phones paired on the local network have to pair again.
05 What is interesting
Worth a closer look
-
Approvals are compare-and-swap
A keystroke goes in only if the prompt read again from the screen still has the fingerprint the person approved. Messages are typed only into an empty input box, never on top of a draft.
-
State and history are kept apart on a flaky link
Snapshots are state, sent whole, so a reconnect just resends the latest. Things that must happen once are numbered events held until acknowledged, and calls are idempotent by id. Reconnects back off with jitter.
-
Built around the prompt cache
It shows each session's cache lifetime, keeps caches warm with tiny pings that stop at 80% of the plan, closes idle sessions after writing a handoff note, and adds its own hints to tool results rather than the system prompt, so the cache stays valid.
-
Moves that survive a bad network
Files travel in pieces relayed by the main machine, four in flight, each checked by SHA-256 and sent again if damaged. Past 1 GB the source packs one archive first, compressed with zstd when both machines have it.
-
Spoken commands without an open microphone
While it reads aloud it listens for a few command words. Only short bursts of speech leave the device, to the machine running the voice, and a command the voice itself just said is ignored.
-
Small dependency footprint
Ten runtime dependencies. Noise, Web Push, WebAuthn verification and the MCP server are written in the repository, each small enough to read.
06 Built with agents
How agents fit in
It was built in eleven days, 149 commits from version 0.1 to 0.76, and every one of its 132 non-merge commits has a Claude co-author. The rules file sets three habits: everything in English, a CHANGELOG entry and a version bump with every pull request, and the full check (types, tests, web build, plugin validation) before every push. Merges go through a script that runs the same checks on the result in a throwaway worktree.
What changed most is who carries messages. Agents ask each other directly instead of through me, request the files they need without secrets being pasted into a conversation, ask the QA console to check their UI work in a real browser, and move themselves off an account whose plan is spent.
It ran real work while it was being built, and the history shows it under load: a 1.2 GB session move that crashed the server, voice servers piling up in memory, transcripts of 90 MB. Each became a fix and a test.