npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@synadia-ai/nats-claude-code-headless

v0.5.4

Published

Headless NATS agent host that spawns, prompts, and stops Claude Code sessions as first-class Synadia Agent Protocol for NATS v0.3 instances. Each session registers as agents.prompt.cc-headless.<owner>.<session_id>; a small controller service adds verb-fir

Readme

claude-code-headless

A headless NATS agent host that spawns Claude Code sessions on demand via the Claude Agent SDK and exposes each one as a first-class Synadia Agent Protocol for NATS v0.3 instance (verb-first subjects + status endpoint). Built on @synadia-ai/agents (caller-side primitives) and @synadia-ai/agent-service (host-side ReferenceAgent).

Each spawned session registers as its own NATS agent under agents.prompt.cc-headless.<owner>.<session_id> — discoverable via $SRV.INFO.agents and promptable with any protocol-compliant client, including the @synadia-ai/agents SDK. A small controller service at agents.prompt.cc-headless.<owner>.<name> (default name = "control") adds request/reply endpoints for session lifecycle — spawn, stop, list — alongside the protocol-required prompt endpoint (which returns help text) and a status endpoint that replies with the same payload as a heartbeat.

In short: one process, many Claude Code sessions, all first-class NATS agents.

This example is the Claude Code analogue of examples/pi-headless/ and is wire-compatible with the same callers — including the examples/agent-web-ui/ browser workspace.

Status. Demo-quality reference. Streams text per-token, surfaces tool calls + results inline, asks for permission via protocol §7 query chunks, and tracks cost per session. See Features below for the wire shapes; Roadmap for what's still ahead.

Quickstart (run from npm)

The package ships a nats-claude-code-headless CLI binary, so the simplest way to try it is via npx — no clone, no build:

export ANTHROPIC_API_KEY=sk-...
NATS_CONTEXT=localhost npx @synadia-ai/nats-claude-code-headless
# or:
NATS_URL=nats://127.0.0.1:4222 npx @synadia-ai/nats-claude-code-headless
# or:
npx @synadia-ai/nats-claude-code-headless --context localhost

npx resolves the package, runs its bundled entry point under Node ≥ 20, and prints:

claude-code-headless: controller listening on agents.prompt.cc-headless.<you>.control
claude-code-headless: control endpoints — agents.spawn.cc-headless.<you>.control  …  agents.stop.…  agents.list.…

For a permanent install:

npm install -g @synadia-ai/nats-claude-code-headless
nats-claude-code-headless --context localhost

The Claude Code native binary is auto-detected via the Claude Agent SDK's resolution chain (--claude-code-path flag, env var, config file, which claude, then the SDK's bundled fallback). See Claude Code binary below for overrides.

Quickstart (run from a local clone)

When you're working on the SDK or this example itself:

# 1. Build both SDKs (workspace siblings, referenced via file:).
(cd ../../client-sdk/typescript && bun install && bun run build)
(cd ../../agent-sdk/typescript  && bun install && bun run build)

# 2. Run claude-code-headless against the local SDK source via bun.
cd ../../examples/claude-code-headless
bun install
export ANTHROPIC_API_KEY=sk-...
bun run start                                    # connects via $NATS_CONTEXT or NATS_URL

# 3. Spawn a session + prompt + stop, from another shell.
bun run scripts/spawn.ts \
  --cwd /tmp/cc-sandbox \
  --prompt "list the files here and tell me what you see" \
  --allowed-tools "Read,Glob,Grep" \
  --stop-after

See README-DEV.md at the repo root for a fuller walk-through of the build / install dance, including how to pick up SDK edits without rebooting everything.

Configuration

Either a NATS context or an explicit URL:

NATS_CONTEXT=localhost bun run start
# or
NATS_URL=nats://127.0.0.1:4222 bun run start
# or
bun run start --context localhost

Optional defaults live in ~/.claude-code-headless/config.json:

{
  "context": "localhost",
  "name": "control",
  "defaultModel": "claude-sonnet-4-6",
  "defaultPermissionMode": "dontAsk",
  "defaultAllowedTools": ["Read", "Glob", "Grep"],
  "defaultMaxTurns": 50,
  "defaultMaxLifetimeS": 1800,
  "claudeCodePath": "/home/you/.local/bin/claude"
}

