Claude Code agent capabilities — tools, the tool loop, multi-turn conversations, MCP, sub-agents, multi-agent teams, sessions — against any OpenAI- or Anthropic-compatible LLM endpoint, running in the browser (WebContainer), Node, and Bun. No backend required, no OAuth, no native binaries.
Live demo:[a full IDE running in your browser]·Docs:[anyclaude-docs.puter.site]·React UI kit:anyclaude-react
It exposes the same query()
async-generator interface and the same SDKMessage
envelope as @anthropic-ai/claude-agent-sdk
, so code written against the official SDK can iterate our output unchanged.
Multi-agent teams go beyond one agent: a coordinator delegates board tasks to worker sub-agents in parallel, you can dispatch a message to a running worker and have it land on its next tool round (push delivery, like the message queue), supervise them live with background dispatch, block for the next finished worker with
(event-driven — integrate each result as it lands, no busy-polling), and even run agents in
wait_for_worker
separate Web Workers or browser tabs that share one mailbox via
BroadcastChannelMailbox
. See Teams & sub-agents.
npm install anyclaude-sdk @webcontainer/api
@webcontainer/api
is an optional peer dependency — only needed if you use
WebContainerWorkspace
. You can supply your own FileSystem
/CommandExecutor
.
import { WebContainer } from '@webcontainer/api'
import {
query,
WebContainerWorkspace,
createOpenAIClient,
ALL_CLAUDE_CODE_TOOLS,
} from 'anyclaude-sdk'
// 1. Boot a WebContainer and wrap it as a workspace.
const wc = await WebContainer.boot()
const workspace = new WebContainerWorkspace(wc)
// 2. Point at any OpenAI-compatible endpoint.
const llm = createOpenAIClient({
apiKey: import.meta.env.VITE_OPENAI_API_KEY,
baseUrl: 'https://api.openai.com/v1', // or Groq, Together, OpenRouter, local…
model: 'gpt-4o',
})
// 3. Run the agent — same shape as the official SDK.
for await (const msg of query({ prompt: 'List the files and summarize the project', workspace, llm })) {
if (msg.type === 'assistant') {
for (const block of msg.message.content) {
if (block.type === 'text') console.log(block.text)
}
} else if (msg.type === 'result' && msg.subtype === 'success') {
console.log('Done:', msg.result)
}
}
Connect external MCP servers or define in-process tools. Because browsers block
direct cross-origin MCP fetches (CORS), pass a mcpProxy
for remote servers:
import { createSdkMcpServer, tool } from 'anyclaude-sdk'
const calc = createSdkMcpServer({
name: 'calc',
tools: [tool('add', 'Add two numbers',
{ type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } }, required: ['a', 'b'] },
(args) => ({ content: [{ type: 'text', text: String(args.a + args.b) }] }))],
})
query({
prompt, workspace, llm,
mcpServers: {
calc, // in-process, no network
docs: { type: 'http', url: 'https://mcp.example.com' }, // remote
},
// Route remote MCP through a CORS proxy (function, `{url}`/`{rawUrl}` template, or bare prefix):
mcpProxy: 'https://my-proxy.example/?url={url}',
})
Remote tools are exposed as mcp__<server>__<tool>
.
Three transport clients, all implementing the same LLMClient
interface:
import { createOpenAIClient, createAnthropicClient, createResponsesClient } from 'anyclaude-sdk'
// OpenAI-compatible Chat Completions (OpenAI, Groq, Together, OpenRouter, xAI, Kilo, local…)
const a = createOpenAIClient({ apiKey, baseUrl: 'https://api.x.ai/v1', model: 'grok-build-0.1' })
// Anthropic Messages API
const b = createAnthropicClient({ apiKey, model: 'claude-sonnet-4-6' })
// OpenAI Responses API (POST /v1/responses)
const c = createResponsesClient({ apiKey, model: 'gpt-4o' })
// AgentRouter gateway (sponsor) — one-line preset over createOpenAIClient; key
// falls back to AGENTROUTER_API_KEY. Reaches Claude/GPT/Gemini/DeepSeek/GLM/… via one base URL.
import { createAgentRouterClient } from 'anyclaude-sdk'
const d = createAgentRouterClient({ model: 'claude-sonnet-4-5-20250929' })
All three normalize tool calls, streaming, and usage to the same StreamResult
,
and include a fallback parser for models that emit tool calls as inline text.
Every client also accepts extraHeaders
/ extraBody
(merge provider-specific
headers/params without a custom client) and surfaces retries
on the result when transient failures were retried.
Use a PromptStream
to push user turns over time:
import { query, PromptStream } from 'anyclaude-sdk'
const prompts = new PromptStream()
const session = query({ prompt: prompts, workspace, llm, model: 'gpt-4o' })
prompts.push('Create a hello.txt with a greeting')
// …later, based on UI input:
prompts.push('Now translate it to French')
prompts.end() // close the conversation
for await (const msg of session) {
// render msg…
}
ALL_CLAUDE_CODE_TOOLS
includes:
| Tool | Purpose |
|---|---|
bash |
|
Run shell commands via jsh (2>&1 //dev/null redirects are stripped) |
|
read_file |
|
Read text (numbered lines, offset/limit), images (auto-downsampled base64), PDFs (document block), and notebooks (.ipynb cells + outputs); binary files are rejected with guidance |
|
write_file |
|
| Write a file, creating parent dirs | |
edit_file |
|
| Exact-match string replace (requires a prior read) | |
multi_edit |
|
| Apply a sequence of edits to one file atomically | |
notebook_edit |
|
Replace/insert/delete cells in a .ipynb |
|
delete_file |
|
| Remove a file/dir | |
glob |
|
Find files by glob pattern (** , * , ? ) |
|
grep |
|
| Regex search across files | |
list_files |
|
| List a directory | |
todo_write |
|
| Track a multi-step task list across turns | |
web_fetch |
|
| Fetch a URL → clean Markdown via the Jina Reader (CORS-free, JS-rendered) | |
web_search |
|
| Web search via Jina + DuckDuckGo HTML; returns top-N title/URL/snippet | |
render_page |
|
Render a public page in a headless browser: Playwright-style actions (click/fill/scroll/press), accessibility snapshot , evaluate JS, console+network logs , and a screenshot (as a viewable image) or extracted markdown/html. Refuses localhost/private/WebContainer hosts. |
read_file
dispatches by file type. Image and PDF bytes are forwarded to the
model automatically as a follow-up user turn (Anthropic gets native
image
/document
blocks; OpenAI-compatible endpoints get image_url
/file
parts), so the model can actually see the file, not just a text summary.
Tune the caps via limits
:
query({ prompt, workspace, llm, limits: { maxTokens: 25000, maxImageBytes: 3_750_000, maxPdfPages: 20 } })
Pass a subset, or your own Tool[]
, via tools:
:
import { readFile, writeFile, editFile } from 'anyclaude-sdk'
query({ prompt, workspace, llm, tools: [readFile, writeFile, editFile] })
A user turn beginning with /
is intercepted. Built-ins: /help
, /clear
,
/compact [focus]
(summarizes history to free context), /tools
, /cost
,
/model
. Define your own prompt-template commands:
import { query, promptCommand } from 'anyclaude-sdk'
query({
prompt: promptStream, workspace, llm,
commands: [promptCommand('review', 'Review the diff', 'Review this code and list issues: $ARGUMENTS')],
})
// user types: /review src/app.ts
Enable with background: true
to run sub-agents or long work off the critical
path. The task
tool gains run_in_background
(returns a task id immediately),
and task_list
/ task_output
/ task_stop
tools let the agent poll them.
Optional off-main-thread execution via a Comlink worker harness
(exposeBackgroundWorker
/ wrapWorker
); the in-thread manager works without it.
query({ prompt, workspace, llm, agents: {}, background: true })
Two halves: Comlink for main→worker control (wrapWorker
/ exposeBackgroundWorker
, above), and ** BroadcastChannelMailbox** so agents in
differentworkers gossip mailbox-style. It's a drop-in
Mailbox
, so the existing team
tools
(send_message
/ dispatch_tasks
) work unchanged across workers:
import { BroadcastChannelMailbox } from 'anyclaude-sdk'
// inside each Web Worker / tab / worker_thread, same channel name:
const mailbox = new BroadcastChannelMailbox({ channelName: 'team', origin: 'planner' })
query({ prompt, workspace, llm, team: true, mailbox })
// messages sent by one worker land in the addressed agent's inbox in another.
Uses the global BroadcastChannel
by default. For durable cross-tab delivery (IndexedDB/localStorage fallbacks, older browsers, Node) use the one-call helper — it's backed by the bundled broadcast-channel package, lazy-imported so it stays out of bundles that don't use it:
const mailbox = await BroadcastChannelMailbox.crossTab({ channelName: 'team', origin: 'planner' })
query({ prompt, workspace, llm, team: true, mailbox })
Push delivery to a running agent. Messages addressed to an agent are
auto-injected into its transcript at the next turn boundary — same model as the
message queue, but from the shared mailbox. So a coordinator (or peer, or another
worker) can redirect a running sub-agent mid-task and it lands on the
sub-agent's next tool round, no polling tool needed. dispatch_tasks
names each
worker worker:<taskId>
so you can target a specific one:
mailbox.send('coordinator', 'worker:task_1', 'while you work: also add logging')
// worker:task_1 sees "[Team messages] - from coordinator: ..." on its next step.
On by default with team: true
; opt out via query({ deliverTeamMessages: false })
.
You aren't tied to WebContainer. A Sandbox
is just a FileSystem
plus a
CommandExecutor
, and you can mix and match.
Adapters wrap each provider's client structurally (no hard dependency on their SDKs — install only the one you use):
import { E2BSandbox, VercelSandbox, DaytonaSandbox, CloudflareSandbox } from 'anyclaude-sdk'
// e.g. E2B
import { Sandbox } from 'e2b'
const sbx = await Sandbox.create()
const workspace = new E2BSandbox(sbx)
query({ prompt, workspace, llm })
Supported: WebContainer, E2B, Vercel Sandbox, Daytona,
Cloudflare Sandbox, and LocalSandbox (real OS). All implement the same
Sandbox
interface.
Run the agent directly against the host machine's filesystem and shell — like Claude Code — with automatic platform detection (Windows / macOS / Linux):
import { LocalSandbox, createAnthropicClient, query } from 'anyclaude-sdk'
const workspace = new LocalSandbox({ cwd: '/path/to/project' }) // defaults to process.cwd()
const llm = createAnthropicClient({ baseUrl, model: 'claude-sonnet-4-6', apiKey })
for await (const msg of query({ prompt: 'add a CLI flag and run the tests', workspace, llm })) { /* … */ }
The agent's working directory is taken from the sandbox automatically. See
examples/local-agent.mjs
for a runnable headless demo. On Windows it uses
cmd.exe
; elsewhere $SHELL
//bin/sh
(override via shell
/shellArgs
).
For a durable local filesystem in the browser, use a DB-backed FS and seed a
standard Linux tree. DexieFileSystem
(IndexedDB) is the recommended default
— persistent across reloads, indexed for fast readdir
/glob
, with metadata (mode, mtime, symlinks):
import {
DexieFileSystem, OpfsFileSystem, seedLinuxTree, composeWorkspace, NoopCommandExecutor,
} from 'anyclaude-sdk'
const fs = new DexieFileSystem('my-project-fs') // or: new OpfsFileSystem()
await seedLinuxTree(fs) // /bin /etc /home/user /tmp /usr …
// File-only agent (no shell):
const workspace = composeWorkspace(fs, new NoopCommandExecutor(), '/home/user')
// …or pair a persistent FS with a remote shell:
// const workspace = composeWorkspace(fs, new E2BSandbox(sbx), '/home/user')
OpfsFileSystem
(Origin Private File System) is offered alongside Dexie for
large-binary / native-handle scenarios; use OpfsFileSystem.isSupported()
to feature-detect.
A MemoryFileSystem
also ships for tests:
import { MemoryFileSystem, NoopCommandExecutor, composeWorkspace } from 'anyclaude-sdk'
const fs = new MemoryFileSystem()
await fs.writeFile('/app/index.ts', 'export const x = 1')
const workspace = composeWorkspace(fs, new NoopCommandExecutor())
Declare reusable prompt-skills inline — each becomes a /name
slash command and is invokable by the agent through the skill
tool. $ARGUMENTS
is substituted at call time:
import { query, defineSkill } from 'anyclaude-sdk'
query({
prompt, workspace, llm,
skills: [
defineSkill({
name: 'changelog',
description: 'Summarize git changes into a changelog entry',
instructions: 'Write a concise changelog entry for: $ARGUMENTS',
argumentHint: '<since>',
}),
],
})
You can also pass plain Skill
objects, or skills: true
to load .claude/skills/*.md
from the workspace.
Run query()
in a serverless function and stream SDKMessage
s to the browser. For runs longer than the platform's time cap, checkpoint at a turn boundary and continue transparently in a fresh invocation:
// near the deadline, persist to the store, emit a `d` message
query({ prompt, workspace, llm, sessionStore, maxDurationMs: 20_000 })
// later — resume + continue the tool loop with NO new user message
query({ workspace, llm, sessionStore, resume: true, continueRun: true })
Pluggable SessionStore
adapters (all implement SessionStoreLike
): SessionStore
(IndexedDB), MemorySessionStore
, KVSessionStore
(Vercel KV / Upstash), RedisSessionStore
, PostgresSessionStore
(Neon / pg / postgres.js), SupabaseSessionStore
.
Declare tools the host executes — e.g. run bash
in the user's browser WebContainer while the agent loop runs on your server. The run s with a client_tool_request
; the client executes it and you resume with the result:
import { WORKSPACE_TOOL_NAMES } from 'anyclaude-sdk'
query({ prompt, llm, workspace, sessionId, clientTools: WORKSPACE_TOOL_NAMES }) // → emits client_tool_request + s
query({ llm, workspace, sessionId, resume: true, continueRun: true, clientToolResults }) // → continues
On the browser side, anyclaude-react
turns those into a ready executor map backed by any workspace — a WebContainer (real shell + files), the user's IndexedDB (DexieFileSystem
), OPFS, or memory:
import { createWebContainerClientTools, createWorkspaceClientTools } from 'anyclaude-react'
useAgent({ endpoint: '/api/agent', clientTools: createWebContainerClientTools(wc) }) // files + bash
useAgent({ endpoint: '/api/agent', clientTools: createWorkspaceClientTools(new DexieFileSystem('my-db')) }) // IndexedDB
Provide onAskUser
and the agent gains an ask_user_question
tool to put a decision to the user — multiple-choice or free-text (inputType: 'choice' | 'text' | 'textarea' | 'number'
, plus allowOther
). Hosts that only render choices can ignore the new fields:
query({ prompt, workspace, llm, onAskUser: async ({ question, options, inputType }) =>
inputType && inputType !== 'choice' ? promptText(question) : pickOne(question, options) })
The agent loop runs server-side, so your system prompt, tool instructions, and retrieved context live in the server→LLM request and never reach the browser. To also strip sensitive artifacts (reasoning, raw tool output / RAG, model identity) from the streamed messages, wrap the stream — a pure, opt-in output transform:
import { projectMessages } from 'anyclaude-sdk'
for await (const m of projectMessages(query({ /* ... */ }), { preset: 'public' }))
res.write(JSON.stringify(m) + '\n')
d
and client_tool_request
control messages are always preserved. (Note: anything that runs in the browser — createAgentClient
mode — necessarily exposes its request; use the server/endpoint path when the prompt is proprietary.)
npm install anyclaude-react
useAgent()
plus restylable components — chat (AgentChat
, ChatPanel
, Transcript
, MarkdownMessage
, Composer
, Working
, ToolCall
) and an IDE set (Terminal
, FileExplorer
, CodeEditor
, AskUser
). createAgentClient
/ createEndpointClient
auto-stitch d
continuations and run clientTools
in the browser.
Stand up an Anthropic Messages API-compatible endpoint backed by any OpenAI-compatible model, so Claude Code itself (or any Anthropic-Messages client) runs against DeepSeek / Qwen / GLM / Kimi / local Ollama. Unlike a naive proxy, inline tool-call dialects are recovered into proper tool_use blocks, so tool use actually works on cheap models.
import { createOpenAIClient } from 'anyclaude-sdk/llm'
import { anthropicToChat, anthropicSSE } from 'anyclaude-sdk/anthropic-endpoint'
const llm = createOpenAIClient({ baseUrl: 'https://api.deepseek.com/v1', model: 'deepseek-chat', apiKey })
// POST /v1/messages:
for await (const evt of anthropicSSE(llm, anthropicToChat(body), { model: 'deepseek-chat' })) res.write(evt)
// then: ANTHROPIC_BASE_URL=http://localhost:8787 claude
Runnable: examples/claude-code-router.
Frontier models emit clean native function-calls; cheaper ones often don't. Three layers (in anyclaude-sdk/llm
) close the gap: tool-call dialects (parseToolCalls
— xml-function / hermes / json-fence), auto-detected model profiles (profileForModel
— qwen/deepseek/moonshot/zhipu/mistral/llama), and self-healing argument repair (query({ repairToolCalls })
, on by default — validates args and feeds the model a corrective tool_result instead of running with garbage). Prove it on your endpoints with scripts/compat-matrix.mjs →
npm create anyclaude-app@latest my-app # template: bolt — WebContainer + chat + live preview, no backend
The bolt
template wires useWebContainerPreview({ wc })
(boot a dev server → live preview URL) + a browser-side query()
- the IDE components. See anyclaude-react.
Keep a large pool of rarely-used tools out of the per-turn payload (big savings on weak/uncached models) while staying discoverable + callable. Mark them deferred; tool_search
indexes them and the loop arms a tool (sends its schema on subsequent turns) once search surfaces it — then it executes normally.
query({ prompt, workspace, llm,
extraTools: [deploy, ...integrationTools], // e.g. 35 integration tools
deferredTools: ['stripe_charge', 'supabase_query', /* … the niche ones */],
})
// or per-tool: defineTool({ name, description, parameters, run, defer: true })
Only the lean core + tool_search
are sent each turn; the model searches when it needs a niche tool, the SDK arms it, and the call goes through. Register 35, send ~10.
Opt-in knobs for token cost and latency — especially on weak / uncached models:
query({
prompt, workspace, llm,
systemPromptPreset: 'lean', // ~70% shorter built-in prompt — saved every turn on uncached models
keepToolResults: 6, // context editing: stub tool_results older than the last 6 (caps transcript growth)
parallelToolExecution: true, // run a turn's read-only tool calls concurrently (~2× faster on multi-read turns)
deferredTools: [/* niche tools */], // keep rarely-used tools out of the payload until tool_search arms them
})
// custom read tool opting into parallelism:
defineTool({ name: 'get_logs', description: '…', parameters, run, parallelSafe: true })
Mutating tools / bash
/ delegated client tools always execute serially; keepToolResults
and parallelToolExecution
preserve correctness, just trim cost/latency.
Live compaction marker—autoCompact
emits acompact_boundary
withstatus: 'start'
beforesummarizing (for a live "compacting…" shimmer) andstatus: 'end'
after withpost_tokens
.Cancel a queued message—MessageQueue.push()
returns a stable id;remove(id)
cancels a single pending message (per-pill ✕ in a UI).BYO LLM client— reuse the SDK's wire codec:toOpenAIMessages
,consumeSSE
, and the LLM types fromanyclaude-sdk/llm
(no bare-root import in browser bundles).
Runnable Vite projects in examples/:
(WebContainer IDE — real shell + Node in the tab),
browser-ide
browser-chat
, claude-code-router
, vercel-kv-survivor
, vercel-supabase-survivor
, vercel-indexeddb-survivor
, (server brain / browser hands). Try the
vercel-clienttools
.
live demoquery(options): AsyncGenerator<SDKMessage>
— main entry.prompt: string | AsyncIterable<SDKUserMessage>
workspace: FileSystem & CommandExecutor
llm: LLMClient
tools?
,extraTools?
,allowedTools?
/disallowedTools?
,deferredTools?
(lazy-load),model?
,systemPrompt?
/appendSystemPrompt?
,maxTurns?
(default 50),cwd?
,abortController?
- serverless:
sessionStore?
,resume?
,maxDurationMs?
,continueRun?
- client tools:
clientTools?
,clientToolResults?
; interactive:onAskUser?
- also:
mcpServers?
,agents?
,commands?
,hooks?
,background?
,team?
,memory?
,permissionMode?
/canUseTool?
,messageQueue?
createOpenAIClient
/createAnthropicClient
/createResponsesClient
WebContainerWorkspace
,MemoryFileSystem
,NoopCommandExecutor
,LocalSandbox
,composeWorkspace
defineTool
(custom tools),projectMessages
(server-side stream redaction)ALL_CLAUDE_CODE_TOOLS
, individual tools,toolDefs
,toolByName
- browser-clean subpaths:
anyclaude-sdk/{query,loop,llm,fs,workspace,tools,session,memory,compact,permissions,skills,queue,prompt,anthropic-endpoint,telemetry}
anyclaude-sdk/llm
:parseToolCalls
- dialects,
profileForModel
(model profiles),validateToolArguments
(repair),toOpenAIMessages
/consumeSSE
(BYO-client codec)anyclaude-sdk/anthropic-endpoint
:anthropicToChat
,anthropicSSE
,streamResultToAnthropicMessage
(Claude-Code router)runToolLoop
(/loop
),compactWithWindow
(/compact
),track
/telemetryEnabled
(/telemetry
)- All
SDK*
message types,ContentBlockParam
,LLMClient
,ToolDef
,SessionStoreLike
, etc.
| Feature | Official SDK | anyclaude-sdk |
|---|---|---|
| Auth | OAuth token | None required |
| Backend | claude.ai API | Any OpenAI/Anthropic endpoint |
| Runtime | Node only | Browser, Node, Bun |
| File ops | Native filesystem | Pluggable (WebContainer / Memory / IndexedDB / local) |
| Commands | Native shell | jsh (WebContainer) / local / client-side tools |
| MCP / slash commands / background tasks / sub-agents | Built-in | Built-in |
| Serverless survivor + prompt projection | — | Built-in |
The SDK emits anonymous, opt-out usage telemetry (SDK version, runtime, a coarse model-family bucket, and which features are used) — never code, prompts, repo identity, paths, or keys. It sends to an aggregate-only collector (a Puter Worker; source in examples/telemetry-collector). Disable with
ANYCLAUDE_TELEMETRY=0
, DO_NOT_TRACK=1
, or query({ disableTelemetry: true })
; repoint with ANYCLAUDE_TELEMETRY_URL
(or set it to ''
to send nowhere). Full disclosure: TELEMETRY.md.
MIT