Since May I've been building Agent Console, a small desktop app for supervising coding agents — Claude Code and Codex, side by side. It went from v0.1 to v0.76 in a little over three months, across roughly 175 pull requests, and here's the part that still feels strange to write: the agents wrote the overwhelming majority of the code, supervised through the very app they were building.
This post is about what I learned — the design bets that paid off, the ones that bit me, and the war stories that only show up when you ship a real desktop app instead of a demo.
Why a console, not an IDE
Coding agents ship as terminal UIs, and the CLIs are genuinely good at running the agent. They're weak at supervising it. Working with agents daily, what I actually needed was:
- an approval modal I can answer with two keystrokes while reading the exact command about to run,
- a per-turn snapshot I can restore when turn 7 wrecked the work of turn 3,
- a diff viewer scoped to what the agent just changed,
- an evidence trail for the day someone asks "what exactly did the AI do?"
None of that is an editor feature. My thesis became: the agent is the editor; the human needs a cockpit, not a code buffer. So the center of Agent Console is deliberately small — a real PTY terminal that auto-launches the agent, a git diff view, and the approval/snapshot loop. You don't read code in it; you direct the agent and verify its work. That center hasn't grown since v0.1, and defending that boundary was the single best product decision I made.
The multi-engine bet
I designed for two engines from early on — Claude Code and Codex — mostly out of stubbornness. It turned out to be the most profitable architectural constraint in the project.
Supporting two agents forces you to find the real seams. In Agent Console, adding an engine is one profile entry in the frontend plus one Rust adapter in the backend. Everything else — sessions, approvals, resume, the terminal — is engine-neutral because it had to be.
Two things I'd never have learned with a single engine:
Permission stores have philosophies. When a user clicks "always allow" for a tool, Claude Code wants that persisted in settings.json; Codex wants an execpolicy prefix_rule in .codex/rules/*.rules — a completely different model (rule files evaluated by an engine, not a flat allowlist). Same button in my UI, radically different write path per engine. If your integration only supports one agent, you'll bake its philosophy into your core and port painfully later.
Convergence is real. While wiring hooks for both engines, I discovered Codex had adopted Claude Code's hook system with schema-identical payloads (~/.codex/hooks.json). I could serve both engines' lifecycle events with one bridge. Nobody announced this anywhere; you only find it by building against both.
The flip side: you inherit two release treadmills. A Codex update restructured the exec --json event schema with no version marker, silently breaking parsers downstream. I now run a daily watch over both CLIs' releases and filed upstream issues asking for stability guarantees (claude-code#90220, claude-code#90221, codex#41216). If you integrate coding agents, budget for churn as a permanent line item.
Trust needs evidence, not vibes
An approval modal answers "do I let this run?" It doesn't answer the question that comes later: "what did the AI actually do, and can you prove it?"
So every session in Agent Console is witnessed: prompts, human approvals, tool results and per-turn diffs land in a hash-chained ledger. You can export signed proof packets (Ed25519, DSSE envelopes) that anyone verifies in a browser, no server involved. I published the protocol separately as Testigo.
Being precise about what this proves matters: it proves this sequence of events happened in this order and wasn't edited after the fact. It does not prove the code is good. It's an audit trail, not an oracle. But when a client asks what the AI did to their codebase, "here's a verifiable packet" beats "here's my scrollback" by a wide margin.
The recursive part: this ledger supervised the app's own development. Every PR the agents wrote passed through the approval flow and landed in the chain. At some point the tool became good enough to supervise its own construction — that loop, more than any feature, is the product.
War stories
The hardest bugs were never the features. They were the seams between the webview, the OS, and the terminal.
WebKitGTK's clipboard lies. On Linux, navigator.clipboard.writeText() resolves successfully — and writes nothing. No error, no warning; the promise just lies to you. Copy in the terminal was broken three different ways at once: the false-success API (fixed by going through Tauri's native clipboard plugin), xterm.js selection being invisible to the webview's context menu (fixed by snapshotting the selection on xterm's onSelectionChange, not at click time), and TUIs like vim enabling mouse-tracking so the terminal eats your drag (that's what Shift+drag is for). Each fix looked correct in code review; only a human clicking in the real app could confirm them. GUI bugs demand GUI verification — no amount of unit testing substitutes.
Windows hooks died silently for weeks. The integration installs a lifecycle hook whose command was a bare path to a script. On Linux and macOS: shebang, works. On Windows: no shebang support — and the user's home directory had a space in it, so even the fallback misparsed. The hooks simply never ran, silently. The fix is embarrassingly small — invoke node "<path>" explicitly, quoted — but the lesson isn't: a hook that fails silently is indistinguishable from a hook that never fired, and cross-platform paths-with-spaces remain undefeated after four decades.
Release pipelines have races. With 11 platform targets building concurrently, the updater manifest (latest.json) could go live before all binaries finished uploading — an auto-updater pointing at assets that don't exist yet. The fix was making the manifest publish strictly last. Two-stage releases (build everything, then flip visibility) should be the default for multi-platform updaters, not an afterthought.
Your ML dependency has a glibc opinion. Local semantic search first used ort (ONNX Runtime bindings) — whose prebuilt binaries demand glibc 2.38, newer than what most stable distros ship. CI caught it before users did. I swapped to candle, pure Rust, compiles anywhere. For local-first apps, "pure Rust, no system deps" is worth real performance trade-offs.
The feature that was quietly dead. The knowledge system injected relevant memory into agent prompts via a hook with a hard timeout. The embedding model loaded per request — about 3-4 seconds on first call — while the hook's budget was 1.5 seconds. The injection had never once fired in production, and nothing complained. Metrics I'd collected about the feature measured a code path that never executed. Now I instrument the success path, not just failures: a feature that silently does nothing looks identical to a healthy one from the outside.
What's next
Rooms — you plus N agents, mixed engines, debating and editing on an isolated branch you review and merge — are functional and growing toward consensus detection. On the integration side, I'm betting the stable surface for tools like mine ends up being an app-server protocol rather than parsing CLI output, and I'd happily migrate the day either vendor commits to one.
Agent Console is free and open source (AGPL-3.0), local-first — no accounts, no telemetry, no cloud. Linux, Windows and macOS, installable via winget and AUR. If you live in a terminal next to a coding agent all day, I built this for you: github.com/cyl-castillo/agent-console
Happy to answer questions in the comments — especially from anyone else building on top of these CLIs. We're all reverse-engineering the same seams.
Top comments (0)