Spawn Claude Code, Codex, and Gemini CLI from your Node app. Typed events, MCP forwarding, and permission setup included.
Powering OpenWar and counting.
If you've spawned claude, codex, or gemini from a Node app, you know what you signed up for. Windows .cmd shim quirks. Hand-parsing stdout. Writing MCP config files to disk. Getting Claude Code to actually let your MCP tools through its permission gate. Then doing it all again the next time you add a CLI to the mix.
Squire is the runtime layer that handles that. Give it a binary and a prompt; you get back a typed event stream, MCP forwarding that works, and permission setup that doesn't need babysitting. Spawn one agent or bridge several from the same Node process. Cross-platform, zero runtime dependencies, MIT.
It's a tool, not a framework. Squire doesn't have opinions about how you structure your agent loop, what you log, or which CLI is "best." It hands you the primitives and stays out of the way.
Bonus most people miss: the CLIs Squire wraps auth through your subscription, not your API key. Claude Code uses your Claude Code OAuth. Gemini CLI uses your Google AI account. Squire lets your Node app inherit that subscription auth instead of burning API tokens at retail. The $20/mo seat your team already pays for can now power the apps you're building.
npm install @pythonluvr/squireimport { Squire } from '@pythonluvr/squire'
const squire = new Squire({
binary: 'claude',
args: ['--permission-mode', 'bypassPermissions'],
cwd: process.cwd(),
})
squire.on('stdout', (chunk) => process.stdout.write(chunk))
squire.on('event', (event) => {
if (event.type === 'message_stop') console.log('\nexit code:', event.code)
})
await squire.start('Hello, agent.')- Subprocess spawn. Cross-platform
child_processwrapper with Windows.cmd/.bat/ extensionless-binary handling baked in. Works the same way on macOS and Linux. - Structured event streaming. A typed
SquireEventunion (stdout,stderr,text_delta,tool_call,tool_result,thinking_delta,usage,message_start,message_stop,error) replaces ad-hoc stdio parsing. v1.1 ships dedicated parsers for Claude Code (adapter: 'claude-code') and Gemini CLI (adapter: 'gemini-cli') that emit semantic events; the built-intext-streamadapter remains the default fallback for any other CLI. - MCP forwarding. Pass
mcp.serversor a pre-builtmcp.configPathand Squire wires the child's--mcp-configflag for you. The temp config file is cleaned up onstop(). - Permission auto-setup. For Claude Code,
autoSetup.claudeCodemergesallowedToolspatterns into~/.claude/settings.jsonatomically (preserving everything else in the file). Idempotent.
v1.0 is binary-agnostic. Any CLI that accepts a prompt on stdin and emits output on stdout will work with the built-in text-stream adapter.
| CLI | Notes |
|---|---|
| Claude Code | binary: 'claude', adapter: 'claude-code'. Pair with autoSetup.claudeCode for MCP-tool permissions. Emits text_delta, thinking_delta, tool_call, tool_result, usage. |
| Gemini CLI | binary: 'gemini', adapter: 'gemini-cli'. Honors --mcp-config. Emits text_delta, tool_call, tool_result, usage. |
| OpenAI Codex CLI | binary: 'codex'. Honors --mcp-config. Use the default text-stream adapter; a dedicated codex adapter is planned for a follow-up release. |
| Custom | Any binary on PATH or absolute path. Use text-stream or register your own SquireAdapter. |
The dedicated adapters parse each vendor's stream-json output line-by-line and fall back to raw stdout events for any line they cannot interpret, so a vendor format tweak degrades gracefully instead of crashing. The SquireAdapter interface is exported for callers who want to ship custom parsers.
| Platform | Status |
|---|---|
| Linux | First-class. |
| macOS | First-class. |
| Windows | First-class. Handles .cmd / .bat shims (needed for npm-installed CLIs) and PATHEXT walk for extensionless binaries via shell: true auto-detection. |
The Windows logic is in src/spawn.ts. It's deliberately scoped: shell: true only when the binary needs it, so paths containing spaces (the C:\Program Files\... case) keep working.
| Option | Type | Default | Notes |
|---|---|---|---|
binary |
string |
required | Path or PATH-resolvable binary name. |
args |
string[] |
[] |
Default args prepended before per-call args. |
cwd |
string |
process.cwd() |
Working directory for the child. |
env |
Record<string,string> |
{} |
Merged on top of process.env. |
timeoutMs |
number |
600000 |
Hard timeout per start(). 0 = unlimited. |
shell |
boolean |
auto | Force shell mode; otherwise auto-detected per platform. |
mcp |
SquireMcpOptions |
off | See "MCP forwarding" below. |
autoSetup |
SquireAutoSetupOptions |
off | See "Permission auto-setup" below. |
adapter |
string |
'text-stream' |
Name of a registered SquireAdapter. |
start(prompt: string, opts?: { signal?: AbortSignal, extraArgs?: string[] }): Promise<void>: spawn the child, pipe the prompt to stdin, stream events until exit. Resolves when the child closes (cleanly or otherwise).send(followup: string): Promise<void>: write more input to the child's stdin. Throws if the child is dead or stdin was closed.stop({ graceful?: boolean }): Promise<void>: SIGTERM then SIGKILL after 3 seconds (default);{ graceful: false }sends SIGKILL immediately.pid: number | null: child PID once spawned.
squire.on('stdout', (chunk: string) => { /* raw */ })
squire.on('stderr', (chunk: string) => { /* raw */ })
squire.on('event', (event: SquireEvent) => { /* discriminated union, see events.ts */ })
squire.on('exit', (code: number | null) => { /* terminal */ })SquireEvent is a discriminated union; narrow on event.type. The v1.0 type union is documented in src/events.ts.
All thrown errors are SquireError (or SquireAutoSetupError for permission-file failures). Narrow on err.code:
| Code | Meaning |
|---|---|
INVALID_OPTIONS |
Constructor or registerSquireAdapter rejected the input. |
ALREADY_STARTED |
start() called twice on the same instance. |
NOT_STARTED |
send() called before start(). |
SPAWN_FAILED |
Child failed to spawn (ENOENT, EACCES, etc). |
TIMEOUT |
timeoutMs exceeded. |
NON_ZERO_EXIT |
Child exited with a non-zero status code. |
ADAPTER_UNSUPPORTED_FEATURE |
Adapter or transport doesn't support the requested operation. |
ADAPTER_PARSE |
Adapter failed to parse the child's output (reserved for v1.x adapters). |
AUTOSETUP_READ / AUTOSETUP_PARSE / AUTOSETUP_WRITE |
Claude Code settings merge failed; see .path. |
MCP_CONFIG_WRITE |
Could not write the temp MCP config file. |
new Squire({
binary: 'claude',
mcp: {
servers: {
myserver: { command: 'node', args: ['./mcp-server.js'] },
},
allowList: ['mcp__myserver__*'],
},
})If you already have an MCP config file, pass mcp.configPath instead and Squire will skip the temp-file write.
The flag defaults to --mcp-config (matches Claude Code, Codex, Gemini CLI). Override with mcp.configFlag for custom CLIs.
new Squire({
binary: 'claude',
mcp: { /* ... */ allowList: ['mcp__myserver__*'] },
autoSetup: {
claudeCode: { writeSettings: true },
},
})Claude Code treats external MCP tools as separate-trust by design. Without this step, the child halts at its own permission gate on the first MCP tool call. Squire merges the allowList patterns into ~/.claude/settings.json atomically, preserving everything else in the file.
Falls back to ~/.claude/settings.json by default; override with autoSetup.claudeCode.settingsPath.
import { registerSquireAdapter, type SquireAdapter } from '@pythonluvr/squire'
const myAdapter: SquireAdapter = {
name: 'codex-json',
create(ctx) {
return {
onStdout(chunk) { /* parse, return SquireEvent[] */ return [] },
onStderr(chunk) { return [{ type: 'stderr', chunk }] },
}
},
}
registerSquireAdapter(myAdapter)
new Squire({ binary: 'codex', adapter: 'codex-json' })Two runnable example apps live in examples/:
examples/minimal: the smallest possible Squire app. Spawns Claude Code, sends one prompt, prints streamed text deltas. Start here.examples/bridge-multi-cli: spawns Claude Code and Gemini CLI sequentially in the same Node process and pipes the first reply into the second prompt. Shows the multi-CLI bridging differentiator end to end.
Each example is standalone with its own package.json; cd into the directory and run npm install && npm start.
- OpenWar is a phase-gated agent runtime. It uses Squire under the hood for its
cli-bridgeadapter, layering a phase machine, deterministic detectors, and brief/trace persistence on top.
If you're using Squire in a project, send a PR adding a line here.
MIT. See LICENSE.
PRs welcome. See docs/contributing.md for the basics. The public API is frozen at v1.0.0: additive changes only on the v1.x line; breaking changes wait for v2.0.
