Bastiaan Breemer

Tooling · 7 min read

Building on top of Claude Code and Codex

After the T3 Code post I wanted the same thing on a headless box, without a Node toolchain on the server. So I wrote starcode, a web console for coding agents in one Go binary. It started on 4 September. Seven weeks later it has about 35,000 lines of Go, 8,900 of them tests, and I use it every day.

That sounds like a lot for a side project. It isn't, because I didn't write an agent. Claude Code and Codex already are the agent, and both of them will run as a process you talk to over stdin and stdout. Everything starcode does sits on top of that. The UI turned out to be the smaller half.

The CLIs are already servers

Claude Code has a print mode meant for scripts. With the stream-json formats on both ends, it stops being a one-shot command and becomes a long-running session:

claude -p \
  --input-format stream-json \
  --output-format stream-json \
  --verbose \
  --include-partial-messages \
  --permission-prompt-tool stdio

You write user messages to stdin as JSON lines. Stdout sends back one JSON object per line: a system init with the session id, stream_event lines with text, thinking and tool input as they are generated, the finished assistant message, and a result when the turn ends. Next to that runs a control channel. A control_request with subtype interrupt stops the turn, set_permission_mode switches modes mid-session. --permission-prompt-tool stdio is the flag that matters most. Instead of asking in a terminal nobody is looking at, the CLI sends a can_use_tool request on stdout and waits for your answer on stdin. That is the whole approval flow in starcode, buttons in a transcript that write one line back.

Codex goes further. codex app-server is a JSON-RPC server over stdio, and one process can carry many threads at once. Notifications like item/agentMessage/delta and turn/completed arrive tagged with their thread, and approvals come as requests the server makes to you (item/permissions/requestApproval). It will also print its own schema with codex app-server generate-json-schema, which is more than Claude Code gives you. For Claude I read the stream and the transcripts until I knew what each line meant.

What you get without writing it

Because starcode runs the real CLI, it gets everything the CLI has. The agent's own tools, subagents, MCP servers from your config, compaction, skills and hooks all work as they do in a terminal. Billing is whatever account the CLI is signed in with, so a Claude or ChatGPT subscription works and there is no API key in the picture. When a new model ships, it shows up after claude update. I don't touch starcode for any of it.

The adapters are the only part that knows about either CLI. Both turn their protocol into the same small set of events (text, thinking, a tool call, an approval, the end of a turn), and the rest of starcode only sees those. The Claude adapter and the Codex adapter together are about 7,800 lines. Everything else is the console.

What the protocol lets you add

Most of the features I like in starcode are a few lines of protocol each, plus the UI around them.

Rewind and branching come from two flags. --resume <session> --resume-session-at <message> --fork-session starts a new session that is a copy of the old conversation up to a given point. Rewinding a thread drops a prompt and everything after it, and the agent's next turn runs in a fork that ends where that prompt began. Branching is the same fork, kept next to the original. The files on disk stay as they are in both cases, which is what I want. The conversation is what went wrong, not the code.

Surviving a restart is --resume again. Starcode keeps every event in a SQLite log, so when it comes back up it knows which turns were running and resumes each session with a prompt that says what happened.

Usage limits come from a get_usage control request. It goes through the same control channel without a prompt, so it costs a process launch and no tokens. During a turn, rate_limit_event lines update the same bars. The costs per thread come from the transcripts both CLIs write to disk anyway (~/.claude/projects/ holds JSONL files, one per session), so starcode just scans them.

Handing a thread to another agent, when one runs out of quota, needs nothing from either CLI. Starcode writes the conversation to a file, starts a thread with the other agent in the same checkout, and tells it to read the file and say where things stand before changing anything.

Giving the agent tools back

The step that changed how I work was the other direction. Starcode registers itself as an MCP server for every session it starts, through --mcp-config for Claude and an mcp_servers.starcode config entry for Codex. The server is starcode mcp, a small stdio process that forwards each call over a socket to the running instance. Each agent's environment holds its thread id and a key derived from it, so one thread can't act as another.

Through it, an agent can rename its thread, link the pull request it opened and watch it, snooze itself with a prompt for later, schedule prompts on a cron, and notify my phone. It can also start threads of its own. They show up under the parent in the sidebar, and when a child finishes, the parent gets a prompt with its reply.

None of this is clever on the agent side. "Open the PR, watch it, and fix what CI says" used to be me checking back three times. Now the agent links the PR, starcode prompts it when a check fails, and I read the result once.

Building it from inside itself

Once starcode could run threads reliably, I moved its own development into it. It runs as a systemd user service, from the binary in its own repo. A thread working on starcode runs make build like any other project. The running instance notices the binary changed and puts a banner on every open page: "starcode was rebuilt. Restart to run the new version." Pressing it drains, re-execs the new binary in place (same pid, so systemd sees nothing), and the pages reconnect within a second or two. The threads that were running resume, including the one that just rebuilt it.

That loop is the reason the project got as big as it did. When something annoyed me while using it, I typed the complaint into a thread in the same window, and a few minutes later I pressed restart and the annoyance was gone. 37 of the 54 commits carry a Claude co-author line. The feature list in the README reads like a log of things that bothered me on a given evening.

It has a cost. A thread that breaks the build can't break starcode, because a failed build leaves the old binary in place. A thread that ships a bug in the restart path can. For that there is still systemctl --user restart starcode and a terminal.

Where it gets brittle

The Claude stream-json format is not a stable public API. The Agent SDK sits on the same protocol, so it is not going away, but fields appear and change between versions. Codex's schema helps, and it still moves. What keeps it working is the integration tests for each adapter, which start the real CLIs and run real turns. They cost a few cents per run, and they are the first thing that fails after an update. For everything that isn't the adapters there is a scripted fake agent, so UI work and the browser tests cost no tokens at all.

Some of it is reaching further than I'd like. The Claude adapter reads ai-title lines from the transcript files to pick up the title the CLI generates, because nothing on the stream carries it. Banked plan resets go through an undocumented endpoint. Both will break one day, and when they do it will be a small fix in one file rather than a broken tool.

What I took from it

The interesting part of a coding agent tool is no longer the agent. Anthropic and OpenAI keep improving that, and a tool built around their CLIs gets it on the next update. What's left to build is everything around the session: where it runs, who can reach it, what happens when you aren't looking, and which tools the agent has to reach back. That is a much smaller problem, small enough that the agent can do most of it, from inside the thing it is building.