Env overrides:

| Variable | Overrides | Default | | --- | --- | --- | | CLAUDE_CODE_HEADLESS_OWNER | Owner subject token (3rd segment) | $USER | | CLAUDE_CODE_HEADLESS_NAME | Controller instance name (4th token) | control | | CLAUDE_CODE_HEADLESS_DEFAULT_MODEL | Default Claude model id for spawns | claude-sonnet-4-6 | | CLAUDE_CODE_HEADLESS_DEFAULT_PERMISSION_MODE | Default permission mode for spawns | dontAsk | | CLAUDE_CODE_HEADLESS_DEFAULT_ALLOWED_TOOLS | Default tool allowlist (comma-separated) | Read,Glob,Grep | | CLAUDE_CODE_HEADLESS_DEFAULT_MAX_TURNS | Safety cap on turns per prompt | 50 | | CLAUDE_CODE_HEADLESS_DEFAULT_MAX_LIFETIME | Default session lifetime, in seconds | 1800 | | CLAUDE_CODE_HEADLESS_CLAUDE_PATH | Path to the claude executable | (auto-detected via which claude) |

Precedence (high → low): CLI flags → env vars → ~/.claude-code-headless/config.json → built-in defaults.

CLI flag: --claude-code-path /abs/path/to/claude (alias --claude-path).

Claude Code binary

The Claude Agent SDK ships per-platform native binaries via optional npm packages, but auto-detection sometimes picks a variant that isn't installed (e.g. linux-x64-musl on glibc machines). When that happens you'll see Claude Code native binary not found at .../claude-agent-sdk-<platform>/claude on the first spawn.

This entry point side-steps that by resolving an explicit path on startup, in priority order:

  1. --claude-code-path CLI flag
  2. CLAUDE_CODE_HEADLESS_CLAUDE_PATH env var
  3. claudeCodePath field in ~/.claude-code-headless/config.json
  4. Auto-detected via which claude on PATH (typically the binary installed by the official Claude Code installer at ~/.local/bin/claude)
  5. None of the above → the SDK falls back to its bundled native binary, which is where the failure mode above shows up

The active path is logged at startup. If you need to override it, the CLI flag is the quickest route.

Anthropic auth

Set ANTHROPIC_API_KEY in env before launching. The SDK uses the API key in preference to any ~/.claude/ credentials when both are present.

Bedrock / Vertex / Azure deployments work too — set the SDK's standard provider env vars (CLAUDE_CODE_USE_BEDROCK=1 etc) and they take precedence.

Defaults rationale

The out-of-the-box defaults are deliberately conservative for a public reference:

| Setting | Default | Why | | --- | --- | --- | | permission_mode | dontAsk | Deterministic, headless-friendly. Never hangs on a permission prompt. Anything not in allowed_tools is denied. | | allowed_tools | ["Read", "Glob", "Grep"] | Read-only out of the box. Callers expand per-spawn for write/edit/bash. | | model | claude-sonnet-4-6 | Right cost/quality balance for a per-request spawner. | | max_turns | 50 | Safety cap — runaway loops fail visibly instead of silently. | | max_lifetime_s | 1800 | Sessions auto-expire after 30 min unless overridden. |

Subject layout

Verb-first throughout — protocol verbs and claude-code-headless extension verbs share the same agents.<verb>.cc-headless.<owner>.<token> shape, so a tracer or audit layer can subscribe to agents.<verb>.> and parse identity positionally.

agents.prompt.cc-headless.<owner>.<name>      ← controller prompt endpoint (help text)
agents.status.cc-headless.<owner>.<name>      ← controller status (replies with heartbeat-shaped payload)
agents.hb.cc-headless.<owner>.<name>          ← controller heartbeat (30 s)
agents.spawn.cc-headless.<owner>.<name>       ← POST JSON → session descriptor
agents.stop.cc-headless.<owner>.<name>        ← POST { session_id } → { ok: true }
agents.list.cc-headless.<owner>.<name>        ← (empty) → { sessions: [...] }

