Why ActiveCLI
Wrapping a CLI is easy until you ship it. Then you find that the executable is not on the PATH your process inherited, that Windows is four different environments, that killing the child leaves its children running, and that a hard kill of your own process leaves the CLI alive and billing.
Those problems are the same for every CLI, and they are most of what this library is.
The pitfalls
Section titled “The pitfalls”| Problem | What it looks like in the wild |
|---|---|
| GUI processes inherit a minimal PATH | A CLI the user installed with brew or nvm is invisible; spawn fails with ENOENT |
where claude names the wrong file |
npm ships an extension-less wrapper beside claude.cmd; cmd.exe runs the latter |
| WSL UNC paths cannot be a cwd | \\wsl.localhost\... fails from Windows, and does not exist inside the distro |
shell: true on win32 splits on spaces |
A launcher under C:\Program Files\ tears in half; arguments become injectable |
cmd.exe expands %VAR% inside quotes |
An argument is silently rewritten before the CLI sees it |
cmd.exe ends the command at & |
JSON config arrives truncated; CI caught this on the first run |
| A capable terminal decorates output with ANSI escapes | \x1b[1 q lands on the same stream as the protocol and the JSON line fails to parse |
| Killing the child is not killing the tree | Subagent shells and background tasks keep running |
| git-bash workers detach from the tree | taskkill /T cannot see them; they survive as orphans |
| A hard SIGKILL runs none of your cleanup | The CLI outlives the host, invisible to any later one |
What CI caught that macOS never would
Section titled “What CI caught that macOS never would”One defect surfaced on the first Windows CI run that a macOS machine could not have revealed.
An argument passing through cmd.exe containing a&b|c<d>e was truncated at a, and cmd emitted 'b' is not recognized as an internal or external command. The received wisdom — that Node’s CommandLineToArgvW quoting renders & literal — is wrong, and the original comment in the code said so too.
The JSON that mcp add-json passes is full of exactly these characters. A Windows user’s MCP configuration was about to be silently truncated on the way to disk.
The remedy is to caret-escape & | < > and nothing else. A first attempt that also escaped parentheses broke ordinary arguments such as write("hi") — a caret is only meaningful in an unquoted context, and parentheses are only special inside block syntax.
What the survey found
Section titled “What the survey found”The library was written after a survey (2026-08-11) for projects meeting two conditions: that they wrap a CLI rather than an official SDK, and that the result is consumable as an SDK rather than as an application.
Every project meeting both conditions is small.
| Repository | Stars | State |
|---|---|---|
| jayminwest/overstory | 1,327 | Archived (2026-05-28) |
| Enderfga/claw-orchestrator | 545 | Largest living candidate |
| RobertTLange/headless-cli | 37 | Supports 8 CLIs; closest fit to both conditions |
| six-ddc/codex-dynamic-workflows | 16 | — |
The highly-starred projects all fail one condition or the other: pi (87,262) is a general-purpose toolkit, CLI-Anything (46,892) addresses a different axis, claude_codex_bridge (3,394) is a TUI, and myclaude (2,743) is a Go binary.
Wrapping a CLI and handing it to a person earns thousands of stars. Wrapping a CLI and handing it to a library consumer is, so far, uniformly small.
Nobody had established an AbstractAdapter
Section titled “Nobody had established an AbstractAdapter”The largest competing JetBrains plugin, zhukunpenglinyutong/jetbrains-cc-gui (5,315 stars), attaches six CLI channels under ai-bridge/channels/ — claude, codex, grok, kimi, opencode, pi — with no common contract between them.
| Channel | Parameters its send takes |
|---|---|
| Claude | sessionId, disableThinking, agentPrompt |
| Codex | threadId, baseUrl, apiKey, serviceTier |
It is the same verb with different parameters, and the command lists differ too — seven for Claude against three for Codex. The caller must know which provider it is addressing in order to build a payload. That is not integration; it is separate storage under one roof.
The cause is structural. The code is written as functions scattered across modules — export async function handleClaudeCommand(command, args, stdinData) — dispatched by a large switch, with state passed along as arguments. This is the concrete counter-example the design principles are aimed at, and evidence that a function-module approach leads to failed abstraction.
Relationship to ACP
Section titled “Relationship to ACP”Zed’s Agent Client Protocol has been built into JetBrains IDEs since 2025-12 and supports 25+ agents. ActiveCLI does not compete with it, for two reasons.
- ACP presumes a human sits at an editor and approves. That premise does not hold for a program driving a CLI unattended.
- ACP does not standardise the process-execution problem — locating the executable, Windows quoting, WSL paths. Its adopters carry those problems unchanged: Zed #56176 reports ACP servers failing to launch under WSL, the Kiro documentation offers “hard-code an absolute path” as the official remedy, and JetBrains AI Assistant does not support WSL at all.
ACP is therefore a candidate adapter, not a rival. headless-cli absorbed it the same way.