Skip to content

Programmatic API

Use claude-coder as a TypeScript library to drive Claude Code from your own code.

The package is ESM and requires Node >= 22. Everything public is exported from the package barrel (src/index.ts); drive the fleet in-process via FleetApi. Auth is always your Claude subscription OAuth — never an API key. If a billed credential is in force, start() fails closed. See security.

Note

Install with npm install @ahmed-hobeishy/claude-coder and import from @ahmed-hobeishy/claude-coder (the unscoped claude-coder npm name belongs to an unrelated project; the bin is still plain claude-coder). From a source checkout, npm install compiles dist/ via prepare.

The public surface

Grouped by what you reach for. Names are exact (src/index.ts).

Group Exports
Transports SdkTransport, SdkTransportOptions, PtyTransport, PtyTransportOptions
Event fan-out EventHub, EventHubOptions
Fleet / sessions SessionManager, SessionManagerOptions, CreateOptions, ManagedSession, ManagedSessionView, FleetEvent
Errors TransportError, TransportErrorKind
Capabilities Capabilities, SDK_CAPABILITIES, PTY_CAPABILITIES
Event policy COALESCEABLE_KINDS, UNDROPPABLE_KINDS, isCoalesceable, isUndroppable
ID helpers sessionId, turnId, sendId, toolUseId, newSessionId, newTurnId, newSendId
Key types Session, SessionOptions, SessionState, SessionEvent, TurnResult, SendHandle, PermissionMode, PermissionRequest

The claude-coder barrel also re-exports profile and secrets symbols. See profiles and secrets for those.

Advanced & lower-level exports

The barrel additionally re-exports the building blocks that SessionManager and FleetApi are composed from. Most callers reach for FleetApi/SessionManager instead and never construct these directly; they are exported for inspection, testing, and bespoke assembly. Read src/index.ts for the full list — the categories below name the seams, not every symbol.

Category What it is Representative exports
Catalog primitives On-disk session discovery + the live in-memory registry the manager reads through SessionCatalog, LiveSessionRegistry
Fleet / policy seam The admission, accounting, persistence, and locking pieces SessionManager wires together FleetPolicy, PolicyAccounting, AdmissionController, TransportProvider, FleetStore, OwnerLockRegistry

These are advanced; if you only want to drive sessions, stay on FleetApi (below) or a single Transport.

EventHub is the per-session fan-out primitive (publish / subscribe / close) that the transports use internally; callers rarely construct it directly. EventHubOptions has two knobs: bufferSize (per-subscriber soft cap for coalesceable text/raw events, default 1024) and replayDepth (how many latest undroppable control events a replay subscriber backfills, default 0). See concepts.

The core types

A Transport makes a Session. A Session runs turns and emits events.

interface Session {
  readonly id: SessionId;
  resumeKey?: SessionId; // PTY: captured async; use to resume
  get state(): SessionState; // 'starting'|'idle'|'busy'|'stalled'|'closed'|'error'
  readonly pendingPermissions: ReadonlyArray<PermissionRequest>; // permission is data, not a state
  get awaitingPermission(): boolean; // true while any pendingPermissions are outstanding
  events(opts?: { replay?: boolean; signal?: AbortSignal }): AsyncIterable<SessionEvent>;
  send(input: string): SendHandle; // non-blocking; returns a handle
  turn(input: string): Promise<TurnResult>; // blocks until the turn boundary
  interrupt(opts?: { drainQueue?: boolean }): Promise<{ interrupted: boolean }>;
  setPermissionMode(mode: PermissionMode): void;
  close(opts?: { graceful?: boolean; timeoutMs?: number }): Promise<void>;
}

SessionEvent is one normalized union (kinds: state, text, turn_complete, tool_use, tool_result, permission_request, question_request, usage, limit, exit, error, raw). The content events (text, turn_complete, tool_use, tool_result, question_request) each carry a fidelity tag: 'structured' (SDK) or 'inferred' (PTY). The other kinds do not. See concepts.

Warning

text is not an append-only delta. Do not blindly concatenate text events — render the latest, or rely on turn_complete.result.text for the final answer.

Minimal SDK example

A complete, runnable program: construct a transport, start a session, run one turn while consuming events, then close. This is the verified shape — it type-checks against the published declarations.

import { SdkTransport, TransportError } from '@ahmed-hobeishy/claude-coder';