agents.prompt.cc-headless.<owner>.<session_id>  ← spawned session prompt
agents.status.cc-headless.<owner>.<session_id>  ← spawned session status
agents.hb.cc-headless.<owner>.<session_id>      ← spawned session heartbeat (30 s)

The cc-headless token disambiguates this controller (and its spawned sessions) from the regular Claude Code agent at agents/claude-code/, which uses the cc token for the opposite direction (Claude Code as MCP-driven NATS client).

Wire examples

Spawn

nats req agents.spawn.cc-headless.$USER.control \
  '{"cwd":"/tmp/cc-sandbox","model":"claude-sonnet-4-6","allowed_tools":["Read","Glob","Grep","Edit"],"permission_mode":"acceptEdits","max_lifetime_s":900}' \
  --timeout=15s
# → { "session_id":"sess-a1b2c3d4", "subject":"agents.prompt.cc-headless.$USER.sess-a1b2c3d4", "status_subject":"agents.status.cc-headless.$USER.sess-a1b2c3d4", ... }

Prompt (protocol-standard — no custom format)

nats req agents.prompt.cc-headless.$USER.sess-a1b2c3d4 \
  'list the files here and summarise what you see' \
  --replies=0 --reply-timeout=30s --timeout=120s
# → {"type":"status","data":"ack"}
# → {"type":"response","data":"There are three files: …"}
# → (empty terminator)

--reply-timeout=30s is important: the default 300 ms is shorter than the gap between the immediate ack chunk (sent on request receipt, before the Anthropic API call) and the LLM's first response, so nats req exits after the ack alone. SDK callers (requestMany with strategy:"sentinel") wait the full maxWait regardless of inter-arrival gaps and don't need this flag.

Programmatically with the SDK:

import { connect } from "@nats-io/transport-node";
import { Agents } from "@synadia-ai/agents";

const nc = await connect({ servers: "nats://localhost:4222" });
const agents = new Agents({ nc });

const all = await agents.discover();
const session = all.find((a) => a.name === "sess-a1b2c3d4")!;
for await (const ev of await session.prompt("summarise the files here")) {
  if (ev.type === "response") process.stdout.write(ev.text);
}

await agents.close();
await nc.close();

Stop

nats req agents.stop.cc-headless.$USER.control '{"session_id":"sess-a1b2c3d4"}'
# → { "ok": true, "session_id":"sess-a1b2c3d4" }

List

nats req agents.list.cc-headless.$USER.control ''
# → { "sessions": [ { "session_id":"sess-a1b2c3d4", "cwd":"/tmp/cc-sandbox", "remaining_lifetime_s": 867, ... } ] }

Errors

Custom endpoints respond with NATS micro-service error headers (Nats-Service-Error-Code / Nats-Service-Error):

| Code | When | |------|-----------------------------------------------------------------------------------------------| | 400 | Bad JSON, missing/invalid cwd, bad session_id, unknown permission_mode, bad allowed_tools | | 404 | stop for an unknown session | | 500 | Claude Agent SDK threw during a prompt turn |

Session prompt endpoints follow protocol §9.

CLI helpers

  • bun run scripts/spawn.ts --cwd /path [--prompt …] [--stop-after] — end-to-end smoke test.
  • bun run scripts/list.ts — print active sessions from every reachable controller.
  • bun run scripts/stop.ts SESSION_ID — dispose a session.

