@mulmuri/claude-agent-sdk
v0.1.0
Published
Claude Agent SDK-compatible client that reaches Claude Code through Codex CLI (interactive `claude "..."`) or the Claude Code MCP server — never through `claude -p`.
Maintainers
Readme
@mulmuri/claude-agent-sdk
A Claude Agent SDK-compatible
client that never calls claude -p. Instead of driving the Claude Code CLI
directly, it reaches Claude through one of two user-selectable transports:
| Backend | Chain | How it works |
| --- | --- | --- |
| "interactive" | SDK → Codex CLI → claude "..." | Spawns codex exec, which is instructed to run the Claude Code CLI interactively (claude "...", stdin closed — the session answers once and exits) and relay the reply verbatim. |
| "mcp" (default) | SDK → claude mcp serve (MCP) | Spawns Claude Code as an MCP server and drives it as an MCP client. Primary path: the server's Agent tool. Current Claude Code builds expose an empty agent registry in serve mode, so the SDK automatically falls back to the server's Workflow tool (whose agent() primitive does work), reads the run's result from its state file, and tails the agent transcript to stream real assistant messages. |
The public interface mirrors query() from @anthropic-ai/claude-agent-sdk:
an async generator of SDK-shaped messages (system:init → assistant/user →
result) with an interrupt() method.
Requirements
- Node.js ≥ 18
- Claude Code CLI installed and authenticated (both backends)
- Codex CLI installed and authenticated (
"interactive"backend only)
No API keys are read by this package — authentication is whatever the CLIs already have.
Install
npm install @mulmuri/claude-agent-sdkUsage
import { query } from "@mulmuri/claude-agent-sdk";
for await (const message of query({
prompt: "Summarize this repository",
options: {
backend: "mcp", // or "interactive" — the user's choice
model: "sonnet",
cwd: "/path/to/project",
},
})) {
switch (message.type) {
case "assistant":
for (const block of message.message.content) {
if (block.type === "text") console.log(block.text);
}
break;
case "result":
if (message.is_error) console.error("failed:", message.result);
else console.log("final:", message.result);
break;
}
}The backend can also be selected without code changes via the
CLAUDE_AGENT_BACKEND environment variable (interactive or mcp);
options.backend wins when both are set.
Interrupting a running query:
const q = query({ prompt: "long task...", options: { backend: "interactive" } });
setTimeout(() => q.interrupt(), 10_000);
for await (const message of q) { /* ... */ }Options
| Option | Backends | Description |
| --- | --- | --- |
| backend | — | "interactive" | "mcp". Default: CLAUDE_AGENT_BACKEND env var, then "mcp". |
| model | both | Forwarded to claude --model (interactive) or mapped to the Agent tool's sonnet/opus/haiku/fable short names (mcp). |
| cwd | both | Working directory for the spawned CLI processes. |
| systemPrompt | both | Interactive: passed via claude --append-system-prompt. MCP: prepended to the prompt inside <system-instructions> tags (best effort — the Agent tool has no system-prompt parameter). |
| permissionMode | both | "default" | "acceptEdits" | "bypassPermissions" | "plan". Forwarded to the Agent tool's mode on MCP. |
| abortController | both | Aborting kills the underlying CLI processes. |
| timeoutMs | both | Overall wall-clock timeout for the query. |
| stderr | both | Callback receiving stderr from the spawned processes. |
| pathToClaudeExecutable | both | Default claude on PATH. |
| pathToCodexExecutable | interactive | Default codex on PATH. |
| codexSandbox | interactive | Codex sandbox policy. Default danger-full-access — Codex must be able to spawn claude, which needs network and credential access, so stricter sandboxes typically break the relay. |
| allowedTools, disallowedTools, maxTurns | — | Accepted for interface compatibility, not enforced (see below). |
What you get in the message stream
system(subtypeinit) — session id, cwd, tool names (the MCP server's real tool list, or["Bash"]for the Codex relay), model, permission mode.assistant— on the interactive backend these stream live from Codex's JSONL events: text messages, plustool_useblocks for every shell command Codex runs (you can watch it invokeclaude "..."). On the MCP backend a single assistant message carries Claude's reply.user—tool_resultblocks paired with the relay'stool_useblocks (interactive backend only).result—subtype: "success"with the final reply inresult, orsubtype: "error_during_execution"with diagnostic output.
Compatibility notes (intentional gaps)
The Claude Agent SDK and the Claude Code CLI surfaces are not 1:1, so some SDK features cannot be honored over these transports:
total_cost_usdis always0andusagereflects the relay's tokens on the interactive backend (Codex's usage), zeros on MCP — Claude's own token counts are not observable without--print-mode JSON output, which this package deliberately does not use.allowedTools/disallowedTools/maxTurnsare accepted but not enforced; neither transport exposes those controls.- Single-shot prompts only —
promptis a string. Multi-turn streaming input (AsyncIterable<SDKUserMessage>) and session resumption are not supported; eachquery()is a fresh session. - Hooks, custom MCP servers, canUseTool callbacks — not supported.
- The interactive backend's reply fidelity depends on the Codex relay; the SDK reads Claude's reply from a file Claude itself wrote (not from Codex's paraphrase) whenever possible.
- Model selection on the MCP backend is best effort. The SDK forwards it
through
ANTHROPIC_MODELand the workflow'sagent()options, but if the machine's~/.claude/settings.jsonpins a model viaenv.ANTHROPIC_MODEL/env.CLAUDE_CODE_SUBAGENT_MODEL, those settings win inside Claude Code. On the interactive backend the flag is passed asclaude --model, which wins.
How the interactive backend works (the fine print)
The SDK writes your prompt (and optional system prompt) to temp files.
It spawns
codex exec --json --ephemeralwith an instruction to run, exactly:claude [--model M] [--append-system-prompt "$(cat sys.txt)"] "$(cat prompt.txt)" </dev/null >out.txt 2>err.txtNote: no
-p/--print. With a prompt argument and stdin closed, the interactive CLI answers once and exits on its own. If a TTY turns out to be required, Codex is instructed to retry underscript -q /dev/null.Codex's JSONL events are mapped to SDK
assistant/usermessages in real time, so you can observe the relay working.When Codex exits, the SDK reads
out.txt(Claude's verbatim reply, ANSI stripped) as the canonicalresult, falling back to Codex's final message.
Development
npm install
npm run build
node examples/mcp.mjs # MCP backend smoke test
node examples/interactive.mjs # Codex-relay backend smoke testTesting
The test suite runs against the real CLIs and real models — no mocks:
npm test # everything: build + type-level check + unit + E2E
npm run test:unit # fast: queue/parser/command-builder unit tests
npm run test:e2e # real `claude mcp serve` + real `codex exec` round trips
npm run test:types # consumer-style code must type-check against dist types
SKIP_E2E=1 npm test # CI without CLI credentials: skips live-model testsThe E2E tests assert the full message protocol (init first, one terminal
result, consistent session ids, unique uuids), the actual answer content,
system-prompt steering, error results for missing executables, interrupt and
abort semantics, timeouts — and, on the interactive backend, that the relay's
observed shell command really invoked claude without -p/--print.
Expect the E2E tier to take a few minutes and consume a small number of
real model calls (haiku where possible).
Publishing
npm publish --access publicprepublishOnly rebuilds dist/; only dist/, README.md, and LICENSE
are shipped.
License
MIT
