{"slug": "lessons-from-putting-14-coding-agent-clis-behind-one-interface", "title": "Lessons from putting 14 coding-agent CLIs behind one interface", "summary": "A developer built a desktop app that unifies 14 coding-agent CLIs — including Claude Code, Codex, Gemini CLI, Cursor and OpenCode — behind a single interface, letting users switch frameworks mid-conversation. Twelve of the 14 speak the Agent Client Protocol, while Claude Code runs in-process via its SDK and Codex through its own JSON-RPC server, with the app enforcing 32 capability flags through bidirectional tests. In 630 live runs across five framework/model pairs, listing the skill index in the prompt raised correct skill loading from 47–63% to 97–100%, with no wrong skill loaded in the 180 runs that required none.", "body_md": "We build a desktop app where one chat can be driven by any of 14 agent CLIs: Claude Code, Codex, OpenCode, Gemini\n\nCLI, Kimi Code, Qwen Code, MiniMax Code, CodeBuddy, Grok Build, Cursor, Hermes Agent, OpenClaw, NanoClaw and\n\nAntigravity CLI. You can switch the framework partway through a conversation and keep going. Getting there taught us\n\nmore about the differences between these tools than about what they share. Here is what held up.\n\nTwelve of the fourteen speak the [Agent Client Protocol](https://agentclientprotocol.com) (ACP), so they share one\n\nbackend. Claude Code runs through its SDK, in-process. Codex runs through `codex app-server`, its own JSON-RPC\n\nserver, which has a lovely property: `codex app-server generate-json-schema` prints its entire interface (at 0.149:\n\n83 client methods, 10 server requests, 65 notifications). We wrote that backend against the generated schema\n\ninstead of guessing.\n\nWhy not put everything on ACP? Because a shared protocol only covers what everyone has. Route Claude Code through it\n\nand constrained structured output becomes \"ask for JSON and retry until it validates\". Route Codex through it and a\n\nreal compaction call becomes a slash command. Our rule: **if standardising costs the strongest framework a capability it has today, the layer is wrong.** The layer should describe the differences, not remove them.\n\nEvery framework is one object behind one interface: eight required operations (`sendMessage`, `interrupt`,\n\n`listSessions`, `getMessages`, `setConfig`, `reclaim`, plus `id` and `capabilities`) and optional ones like\n\n`forkSession` or `respondToPermission`.\n\nEach optional method is tied to one of 32 capability flags, and a test checks the pairing in **both** directions:\n\ndeclared means implemented, and undeclared means absent. The second direction is the one people skip, and it's what\n\nmakes \"what the app believes a framework can do\" equal \"what it does\". The catalog entry is typed so every flag\n\nneeds an explicit answer — add a 33rd flag and the build fails until all 14 frameworks answer it.\n\nA tick is a claim about *our* implementation, so we also record how each capability is reached. For Claude Code all\n\n32 are native. For Gemini CLI 14 are native, 8 are the app's by design (things no framework will ship, like a\n\nscheduler), and 10 are stand-ins until the framework ships its own — each stand-in records where to look for the\n\nreal one.\n\nWe tested this live: 22 skills, 14 requests (10 that need a skill, 4 that must not load one), in English, Romanian\n\nand Russian, on five framework/model pairs — 630 runs. With search only, the right skill loaded 47–63% of the time.\n\nWith the skill index listed in the prompt: 97–100%, and no wrong skill in any of the 180 runs that should have loaded\n\nnone.\n\nThe instructions, skill index and tool index are built once when the session starts and never rewritten. A prefix\n\nthat never changes gets cached: on Claude Code, turn one wrote 45,021 tokens to the prompt cache; turn two read all\n\n45,021 back and wrote 75 new ones. If something changes mid-session (a skill installed), the next turn carries a\n\none-line note instead of a rewritten prompt.\n\nACP has no method for subagents, background tasks or forms; other protocols have different gaps. Building each\n\ncapability once per protocol means 14 implementations of everything. Instead the app runs a local\n\n[MCP](https://modelcontextprotocol.io) server and every framework connects to it. A tool is written once — name,\n\ndescription, schema, handler — and because the handler runs in the app's process, it can open a form and wait for\n\nthe answer, or start a second session as a subagent, with no protocol support at all.\n\nThe catch is naming. The same tool reaches each model under a different name, with different limits:\n\n| Framework | `skill_load` as the model sees it | Limit that bites | \n|---|---|---|\n| Claude Code | `mcp__tesseract__skill_load` | descriptions cut at 2,048 chars | \n| Codex | `mcp__tesseract__` namespace +`skill_load` | names `[A-Za-z0-9_]` only; schemas over 5,000 bytes shrunk | \n| Gemini CLI | `mcp_tesseract_skill_load` | names over 63 chars mangled; `$schema` stripped | \n| OpenCode | `tesseract_skill_load` | forces `additionalProperties: false` | \n\nSo every tool meets the strictest common standard at build time: a lower-case `area_verb` name of at most 40\n\ncharacters, a description of at most 2,048, a schema of at most 5 KB with no root-level combinators.\n\nAsking OpenCode for an image with every tool handed over directly: 82 tools, about 43,600 tokens on every model call.\n\nWith a small always-loaded core (`skill_find`, `skill_load`, `tool_find`, `tool_call`, `tool_call_readonly`,\n\n`ask_user`, `plan_update`, `plan_propose`) and everything else behind `tool_find`: 19 tools, about 8,600 tokens, and\n\nthe picture still arrived.\n\nTwo dispatchers, not one, because frameworks run MCP calls without a read-only hint one at a time: three 4-second\n\ntools took 12 s without the hint and 4 s with it. But one dispatcher marked read-only would let a framework\n\nauto-approve tools that change things — so read-only tools go through `tool_call_readonly`, which refuses anything\n\noff its allowlist.\n\nMake the dispatcher forgiving: accept `tool`/` args` because that's what models send, parse `args` if it arrives as a\n\nJSON string, and answer a wrong shape with the schema. Across five framework/model pairs and 166 real tool\n\ndefinitions, the right tool with valid arguments came back 76 times out of 80.\n\nWe replace planning, questions, subagents, skills and tool search on every framework — but not the shell and the file\n\neditor. A harness is tuned with its model. Terminal-Bench 2.0 ([arXiv 2601.11868](https://arxiv.org/abs/2601.11868))\n\nran the same models in their vendors' harnesses and in a neutral one: one scored 62.9% in its own and 54.0% in the\n\nneutral one; another went the other way, 52.1% vs 57.8%. The edit dialect a model was trained on is worth more than\n\nidentical tooling.\n\nThe full write-up, with the event mapping and how history moves between frameworks, is on our blog:\n\n[https://tesseract.dev/blog/one-chat-fourteen-agent-frameworks/?utm_source=27a2vo](https://tesseract.dev/blog/one-chat-fourteen-agent-frameworks/?utm_source=27a2vo)", "url": "https://wpnews.pro/news/lessons-from-putting-14-coding-agent-clis-behind-one-interface", "canonical_source": "https://dev.to/tesseract_app/lessons-from-putting-14-coding-agent-clis-behind-one-interface-gch", "published_at": "2026-11-06 19:18:21+00:00", "updated_at": "2026-11-06 19:23:30.302735+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "agent-protocols", "large-language-models"], "entities": ["Claude Code", "Codex", "OpenCode", "Gemini CLI", "Cursor", "Agent Client Protocol", "Model Context Protocol"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/lessons-from-putting-14-coding-agent-clis-behind-one-interface", "markdown": "https://wpnews.pro/news/lessons-from-putting-14-coding-agent-clis-behind-one-interface.md", "text": "https://wpnews.pro/news/lessons-from-putting-14-coding-agent-clis-behind-one-interface.txt", "jsonld": "https://wpnews.pro/news/lessons-from-putting-14-coding-agent-clis-behind-one-interface.jsonld"}}