Run Claude agents in isolated Docker containers with streaming output, filesystem IPC, and built-in MCP tools. Plain .js files. No build step. Zero dependencies.
The core primitives you need to orchestrate Claude agents in containers.
Spawn Docker, Podman, or Apple containers. Stream agent output in real-time via sentinel-delimited JSON over stdout.
Atomic JSON file-based communication between host and container. No sockets, no ports—just files. Simple and reliable.
Per-group concurrency with configurable global limits. Exponential backoff retry. One container per group, up to N in parallel.
Built-in MCP server exposes send_message, schedule_task, and task lifecycle tools to the agent inside the container.
Allowlist-based volume mount validation. Blocks sensitive paths (.ssh, .env, credentials) by default. Symlink-aware.
The host side uses only Node.js built-ins. No native modules, no build step. Pure JavaScript ESM that just works.
Install, configure, run. Plain JavaScript — no compilation, no transpilation.
import { runContainerAgent, createConfig } from 'jsclaw'; const result = await runContainerAgent( { name: 'my-agent', folder: 'my-agent' }, { prompt: 'Analyze the codebase and suggest improvements.', groupFolder: 'my-agent', chatJid: 'user-1', isMain: true }, (proc, name) => console.log(`Started ${name}`), async (output) => console.log(output.result), );
Host process communicates with isolated containers via stdin/stdout and filesystem IPC.
Granular ESM exports — import only what you need.
| Export | Description |
|---|---|
createConfig(overrides?) |
Create configuration with defaults, env vars, and overrides |
runContainerAgent(group, input, ...) |
Spawn a container, run a Claude agent, stream results |
GroupQueue |
Per-group concurrency queue with global container limit |
startIpcWatcher(deps, config?) |
Poll IPC directories for messages and task operations |
writeIpcFile(dir, data) |
Atomic JSON file write (temp + rename) |
drainIpcDir(dir, filter?) |
Read all JSON files from a directory, parse, and delete |
validateAdditionalMounts(...) |
Validate volume mounts against a security allowlist |
buildVolumeMounts(group, config) |
Build Docker volume mount arguments for a group |
parseContainerOutput(buffer) |
Parse sentinel-delimited JSON from container stdout |
createLogger(options?) |
Minimal JSON logger (no dependencies) |
Tools available to the Claude agent inside the container via the built-in MCP server.
| Tool | Description |
|---|---|
send_message |
Send a message to the user or group immediately |
schedule_task |
Schedule a cron, interval, or one-shot task |
list_tasks |
List all scheduled tasks for the current group |
pause_task |
Pause a scheduled task by ID |
resume_task |
Resume a paused task by ID |
cancel_task |
Cancel and delete a scheduled task |
Configure via createConfig() overrides or environment variables.
| Env Variable | Default | Description |
|---|---|---|
JSCLAW_CONTAINER_IMAGE |
jsclaw-agent:latest | Docker image for agent containers |
JSCLAW_CONTAINER_RUNTIME |
docker | docker, podman, or container |
JSCLAW_CONTAINER_TIMEOUT |
1800000 | Idle timeout in milliseconds |
JSCLAW_MAX_CONCURRENT |
5 | Maximum concurrent containers |
JSCLAW_DATA_DIR |
./data | IPC data directory |
JSCLAW_GROUPS_DIR |
./groups | Group workspace directory |
ANTHROPIC_API_KEY |
— | Required for Claude API access |
Bring your own I/O — here's a full Telegram bot wired to jsclaw. Per-chat containers, session continuity, queue concurrency.
import { Bot } from 'grammy'; import { runContainerAgent, GroupQueue, createConfig } from 'jsclaw'; const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN); const queue = new GroupQueue(createConfig()); const sessions = new Map(); bot.on('message:text', async (ctx) => { const id = String(ctx.chat.id); await ctx.replyWithChatAction('typing'); await queue.enqueueTask(id, `msg-${Date.now()}`, async () => { await runContainerAgent( { name: id, folder: id }, { prompt: ctx.message.text, groupFolder: id, chatJid: id, isMain: true, sessionId: sessions.get(id) }, null, async (out) => { if (out.result) await ctx.reply(out.result); if (out.newSessionId) sessions.set(id, out.newSessionId); }, ); }); }); bot.start();
Full example with error handling, message splitting, and graceful shutdown: examples/telegram.js