The CharClaw Agents SDK
@charclaw/agents is the TypeScript library at the heart of CharClaw. It runs interactive coding-agent CLIs — Claude Code, OpenAI Codex, Google Gemini, Block Goose, OpenCode, Pi — as long-running background tasks inside isolated Daytona sandboxes or directly on a developer's machine, and it streams structured events back to your web service or desktop app via a single polling API.
You can use it standalone in any Node.js project. CharClaw uses it to power its Kanban board, its chat-with-agent surface, and its autopilot scheduler.
On this page
Why this SDK exists
Coding-agent CLIs are designed for an interactive shell. To wire them into a long-running web service, a scheduled job, or a desktop app that survives sleep/wake cycles, four problems show up immediately:
- Background execution. The agent's turn often outlives the request that started it.
- Streaming without stdin/stdout pipes. Once the spawning process is gone, you can't read its pipes — you need a polling-friendly transport.
- Sandbox isolation. Agent activity should not see your production secrets or your home directory.
- Re-attachment. If a serverless function cold-starts mid-turn, the new instance has to pick up exactly where the old one left off.
@charclaw/agents handles all four with a single
createSession entry point, a unified
Event stream, and a sandbox abstraction that targets
either a Daytona cloud sandbox or the local host.
Install
npm install @charclaw/agents @daytonaio/sdk
The @daytonaio/sdk dependency is only required if
you'll run agents in Daytona sandboxes. For pure local-machine use,
install just @charclaw/agents.
Quick start — Daytona sandbox
import { Daytona } from "@daytonaio/sdk"
import { adaptDaytonaSandbox, createSession } from "@charclaw/agents"
const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY! })
const raw = await daytona.create()
const sandbox = adaptDaytonaSandbox(raw)
const session = await createSession("claude", {
sandbox,
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
model: "sonnet",
systemPrompt: "You are a careful, focused engineer.",
})
await session.start("Add input validation to the /signup route.")
while (true) {
const { events, running } = await session.getEvents()
for (const e of events) {
if (e.type === "token") process.stdout.write(e.text)
if (e.type === "tool_start") console.log(`\n[tool] ${e.name}`)
}
if (!running) break
await new Promise(r => setTimeout(r, 1000))
}
await raw.delete()
Quick start — local sandbox
import { createLocalSandbox, createSession, localWorkdir } from "@charclaw/agents"
const sandbox = createLocalSandbox({ cwd: localWorkdir("acme", "api", "main") })
const session = await createSession("claude", {
sandbox,
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
})
await session.start("Summarize the diff on this branch.")
localWorkdir(...segments) returns a path under
~/charclaw-workspaces. Without arguments it returns the
base directory; with arguments it joins them, which is convenient for
per-repo and per-branch checkouts.
Restart-tolerant turns
Every session writes its metadata, parser cursor, and current turn's
PID to ~/.charclaw-sessions/<id>/ inside the sandbox.
That means a second Node process can re-attach to an in-flight turn
and continue polling without losing state:
import { adaptDaytonaSandbox, getSession } from "@charclaw/agents"
// Save these IDs somewhere durable when you start the turn.
const savedSandboxId = "sb_..."
const savedSessionId = "..."
// In a different process — possibly a serverless function cold-start.
const sandbox = adaptDaytonaSandbox(await daytona.get(savedSandboxId))
const session = await getSession(savedSessionId, { sandbox })
const { events, running } = await session.getEvents()
The polling cursor advances atomically; if two processes poll
concurrently, each sees its own slice of new events and the cursor
ends up at max(positions).
Supported agents
| Provider | CLI | Auth env | Parser status |
|---|---|---|---|
"claude" |
Claude Code | ANTHROPIC_API_KEY or CLAUDE_CODE_CREDENTIALS |
Stable |
"codex" |
OpenAI Codex CLI | OPENAI_API_KEY |
Tolerant — validate against your CLI version |
"gemini" |
Google Gemini CLI | GEMINI_API_KEY |
Tolerant |
"goose" |
Block Goose | Provider-specific | Tolerant |
"opencode" |
OpenCode | Provider-specific | Tolerant |
"pi" |
Pi | Provider-specific | Tolerant |
"mock" |
Built-in echo | None | Stable — useful for tests |
API reference
createSession(provider, options)
Creates a new session and provisions its directory inside the sandbox. The provider's CLI is auto-installed on the first session per sandbox.
const session = await createSession("claude", {
sandbox, // CodeAgentSandbox
env: { ANTHROPIC_API_KEY: "sk-..." }, // session-scoped env vars
model: "sonnet", // optional
systemPrompt: "You are helpful.", // optional
cwd: "/workspace/my-repo", // optional working dir
sessionId: "explicit-id", // optional override
})
session.start(prompt, options?)
Spawns the agent process for one turn. Returns immediately with the turn handle (PID, output file, turn ID).
const { pid, outputFile, turnId } = await session.start("Refactor X.", {
env: { GITHUB_TOKEN: "ghp_..." }, // run-scoped, cleared after
history: priorMessages, // optional context hint
})
session.getEvents()
Polls for new events since the last call. Always safe to call —
returns an empty events array if nothing new has
appeared. Persists parser state between calls.
const { events, running, sessionId, cursor, runPhase } = await session.getEvents()
session.cancel()
SIGTERM, brief grace, SIGKILL. Updates the persisted phase to "cancelled".
getSession(sessionId, options)
Re-attaches to an existing session by reading session.json
and state.json from the sandbox. Useful for continuing a
turn from a different process or after a restart.
Event types
type Event =
| { type: "session"; id: string }
| { type: "token"; text: string }
| { type: "tool_start"; name: string; id?: string; input?: unknown }
| { type: "tool_delta"; id?: string; text: string }
| { type: "tool_end"; name?: string; id?: string; output?: string; isError?: boolean }
| { type: "end"; error?: string }
| { type: "agent_crashed"; message?: string; output?: string }
Tool names are normalized to a canonical lowercase form
(shell, read, write,
edit, glob, grep,
task, todo, fetch) regardless
of which agent emits them. Use getToolDisplayName(name)
to render the canonical UI label (Bash, Read, Write, …).
How it works under the hood
-
Provision.
createSessioncreates~/.charclaw-sessions/<id>/and writessession.json(provider, model, system prompt) andstate.json(parser cursor, phase, history). -
Launch.
session.startcallssandbox.executeBackground, which wraps the command innohup(Daytona) orspawn(detached: true)(local). The wrapper writes the agent's stdout/stderr to a turn-specific log file and creates a.donesentinel file when the command exits. -
Poll.
session.getEventsreads the log file's new bytes (sliced from the last cursor position), runs the agent's JSON-Lines parser, and returns the events. State is persisted back tostate.json. -
Detect end. Either the
.donefile exists (clean finish) or the process is no longer alive (crash / kill / external SIGTERM). Either way,runninggoes false. -
Re-attach. Because every byte of state lives in
files inside the sandbox, any process with sandbox access can call
getSessionand pick up where the previous one left off.
Debugging
CHARCLAW_AGENTS_DEBUG=1 node my-script.js
The SDK prints structured debug messages to stderr
when this flag is set: PID acquisition, command construction, parser
state advances, and CLI installation steps.
License
GNU AGPL v3 or later. See the full license text.
@charclaw/agents as a
network service, you must offer the source code of your
modifications to the users of that service.
If that does not fit your use case, contact
anitc98@gmail.com
for commercial licensing.
→ Read about the launch in Launching the CharClaw Agents SDK, or browse the source on GitHub.