by dzhng
Run Claude Agent (Claude Code) in a sandbox, control it via websocket
# Add to your Claude Code skills
git clone https://github.com/dzhng/claude-agent-serverA WebSocket server that wraps the Claude Agent SDK, allowing real-time bidirectional communication with Claude through WebSockets. Deploy it as an E2B sandbox and connect via the TypeScript client library.
Typical Workflow:
bun run build:e2b@dzhng/claude-agent in your project and connect to your E2B sandboxpackages/server/bun run start:server and bun run test:local to test your changes before rebuildingCreate a .env file in the root directory:
cp .env.example .env
Add your API keys:
ANTHROPIC_API_KEY=sk-ant-your-api-key-here
E2B_API_KEY=e2b_your-api-key-here
Install dependencies:
bun install
Build and deploy the server as an E2B template:
bun run build:e2b
This creates a sandbox template named on E2B. The build process:
No comments yet. Be the first to share your thoughts!
claude-agent-serverThe build may take a few minutes. Once complete, your template is ready to use.
Install the client library in your project:
npm install @dzhng/claude-agent
# or
bun add @dzhng/claude-agent
Connect to your E2B sandbox:
import { ClaudeAgentClient } from '@dzhng/claude-agent'
const client = new ClaudeAgentClient({
e2bApiKey: process.env.E2B_API_KEY,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
template: 'claude-agent-server', // Your E2B template name
debug: true,
})
// Start the client (creates E2B sandbox and connects)
await client.start()
// Listen for messages from Claude
client.onMessage(message => {
if (message.type === 'sdk_message') {
console.log('Claude:', message.data)
}
})
// Send a message to Claude
client.send({
type: 'user_message',
data: {
type: 'user',
session_id: 'my-session',
message: {
role: 'user',
content: 'Hello, Claude!',
},
},
})
// Clean up when done
await client.stop()
That's it! The client library handles:
If you want to customize the server behavior:
The server code is in packages/server/:
index.ts - Main server and WebSocket handlingmessage-handler.ts - Message processing logicconst.ts - Configuration constantsStart the server locally:
bun run start:server
In another terminal, run the test client against localhost:
bun run test:local
This runs packages/client/example-client.ts connected to localhost:3000 instead of E2B.
Once you're satisfied with your changes, rebuild the E2B template:
bun run build:e2b
Your updated server will be deployed to E2B with the same template name.
bun run build:e2bBuilds and deploys the server as an E2B template. This is the main way to deploy your server to the cloud.
bun run test:clientRuns the example client (packages/client/example-client.ts) connected to an E2B sandbox. Requires both E2B_API_KEY and ANTHROPIC_API_KEY in your .env file.
bun run start:serverStarts the server locally on http://localhost:3000. Use this for local development and testing.
bun run test:localRuns the example client connected to localhost:3000. Use this to test your local server changes before rebuilding the E2B image.
npm install @dzhng/claude-agent
# or
bun add @dzhng/claude-agent
interface ClientOptions {
// Required (unless using environment variables)
anthropicApiKey?: string
// E2B Configuration (optional if using connectionUrl)
e2bApiKey?: string
template?: string // E2B template name, defaults to 'claude-agent-server'
timeoutMs?: number // Sandbox timeout, defaults to 5 minutes
// Connection Configuration
connectionUrl?: string // Use this to connect to local/custom server instead of E2B
// Other Options
debug?: boolean // Enable debug logging
// Query Configuration (passed to server)
agents?: Record<string, AgentDefinition>
allowedTools?: string[]
systemPrompt?:
| string
| { type: 'preset'; preset: 'claude_code'; append?: string }
model?: string
}
async start() - Initialize the client and connect to the serversend(message: WSInputMessage) - Send a message to the agentonMessage(handler: (message: WSOutputMessage) => void) - Register a message handler (returns unsubscribe function)async stop() - Disconnect and clean up resourcesimport { ClaudeAgentClient } from '@dzhng/claude-agent'
const client = new ClaudeAgentClient({
e2bApiKey: process.env.E2B_API_KEY,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
template: 'claude-agent-server',
debug: true,
})
await client.start()
client.onMessage(message => {
if (message.type === 'sdk_message') {
console.log('Claude:', message.data)
}
})
client.send({
type: 'user_message',
data: {
type: 'user',
session_id: 'session-1',
message: { role: 'user', content: 'Hello' },
},
})
await client.stop()
const client = new ClaudeAgentClient({
connectionUrl: 'http://localhost:3000',
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
})
await client.start()
For more details, see packages/client/README.md.
Note: If you're using the @dzhng/claude-agent library, you don't need to interact with these endpoints directly. The client handles configuration and WebSocket connections for you. This section is for advanced users who want to connect to the server directly or build their own client.
The server runs on http://localhost:3000 (or your E2B sandbox URL) with:
http://localhost:3000/configws://localhost:3000/wsSet the configuration for the Claude Agent SDK query:
type QueryConfig = {
agents?: Record<string, AgentDefinition>
allowedTools?: string[]
systemPrompt?:
| string
| {
type: 'preset'
preset: 'claude_code'
append?: string
}
model?: string
anthropicApiKey?: string
}
Example:
curl -X POST http://localhost:3000/config \
-H "Content-Type: application/json" \
-d '{
"systemPrompt": "You are a helpful assistant.",
"allowedTools": ["read_file", "write_file"],
"anthropicApiKey": "sk-ant-...",
"model": "claude-3-5-sonnet-20241022",
"agents": {
"myAgent": {
"name": "My Custom Agent",
"description": "A custom agent"
}
}
}'
Response:
{
"success": true,
"config": {
"systemPrompt": "You are a helpful assistant.",
"allowedTools": ["read_file", "write_file"],
"agents": { ... }
}
}
Get the current configuration:
curl http://localhost:3000/config
Response:
{
"config": {
"systemPrompt": "You are a helpful assistant.",
"allowedTools": ["read_file", "write_file"]
}
}
Connect to the WebSocket endpoint:
const ws = new WebSocket('ws://localhost:3000/ws')
Note: The server only accepts one active connection at a time. If another client is already connected, new connection attempts will be rejected with an error message.
Sending Messages (Client → Server)
type WSInputMessage =
| {
type: 'user_message'
data: SDKUserMessage
}
| {
type: 'interrupt'
}
User Message:
Send a wrapped SDKUserMessage:
{
"type": "user_message",
"data": {
"type": "user",
"session_id": "your-session-id",
"parent_tool_use_id": null,
"message": {
"role": "user",
"content": "Hello, Claude!"
}
}
}
Structure:
type: Must be "user_message"data: An SDKUserMessage object containing:
type: Must be "user"session_id: Your session identifier (string)message: An object with role and content
role: Must be "user"content: The message content (string)parent_tool_use_id: Optional, for tool use responsesuuid: Optional, message UUID (auto-generated if not provided)Interrupt Message:
Send an interrupt to stop the current agent operation:
{
"type": "interrupt"
}
Receiving Messages (Server → Client)
type WSOutputMessage =
| { type: 'connected' }
| { type: 'sdk_message'; data: unknown }
| { type: 'error'; error: string }
Connection confirmation:
{
"type": "connected"
}
SDK messages (responses from Claude):
{
"type": "sdk_message",
"data": {
"type": "assistant",
"session_id": "...",
"message": {
/* Claude's response */
}
}
}
Error messages:
{
"type": "error",
"error": "Error description"
}
The server is a simple 1-to-1 relay between a single WebSocket client and the Claude Agent S