DEV Community

Nic Flemmer
Nic Flemmer

Posted on

Wiring seven coding agent CLIs into one app: four ways in, and what each can't do

Styx runs seven coding agent CLIs, plus a plain shell, on the same projects. There isn't one way to drive an agent from another program. There are four, and each has something it can't do. This is the map I wish I'd had before adding the last two.

The four ways in

1. Streaming JSON (Claude Code). The CLI runs headless and emits events as JSON lines. You read tool calls, permission requests and results as data and write messages back. It's the easiest to build a chat on.

2. An app server (Codex). Codex runs as codex app-server, and Styx talks to it over that protocol. The reason is blunt: Codex's terminal UI can't take messages through a pty, so driving it as a terminal isn't an option. A side effect is that you can message Codex mid-turn and it adjusts course at once.

3. ACP (Gemini CLI, Cursor, OpenCode, Goose). The Agent Client Protocol gives native chat, permission cards, models and MCP from a single runner. Cursor's agent can fall back to --print stream-json. Older Goose builds without ACP run in a terminal instead.

4. A terminal (Amp, and a plain shell). Amp's stream output has no documented way to answer an approval from outside. So its interactive UI runs in a pty, and you answer prompts in the task's Terminal tab.

A small router picks the runner for each agent. Everything above the router sees one kind of session.

What each way can't do

Way You get You don't get
Streaming JSON Native chat, tool calls as data Mid-turn messages are held until the turn ends
App server Native chat, messages mid-turn Nothing you'd miss, but only Codex speaks it
ACP Native chat, permission cards, models, MCP Depends on the agent's ACP support
Terminal The agent's full UI Native chat cards, live mode switches, background tasks

For Claude Code, Gemini CLI, Cursor's agent and OpenCode, a message sent mid-turn waits and goes out when the turn ends. Codex takes it straight away. The composer has a send-now button for the rest.

Defaults are not the same everywhere

Goose approves tool calls automatically unless you tell it otherwise, so Styx sets its mode per process: tool calls ask in the default, accept-edits and auto modes. Its chat-only mode, used for plan and don't-ask, disables all tools, reads included. Amp's plan and don't-ask modes likewise reject every tool, reads included. None of this touches the user's own agent config.

The lesson: read what a CLI does with a tool call by default before you wire up anything else.

Terminal details that cost time

A TUI isn't a chat. Multiline messages go in with bracketed paste, so a newline isn't taken for 'send'. The Terminal tab attaches the same process the app started, so the backlog, keyboard approvals and a relaunch of the app all keep working.

You can't always check a login offline

For Goose and Amp, the 'verify' step stays at 'not verified'. Amp has no offline way to ask who you are, and Goose's provider check spends a model turn. The honest state is to say so and still let you start a task.

Testing without the real thing

Every agent gets a fake CLI. The end-to-end tests put fake claude, codex and gemini binaries first on the PATH, so nothing talks to a real provider or a real account.

A checklist for the next one

  1. How does it take input mid-turn?
  2. Can an approval be answered from outside, or only in its terminal?
  3. What is its default permission mode, and can you set it per process?
  4. How do you ask who it's signed in as without spending tokens?
  5. Write the fake CLI before the runner.

Styx is open source (Apache-2.0), and there's a step-by-step guide for adding an agent at https://heystyx.com/docs/contributing/add-an-agent. Goose and Amp came in that way.

Top comments (0)