async function main() {
  // 1. Construct the SDK transport. No options are required; defaults use the
  //    real SDK. `pinBaseUrl` is the one caller knob (pins ANTHROPIC_BASE_URL).
  const transport = new SdkTransport();

  // 2. Start a session. This resolves only AFTER backend init + the auth gate pass.
  //    It fails closed: if the session is on an explicit API key (not subscription
  //    OAuth), start() rejects with TransportError{kind:'auth'} and never runs a turn.
  let session;
  try {
    session = await transport.start({
      cwd: process.cwd(),
      start: { kind: 'new' },
    });
  } catch (err) {
    if (TransportError.is(err) && err.kind === 'auth') {
      console.error('Not a subscription session — refusing to run.');
      process.exit(1);
    }
    throw err;
  }

  // 3. Consume events. Each events() call is a FRESH independent subscription.
  const pump = (async () => {
    for await (const ev of session.events()) {
      if (ev.kind === 'text') process.stdout.write(ev.text);
      else if (ev.kind === 'tool_use') console.log(`\n[tool] ${ev.tool}`);
      else if (ev.kind === 'turn_complete') break; // stop after one turn
    }
  })();

  // 4. Run one turn. turn() blocks until the turn reaches a boundary.
  const result = await session.turn('In one sentence, what is a pseudo-terminal?');
  console.log(`\nstatus=${result.status}`);
  if (result.fidelity === 'structured') {
    console.log(`cost(turn)=$${result.perTurnCostUsd.toFixed(4)}`);
  }

  // 5. Close. graceful cleanly interrupts a busy turn (it settles as 'interrupted')
  //    for an orderly shutdown — it does NOT wait for the turn to finish. Idempotent.
  await pump;
  await session.close({ graceful: true });
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Tip

turn() is the blocking convenience. For streaming control use send(), which returns a SendHandle ({ id, turnId, cancel(), settled }) immediately and lets you await handle.settled yourself.

Notes on the example:

  • Subscription OAuth only. Two distinct defenses. (1) Defense-in-depth: ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL, and AWS_BEARER_TOKEN_BEDROCK are stripped from the child env (hardenEnv/CREDENTIAL_ENV_VARS). (2) The auth rejection is driven by the SDK's reported apiKeySource: assertAuthFromInit accepts only 'oauth'/'none' and rejects anything else as kind:'auth'. Note the pinBaseUrl knob deliberately re-pins ANTHROPIC_BASE_URL after the strip, so a caller-pinned base URL is intentionally allowed.
  • Fail-closed. If init fails, auth fails, or a same-id session is already live, start() tears the backend down before resolving — no half-open session leaks.
  • fidelity gates the rich fields. Only the 'structured' branch of TurnResult carries usage, costUsd, perTurnCostUsd, etc. Narrow on result.fidelity before reading them.

Error handling

TransportError carries a kind and the backend it came from. Narrow with the static guard:

if (TransportError.is(err)) {
  console.error(`[${err.backend}] ${err.kind}: ${err.message}`);
}

TransportErrorKind is one of: auth, spawn, protocol, timeout, permission_denied, unsupported_version, version_drift, quota_exhausted. (Interruption is not an error — it is a normal TurnResult.status of 'interrupted'.) A quota_exhausted error also carries the affected billing pool.

The PTY transport

PtyTransport drives the real claude TUI over a pseudo-terminal instead of the SDK. The interface is identical (start() returns the same Session), but events are 'inferred' fidelity, and resumeKey is late-bound — captured asynchronously after the session reaches idle, so read it after start() resolves rather than relying on it immediately.

import { PtyTransport } from '@ahmed-hobeishy/claude-coder';

const transport = new PtyTransport(); // needs `claude` on PATH (or set claudeBinaryPath)
const session = await transport.start({ cwd: process.cwd(), start: { kind: 'new' } });

PtyTransport is version-gated: it probes claude --version and rejects an out-of-band version (unsupported_version) before any spawn. The supported band is [2.1.0, 2.2.0). By default the folder-trust dialog fails fast; pass autoAcceptTrust: true only for a trusted-by-construction sandbox.

Transport options

Both constructors take an optional options object; the rest of each interface is test-injection seams (queryFn on SDK; spawnFn, versionProbeFn, emulatorFactory, detectorFactory on PTY) plus a shared registry seam on both, not intended for normal use.

Transport Caller-relevant options
SdkTransportOptions pinBaseUrl (pins ANTHROPIC_BASE_URL into the hardened child env)
PtyTransportOptions claudeBinaryPath (else claude on PATH), autoAcceptTrust (default false), pinBaseUrl, prebaked (PrebakedPermissionRules baked into temp settings; default empty), readyTimeoutMs / readyTickMs (auth/ready gate timing)

Choose the SDK transport for structured events, cost/usage, and programmatic permissions. Choose PTY when you specifically need the real interactive TUI behavior. The two backends and their trade-offs are covered in concepts.

Higher-level: FleetApi

FleetApi (exported from the barrel) is the in-process control plane: one object that manages a named fleet of sessions. It wraps a SessionManager and adds profiles, persistence, history, and capacity accounting, returning closure-free serializable views.

Group Methods
Sessions createSession, listSessions, getSession, closeSession, closeAll, renameSession
Turns / input runTurn, sendInput, cancel, interruptSession, setPermissionMode
Streams streamEvents, streamPolicyEvents
Profiles defineProfile, listProfiles, getProfile
Resume / fork / migrate resumeSession, forkSession, migrateSession
Persistence / history listPersisted, reconcile, getHistory, getCatalogInfo
Capacity capacity, liveCount

Reach for FleetApi directly for in-process control. To iterate against the subscription over an HTTP boundary instead — e.g. from an app in another language or process — run the local API gateway (claude-coder serve-api), a loopback server speaking the Anthropic Messages API wire format.