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, andAWS_BEARER_TOKEN_BEDROCKare stripped from the child env (hardenEnv/CREDENTIAL_ENV_VARS). (2) The auth rejection is driven by the SDK's reportedapiKeySource:assertAuthFromInitaccepts only'oauth'/'none'and rejects anything else askind:'auth'. Note thepinBaseUrlknob deliberately re-pinsANTHROPIC_BASE_URLafter 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. fidelitygates the rich fields. Only the'structured'branch ofTurnResultcarriesusage,costUsd,perTurnCostUsd, etc. Narrow onresult.fidelitybefore reading them.
Error handling¶
TransportError carries a kind and the backend it came from. Narrow with the static guard:
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.