DEV Community

Space App
Space App

Posted on Fully Autonomous

One tool layer for eleven AI coding CLIs: what we learned building SpaceApp

Why I built SpaceApp, in my own words:

I built it to be my main work tool. I'm a developer! And it became my main tool and work environment. I no longer need to open email, social media or any development program: absolutely nothing. I'm inside SpaceApp all day; I even watch YouTube videos and listen to music through SpaceApp.

SpaceApp is a self-hosted workspace where AI coding CLIs (Claude Code, Codex, OpenCode, Gemini CLI and others) run side by side in persistent rooms. Each of them comes with its own config format, its own way of registering MCP servers, its own sign-in, its own "resume the last session" flag and its own idea of what Esc means. This post is about the two parts that turned out to matter most: where the agents' tools come from, and where the agents' sessions live.

The N×M problem

Every CLI wants its tools declared in its own file: one JSON shape for one, a TOML table for another, a command-line flag for a third. Multiply that by the tools you want every agent to have (a managed browser, shared memory, tasks, files, deployment checks) and you get N×M configurations that drift apart. Worse, a CLI usually starts every declared MCP server when the session starts, whether or not the agent ever calls it. Twelve panes × five servers is a lot of idle processes.

Capability first, MCP on demand

The first rule we settled on: use what the runtime already has. If the CLI has a shell, file editing and image input, the agent uses them directly. A tool layer that wraps cat in an MCP call only adds latency and tokens.

For SpaceApp's own operations (status, logs, the managed browser, memory), the route depends on the runtime:

  • Codex and Claude Code panes register no MCP server at startup. When the agent needs a SpaceApp operation it calls one on-demand entrypoint, space-capability. It answers "what can I call and with which arguments" from cached, compact schemas without starting anything. The first real call starts a small worker on a pane-private Unix socket, and that worker starts only the backend that was asked for: at most three per pane, each stopped after an idle timeout.
  • Other runtimes keep a single dispatcher (space-mcp-dispatcher) declared once from a shared registry. The dispatcher also spawns registry servers only when called and stops them when idle.

Two details turned out to matter more than the transport:

  1. A declared tool grants nothing. Every operation still passes its own schema, role, approval and broker gates, whichever path it came through. MCP is a transport, not an authorization model.
  2. No replays after ambiguity. If a call may have executed and the transport broke, the call is not retried automatically. A duplicated deploy check is annoying; a duplicated "send" is worse.

The operator can override routing per runtime, model and tool (Automatic, Native only, Allow fallback, Disabled), and the settings page explains which route a call will take.

This path is still marked experimental in the architecture doc: it is enabled for Codex and Claude Code only, because each runtime's capabilities have to be verified before it gets the on-demand path.

Sessions that outlive the web server

The second lesson: the process that serves the UI must not own the agents.

In SpaceApp each CLI runs in a terminal session owned by a separate pane host process, not by the API. The API attaches to that session over a socket. When the API restarts during a deploy, or the browser drops its connection, the agent keeps working, and the pane re-attaches to the same session. The pane host tracks session generations and output sequence numbers, so a reconnecting view knows where it left off.

That separation is also why we never resume a provider conversation by copying a transcript. If a CLI has a native resume (a session id, a thread id), we use it; if it does not, we say so instead of pretending.

Eleven TUIs are eleven TUIs

Things that broke along the way, in roughly the order they hurt:

  • Stop means different keys. Esc interrupts a turn in some CLIs, Ctrl-C in others, and one is stopped through its own server API. Stop now sends each TUI its own interrupt.
  • Model pickers are UI, not API. Switching a model in one CLI means driving its /model menu. In a short pane the menu shows a scrolled window of rows and clips its hints, so the picker has to read scroll markers and page through the menu. It also has to pick "this session only", never the saved default every other session shares.
  • "Ready" screens lie. An old, partial MCP startup message once kept a newer, ready Codex screen from being detected. Now a newer header supersedes the earlier partial marker, while a screen that is still loading keeps blocking.
  • Contracts are not end-to-end proof. A contract suite checks every declared start, resume and login command line of every declared runtime against the wrapper filter. That catches regressions in our adapters; it does not prove that the next upstream release of a CLI behaves the same. We track that gap openly.

What it does not solve

  • Isolation between users. Rooms organise work; they are not a security boundary between mutually untrusted users. The Unix user is the outer trust boundary, so one install should serve one trust domain.
  • Your provider's view. Self-hosting keeps the workspace and its data on your host. The model providers you configure still receive what the agents send them.

Try it

SpaceApp is open source (MIT) and installs with:

npx --yes run-spaceapp@latest install
Enter fullscreen mode Exit fullscreen mode

Architecture: https://github.com/oll4com/spaceapp/blob/main/docs/architecture.md · Known limitations: docs/quality/KNOWN_LIMITATIONS.md in the repository.

If you run several coding agents on one codebase, I would like to hear how you keep their tools and sessions in order.

Top comments (0)