Notes

  • Session identity. The 4th subject token is the session id; metadata.session echoes it. Controllers use name = "control" by default and sessions carry metadata.role = "session".
  • Metadata marker. The controller carries metadata.role = "controller" so clients can tell it apart from sessions. The shared agent: "cc-headless" token already disambiguates this from the regular agent: "cc" (Claude Code as MCP-driven NATS client).
  • Multiple controllers per host. On startup the controller probes $SRV.INFO.agents and, if its target prompt subject is already claimed, picks the next free <name>-2, <name>-3, … suffix automatically. So booting a second claude-code-headless with default settings leaves the first as control and the second as control-2 without explicit --name flags. (For deterministic naming or two stable controllers side-by-side, still pass --name explicitly.)
  • Serial drain per session. Per session, prompts are queued and processed one at a time; the Claude Agent SDK's query() is one full multi-turn round-trip per call, and concurrent re-entry into the same session would interleave context.
  • Session resumption. Each prompt after the first is sent to the SDK with resume: <sdkSessionId> so context carries forward within a session for its full lifetime.
  • Lifetime & pruning. max_lifetime_s bounds a session's wall-clock life; pending requests older than 30 min are evicted (active requests are never evicted).
  • Attachments. Base64 attachments are decoded to ~/.claude-code-headless/attachments/<session_id>/<uuid>/ and their absolute paths are prepended to the prompt text, matching the staging pattern used by agents/pi/ and examples/pi-headless/.
  • Tool-call payload sizes. Tool result outputs are truncated to 4 KB before encoding into a status chunk to stay well under max_payload. The truncation marker is …[truncated].
  • Permission timeout. A pending §7 permission query is denied after 2 minutes of caller silence, so a vanished UI doesn't park a session forever.

Features

What this example showcases beyond the bare wire-compat proof:

Per-token streaming

The SDK's includePartialMessages option is on — text deltas arrive as they're generated and are forwarded as individual response chunks. Web-UI clients see Claude "type" in real time instead of a wall of text per turn. No knob, no opt-in; it's the default.

Tool-call observability

Every tool_use block Claude emits is forwarded as a status chunk with a prefix-tagged JSON payload:

{"type": "status", "data": "tool_use:{\"id\":\"...\",\"name\":\"Bash\",\"input\":{\"command\":\"ls /tmp\"}}"}

Tool results come back the same way:

{"type": "status", "data": "tool_result:{\"tool_use_id\":\"...\",\"output\":\"file1\\nfile2\",\"is_error\":false}"}

Encoding inside status.data keeps the wire spec-compliant (§6.4 says data is a string). SDK callers that don't recognize the prefix just see opaque status tokens; the agent-web-ui bridge translates them into typed events and renders them as collapsible tool cards.

Interactive permissions via §7 queries

When the spawn spec uses permission_mode: "default" (or any mode other than dontAsk/bypassPermissions), the SDK calls our canUseTool callback for each tool not already in allowed_tools. We turn each call into a protocol §7 query chunk:

{"type": "query", "data": {"id": "...", "reply_subject": "...", "prompt": "Claude wants to use tool: Bash\n{\"command\":\"ls /tmp\"}\n\nReply 'yes' to allow or 'no' to deny."}}

Then we wait (up to 2 minutes) for a single reply on reply_subject. The reply text is normalised: yes/y/allow/approve/ok → allow, no/n/deny/reject/cancel → deny, anything else → deny with the reply preserved as the reason. The SDK proceeds (or doesn't) based on what the user answered.

Cost tracking

Each successful turn emits a final cost status chunk with the per-turn and cumulative costs:

{"type": "status", "data": "cost:{\"turn_cost_usd\":0.0123,\"total_cost_usd\":0.0456}"}

Both values flow into the session summary (total_cost_usd, turn_count) and the UI surfaces them on each agent bubble (per-turn) and the session-list card (running total).

Roadmap

What's still ahead, roughly in priority order:

  1. Programmatic MCP server attachment through the spawn spec (mcp_servers field) — bring your own tools per session.
  2. System prompt / append-system-prompt override per spawn — useful for personas (code reviewer, test writer, security auditor, …).
  3. "Always allow" stickiness for permissions — the SDK exposes updatedPermissions on the canUseTool result; we currently ignore it. Wiring it would let users approve once per tool/per session.
  4. Tool result truncation policy — we cap result output at 4 KB to keep payloads manageable; for large outputs (file dumps, build logs) a streaming or paginated approach would be nicer than …[truncated].
  5. Session-file cleanup — periodic pruning of the SDK's on-disk session store so a long-running host doesn't accumulate state forever.
  6. Bedrock / Vertex / Azure auth ergonomics — works today via the SDK's standard env vars but undocumented here; could surface in config + status badge.