Last updated: 2026-04-03
Claw’d is an open-source agentic chat platform where AI agents operate autonomously, communicating with users through a real-time collaborative chat UI. Agents can:
Core design principles:
| Principle | Description |
|---|---|
| Single binary deployment | Compiles to dist/clawd with embedded UI + browser extension |
| Provider-agnostic | Supports Copilot, OpenAI, Anthropic, Ollama, Minimax |
| Plugin-first agents | All agent capabilities are expressed through the ToolPlugin/Plugin interfaces |
| Secure by default | Sandboxed tool execution (bubblewrap/sandbox-exec), path validation, auth tokens |
| Real-time collaboration | WebSocket-driven UI with streaming tokens, tool calls, and read receipts |
| Multi-agent | Multiple agents per channel, sub-agent spawning, remote worker bridge |
flowchart TD
UB["User Browser\nHTTP/WebSocket\n(default: localhost:3456)"]
subgraph Server["Claw'd Server (src/index.ts — Bun HTTP+WS)"]
ChatAPI["Chat API\n/api/*"]
MCP["MCP Endpoint\n/mcp"]
Bridge["Browser\nBridge"]
subgraph DB["SQLite (WAL mode)"]
ChatDB["chat.db\n(messages, agents)"]
MemDB["memory.db\n(LLM sessions)"]
end
subgraph AgentLoop["Agent Loop (src/agent/)"]
LLM["LLM provider (multi-provider)"]
Tools["Tool plugins (browser, workspace)"]
MCPClients["MCP clients (chat + external)"]
Spawner["Sub-agent spawner (spaces)"]
Compactor["Context compactor / token manager"]
end
end
Chrome["Chrome Browser Extension\n(packages/browser-extension/)\nCDP tools (normal)\nStealth mode (anti-bot)"]
Worker["Remote Worker\n(TS / Python / Java)\nFile, shell, browser tools"]
UB -->|HTTP| ChatAPI
UB -->|HTTP| MCP
ChatAPI --> DB
MCP --> DB
Bridge --> AgentLoop
AgentLoop -->|WebSocket| Chrome
AgentLoop -->|"WebSocket (MCP)"| Worker
/api/* routes; WebSocket at /ws for real-time eventschat.db for chat state, memory.db for LLM sessionsWorkerLoop per agent, polling every 200msclawd/
├── src/ # Main server + agent system
│ ├── index.ts # Server entry point (HTTP/WS/routes)
│ ├── config.ts # CLI config parser
│ ├── config-file.ts # ~/.clawd/config.json loader; auth helpers, hot-reload
│ ├── internal-token.ts # Ephemeral INTERNAL_SERVICE_TOKEN (generated at startup)
│ ├── worker-loop.ts # Per-agent polling loop
│ ├── worker-manager.ts # Multi-agent orchestrator
│ ├── utils/
│ │ └── pattern.ts # matchesPattern() — glob matching (* wildcard, ? literal)
│ ├── server/
│ │ ├── database.ts # chat.db lazy singleton + Proxy; _resetForTesting()
│ │ ├── websocket.ts # WebSocket broadcasting
│ │ ├── http-helpers.ts # Shared HTTP helpers (json(), requireAuth(), etc.)
│ │ ├── validate.ts # validateBody<T>(schema, body) — Zod request validation
│ │ ├── routes/ # API route handlers (agents.ts, analytics.ts, …)
│ │ └── browser-bridge.ts # Browser extension WS bridge
│ ├── agent/
│ │ ├── agent.ts # Main Agent class + reasoning loop
│ │ ├── api/ # LLM provider clients, key pool, factory
│ │ ├── tools/ # Tool barrel (tools.ts) + 7 domain modules
│ │ │ ├── tools.ts # 201-line barrel — re-exports full API
│ │ │ ├── registry.ts # ToolDefinition registry + executeTool
│ │ │ ├── file-tools.ts # File read/write/glob/grep
│ │ │ ├── shell-tools.ts # Bash/exec tools
│ │ │ ├── git-tools.ts # Git operations
│ │ │ ├── chat-tools.ts # Chat send/upload/list
│ │ │ ├── web-tools.ts # Web fetch/search
│ │ │ └── memory-tools.ts # Memory recall/save
│ │ ├── plugins/ # All plugins (chat, browser, workspace, tunnel, etc.)
│ │ ├── session/ # Session manager, checkpoints, summarizer
│ │ ├── memory/ # memory.ts, knowledge-base.ts, agent-memory.ts
│ │ ├── mcp/ # MCP client connections
│ │ └── utils/ # sandbox.ts, agent-context.ts
│ ├── db/ # Unified migration system
│ │ ├── migrations.ts # runMigrations(db, migrations, strategy)
│ │ └── migrations/ # Per-DB migration files
│ │ ├── chat-migrations.ts
│ │ ├── memory-migrations.ts
│ │ ├── scheduler-migrations.ts
│ │ ├── kanban-migrations.ts
│ │ └── skills-cache-migrations.ts
│ ├── spaces/ # Sub-agent system
│ │ ├── manager.ts # Space lifecycle management
│ │ ├── worker.ts # Space worker orchestrator
│ │ ├── spawn-plugin.ts # spawn_agent tool
│ │ ├── plugin.ts # complete_task, get_environment
│ │ └── db.ts # spaces table schema
│ ├── scheduler/ # Scheduled jobs (cron, interval, once)
│ │ ├── manager.ts # Scheduler tick loop
│ │ ├── runner.ts # Job executor (creates sub-spaces)
│ │ └── parse-schedule.ts # Natural language schedule parser
│ └── api/ # Agent management, articles, MCP servers
├── packages/
│ ├── ui/ # React SPA (Vite + TypeScript)
│ │ └── src/
│ │ ├── App.tsx # Main app, WS, routing, per-channel auth gate
│ │ ├── MessageList.tsx # Messages + StreamOutputDialog
│ │ ├── auth-fetch.ts # Per-channel token storage + authFetch() wrapper
│ │ └── styles.css # All styles
│ └── browser-extension/ # Chrome MV3 extension
│ ├── manifest.json # Extension manifest
│ └── src/
│ ├── service-worker.js # Command dispatcher (~2700 lines)
│ ├── content-script.js # DOM extraction
│ ├── shield.js # Anti-detection patches
│ └── offscreen.js # WS connection maintainer
├── scripts/ # Build utilities
│ ├── embed-ui.ts # Embeds UI into binary
│ └── zip-extension.ts # Packs extension into binary
├── docs/ # Documentation
├── Dockerfile # Multi-stage Docker build
└── compose.yaml # Docker Compose deployment
| File | Purpose |
|---|---|
src/index.ts |
HTTP server, WebSocket handler, route registration |
src/config.ts |
CLI argument parser (–port, –host, –yolo, –debug) |
src/config-file.ts |
Loads and validates ~/.clawd/config.json; exposes isChannelAuthRequired(), validateApiToken(), hasGlobalAuth() |
src/internal-token.ts |
Generates ephemeral INTERNAL_SERVICE_TOKEN at startup (never persisted, never exposed externally) |
src/utils/pattern.ts |
matchesPattern(value, pattern) — glob matching with * wildcard (? treated as literal) |
src/worker-loop.ts |
Per-agent polling loop (200ms interval) |
src/worker-manager.ts |
Manages lifecycle of all agent WorkerLoop instances |
src/server/database.ts |
chat.db lazy singleton (Proxy), schema, prepared statements; _resetForTesting() |
src/server/http-helpers.ts |
Shared HTTP utilities used across route handlers |
src/server/validate.ts |
Zod-backed validateBody<T>() for HTTP request body validation |
src/db/migrations.ts |
runMigrations(db, migrations, strategy) — unified migration runner (PRAGMA user_version) |
src/server/websocket.ts |
WebSocket connection tracking, message broadcasting |
src/server/browser-bridge.ts |
WebSocket bridge between agents and browser extension |
src/agent/agent.ts |
Core Agent class — reasoning loop, tool dispatch |
src/spaces/manager.ts |
Sub-agent space creation, lifecycle, cleanup |
src/scheduler/manager.ts |
Cron/interval/once job scheduling and execution |
src/index.ts runs a single Bun HTTP + WebSocket server (default: 0.0.0.0:3456). The
file was refactored from ~2557 → ~1905 lines by extracting route handlers into dedicated
modules under src/server/routes/ (e.g., agents.ts, analytics.ts) with shared helpers
in src/server/http-helpers.ts.
All API requests are routed through the HTTP handler. The server serves three primary functions:
/api/*) — Chat, agent management, files, scheduler, analytics/mcp) — Model Context Protocol SSE transport for external clients; auth enforced on all /mcp paths except /mcp/agent/ (Claude Code sub-agents) and /mcp/space/ (per-space token validation)/*) — Embedded React SPA served as fallback for all non-API routes| Upgrade Path | Purpose |
|---|---|
/ws |
Real-time chat events (messages, reactions, agent streaming, tool calls) |
/browser/ws |
Browser extension bridge (command dispatch + results) |
| Event type | Trigger | Payload |
|---|---|---|
message |
New message posted (HTTP API or MCP chat_send_message) |
{ type, channel, message } |
message_changed |
Message updated or appended (chat_update_message, chat_append_message) |
{ type, channel, message } |
agent_streaming |
Agent begins or ends streaming a response | { type, channel, agent_id, is_streaming, avatar_color } |
agent_token |
Streaming token chunk | { type, channel, agent_id, token } |
agent_tool_call |
Tool invoked by agent | { type, channel, agent_id, tool, input } |
agent_seen |
Agent polled for new messages | { type, channel, agent_id, last_seen_ts } |
agent_status |
Agent status changed | { type, channel, agent_id, status } |
agent_sleep |
Agent sleep state changed | { type, channel, agent_id, is_sleeping } |
channel_cleared |
Channel history cleared | { type, channel } |
reaction_added / reaction_removed |
Emoji reaction toggled | { type, channel, item, reaction, user } |
The MCP chat_send_message tool calls broadcastMessage() immediately after inserting to the database, ensuring the UI receives the message in real time without waiting for the 10-second background poll fallback.
Claw’d uses two separate SQLite databases, both in WAL mode for concurrent read/write.
Location: ~/.clawd/data/chat.db
This is the primary database for all chat, agent, and scheduling state.
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | Channel identifier |
name |
TEXT | Display name |
created_by |
TEXT | Creator user/agent ID |
| Column | Type | Description |
|---|---|---|
ts |
TEXT PK | Timestamp (message ID) |
channel |
TEXT | Channel the message belongs to |
user |
TEXT | Sender (user or agent ID) |
text |
TEXT | Message content (Markdown) |
agent_id |
TEXT | Agent that generated this message (nullable) |
subspace_json |
TEXT | Sub-agent space metadata (nullable) |
tool_result_json |
TEXT | Tool execution result (nullable) |
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | File identifier |
name |
TEXT | Storage filename |
mimetype |
TEXT | MIME type |
size |
INTEGER | File size in bytes |
path |
TEXT | File storage path |
message_ts |
TEXT | Associated message timestamp |
uploaded_by |
TEXT | User who uploaded the file |
created_at |
TEXT | Creation timestamp |
public |
INTEGER | Whether the file is publicly accessible |
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | Agent identifier |
channel |
TEXT | Home channel |
avatar_color |
TEXT | Display color |
display_name |
TEXT | Human-readable name |
is_worker |
INTEGER | Whether this is a worker agent |
is_sleeping |
INTEGER | Whether the agent is hibernating |
| Column | Type | Description |
|---|---|---|
channel |
TEXT | Channel ID |
agent_id |
TEXT | Agent ID |
provider |
TEXT | LLM provider for this assignment |
model |
TEXT | LLM model for this assignment |
project |
TEXT | Project/workspace path |
worker_token |
TEXT | Remote worker auth token (nullable) |
| Column | Type | Description |
|---|---|---|
agent_id |
TEXT | Agent ID |
channel |
TEXT | Channel ID |
last_seen_ts |
TEXT | Last message the agent observed |
last_processed_ts |
TEXT | Last message the agent acted on |
last_poll_ts |
TEXT | Last poll timestamp |
| Column | Type | Description |
|---|---|---|
agent_id |
TEXT | Agent ID |
channel |
TEXT | Channel ID |
status |
TEXT | Current status |
hibernate_until |
TEXT | Wake-up timestamp (nullable) |
| Column | Type | Description |
|---|---|---|
channel |
TEXT | Channel ID |
agent_id |
TEXT | Agent that created the summary |
summary |
TEXT | Compressed context summary |
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | Space identifier |
channel |
TEXT | Parent channel |
space_channel |
TEXT | Isolated sub-channel (format: {parent}:{uuid}) |
title |
TEXT | Space task description |
status |
TEXT | Status (active, completed, failed, timed_out) |
| Table | Purpose |
|---|---|
articles |
Knowledge articles |
copilot_calls |
API call analytics and tracking |
Location: ~/.clawd/data/kanban.db
| Table | Purpose |
|---|---|
tasks |
Channel-scoped tasks (status, assignee, priority, due dates) |
plans |
Plan documents with phases |
phases |
Plan phases/milestones |
plan_tasks |
Tasks linked to plan phases |
Location: ~/.clawd/data/scheduler.db
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | Job identifier |
channel |
TEXT | Channel the job belongs to |
title |
TEXT | Job description |
type |
TEXT | Schedule type: once, interval, cron, reminder, or tool_call |
cron_expr |
TEXT | Cron expression (for cron type) |
Location: ~/.clawd/memory.db
This database stores all LLM session context, knowledge retrieval data, and long-term agent memories.
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | Session identifier |
name |
TEXT | Session name (format: {channel}-{agentId}) |
model |
TEXT | LLM model used |
created_at |
INTEGER | Creation timestamp |
updated_at |
INTEGER | Last update timestamp |
| Column | Type | Description |
|---|---|---|
session_id |
TEXT | Foreign key to sessions |
role |
TEXT | Message role (system, user, assistant, tool) |
content |
TEXT | Message content |
tool_calls |
TEXT | JSON-encoded tool call array (nullable) |
tool_call_id |
TEXT | Tool call ID for tool results (nullable) |
FTS5 full-text search index on messages.content for fast session search.
| Column | Type | Description |
|---|---|---|
id |
INTEGER PK | Chunk identifier |
session_id |
TEXT | Session the chunk belongs to |
source_id |
TEXT | Source identifier |
tool_name |
TEXT | Tool that produced this chunk |
chunk_index |
INTEGER | Index of this chunk within the source |
content |
TEXT | Tool output text chunk for retrieval |
created_at |
TEXT | Creation timestamp |
FTS5 full-text search index on knowledge.content.
| Column | Type | Description |
|---|---|---|
agent_id |
TEXT | Agent that owns this memory |
content |
TEXT | Long-term fact, preference, or decision |
channel |
TEXT | Channel context for this memory |
category |
TEXT | Memory category |
source |
TEXT | How this memory was created |
access_count |
INTEGER | Number of times this memory was retrieved |
last_accessed |
TEXT | Last retrieval timestamp |
created_at |
TEXT | Creation timestamp |
updated_at |
TEXT | Last update timestamp |
FTS5 full-text search index on agent_memories.content.
File: src/db/migrations.ts (and src/db/migrations/)
All five databases use a unified migration runner based on PRAGMA user_version:
runMigrations(db, migrations, strategy?)
versioned (default) — runs only migrations with version > PRAGMA user_version, then bumps the version. Safe for production data.recreate-on-mismatch — for cache-style DBs (e.g. skills-cache.db): if current version < target, drops all tables and re-runs from scratch.Each migration is a { version, description, up(db) } object. Migrations run atomically inside db.transaction() so a partial migration never leaves the DB corrupt.
Per-DB migration files:
| File | Database |
|---|---|
src/db/migrations/chat-migrations.ts |
chat.db |
src/db/migrations/memory-migrations.ts |
memory.db |
src/db/migrations/scheduler-migrations.ts |
scheduler.db |
src/db/migrations/kanban-migrations.ts |
kanban.db |
src/db/migrations/skills-cache-migrations.ts |
skills-cache.db |
File: src/worker-loop.ts
Each agent runs its own WorkerLoop instance, managed by WorkerManager:
flowchart TD
subgraph WM["WorkerManager (src/worker-manager.ts)"]
WL1["WorkerLoop (agent-1)\npoll every 200ms"]
WL2["WorkerLoop (agent-2)\npoll every 200ms"]
WLN["WorkerLoop (agent-N)\npoll every 200ms"]
end
subgraph EachLoop["Each Loop"]
S1["1. Check for new messages in channel"]
S2["2. Build prompt (system + context + tools)"]
S3["3. Call LLM (streaming)"]
S4["4. Parse response → execute tool calls"]
S5["5. Post results back to channel"]
S6["6. Repeat until no pending messages"]
end
WL1 --> S1
WL2 --> S1
WLN --> S1
S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S1
Key behaviors:
File: src/agent/agent.ts
The Agent class implements the core reasoning loop:
flowchart TD
LLM["LLM Call (streaming)"]
Text["Text response → post to channel"]
ToolCalls["Tool calls → parse → execute each tool"]
ToolResult["Tool result → inject into context"]
Continue["Continue loop (call LLM again with results)"]
LLM --> Text
LLM --> ToolCalls
ToolCalls --> ToolResult
ToolResult --> Continue
Continue --> LLM
Each iteration:
agent_token eventsbeforeExecute / afterExecute hooks; read-only tools run in parallel via Promise.all; write tools run sequentiallytool role messages, reassembled in original LLM call orderToolDefinition has an optional readOnly?: boolean flag. When set to true, the tool is safe to run concurrently. At execution time:
readOnly=true tools from a single LLM response execute via Promise.all in one batchreadOnly=false) execute sequentially after all read-only batchesparallelTools in config (default: true); set to false to force sequential16 built-in tools are marked readOnly: true: file view/glob/grep, web fetch/search, today, get_environment, list_agents, and several memory-read tools.
File: src/agent/prompt/builder.ts
System prompts are dynamically assembled from 12 conditional sections based on agent configuration and environment:
| Section | Condition | Token Impact |
|---|---|---|
| Identity | Always | ~150 tokens |
| Environment | Always | ~50 tokens |
| Tool Usage | Always | ~100 tokens |
| Output Efficiency | Always | ~80 tokens |
| Safety | Always | ~100 tokens |
| Chat Communication | Main agents only | ~100 tokens |
| Git Rules | If git tools available | ~80 tokens |
| Sub-Agent Guidance | If spawn_agent available | ~120 tokens |
| Task Management | If task tools available | ~40 tokens |
| Artifacts | Main agents only | ~60 tokens |
| Browser Tools | If browser enabled | ~50 tokens |
| Context Awareness | Always | ~40 tokens |
| Sub-Agent Instructions | Sub-agents only | ~80 tokens |
Token budget:
The agent maintains a token budget with dynamic thresholds (contextMode=true) or legacy fixed values:
| Level | Dynamic | Legacy | Action |
|---|---|---|---|
| Normal | <50% effective | <32K | Full session history kept |
| Warning | 50-70% | 32-50K | Soft compaction begins |
| Critical | 70-85% | 50-70K | Aggressive pruning + summarization |
| Emergency | >85% | >70K | Full LLM-generated summary, reset |
Smart message scoring (from message-scoring.ts):
Full reset trigger: When tokens exceed 95% of raw model limit (safety margin). Full reset uses a two-phase approach: (1) aggressive compaction keeping last 15 messages, (2) only if still over critical threshold, full session reset with LLM-generated summary (4096 max output tokens, model-aware input budget).
Hybrid history: Last 20 messages kept in full; older messages stored compact form for replay.
Heartbeat handling: [HEARTBEAT] and <agent_signal> messages dropped automatically during compaction, never persisted.
Agents are extended through two plugin interfaces:
interface ToolPlugin {
getTools(): ToolDefinition[] // Register available tools
beforeExecute?(call): boolean // Pre-execution hook (can block)
afterExecute?(call, result): void // Post-execution hook
}
interface Plugin {
onUserMessage?(message): void // React to user messages
onToolCall?(call): void // React to tool executions
getSystemContext?(): string // Inject into system prompt
// ... additional lifecycle hooks
}
| Plugin | File | Purpose |
|---|---|---|
browser-plugin |
plugins/browser-plugin.ts |
Browser automation tools via extension bridge |
workspace-plugin |
plugins/workspace-plugin.ts |
File system and project workspace tools |
context-mode-plugin |
plugins/context-mode-plugin.ts |
Toggle between action and context-only modes |
state-persistence-plugin |
plugins/state-persistence-plugin.ts |
Save/restore agent state across restarts |
tunnel-plugin |
plugins/tunnel-plugin.ts |
Expose local services via tunnels |
spawn-agent-spaces |
spaces/spawn-plugin.ts |
Sub-agent spawning via spaces system |
The memory system uses three separate stores, each serving distinct retrieval needs:
flowchart LR
subgraph MM["MemoryManager (memory.ts)"]
MMA["Session chat history"]
MMB["FTS5-indexed messages"]
MMC["Context compaction"]
end
subgraph KB["KnowledgeBase (knowledge-base.ts)"]
KBA["Tool output indexing"]
KBB["FTS5 chunk retrieval"]
KBC["BM25 ranking"]
end
subgraph AMS["AgentMemoryStore (agent-memory.ts)"]
AMSA["Per-agent long-term memory"]
AMSB["Categories: fact/preference/decision/lesson/correction"]
AMSC["Priority decay + effectiveness scoring"]
end
MM --> KB
KB --> AMS
MemoryManager (src/agent/memory/memory.ts): Manages session chat history with FTS5 full-text search. Stores raw conversation in memory.db → messages. Subject to context compaction at token thresholds.
KnowledgeBase (src/agent/memory/knowledge-base.ts): Indexes tool outputs (file contents, command results, web pages) as chunks in memory.db → knowledge with FTS5 indexing. Enables recall of past tool results without re-execution.
AgentMemoryStore (src/agent/memory/agent-memory.ts): Per-agent long-term memories with categories:
Supports priority decay, effectiveness scoring, auto-extraction via LLM, and memory consolidation (Phase 3).
File: src/server/mcp.ts (JSON-RPC over HTTP)
The MCP server exposes tools to Claude Code sub-agents via the handleAgentMcpRequest function. Route: /mcp/agent/{channel}/{agentId}
Architecture:
Claude Code Sub-Agent
│
▼ (MCP request with JSON-RPC)
handleAgentMcpRequest(channel, agentId)
│
├── Validates request (OPTIONS/POST only)
├── Extracts JSON-RPC payload
├── Resolves tool name + args
│
▼
Tool Dispatcher
│
├── Chat tools (chat_send_message, chat_poll_and_ack, etc.)
├── File tools (read, write, edit)
├── Agent tools (spawn_agent, list_agents, get_agent_logs, stop_agent)
├── Memory tools (chat_search, memory_summary)
├── Memo tools (memo_save, memo_recall, memo_delete, memo_pin, memo_unpin)
├── Task tools (task_add, task_list, task_complete, etc.)
├── Job tools (job_submit, job_status, job_wait, etc.)
├── Utility tools (get_environment, today, convert_to_markdown)
└── Channel MCP Tools (from connected MCP servers for this channel)
│
▼
HTTP Response (JSON-RPC result or error)
Channel MCP Server Integration:
Connected channel MCP server tools are automatically exposed to Claude Code agents. When a tool list request is made, the MCP server:
WorkerManager.getChannelMcpManager(channel)This enables per-channel MCP server integrations where agents can access channel-specific tools.
Key MCP Tools:
| Tool | Purpose |
|---|---|
chat_send_message |
Post message to channel |
chat_poll_and_ack |
Poll pending messages, acknowledge processed |
spawn_agent |
Spawn Claude Code sub-agent via SDK |
list_agents |
List running sub-agents |
get_agent_logs |
Get sub-agent output logs (tail optional) |
stop_agent |
Stop a running sub-agent |
chat_search |
Search conversation history via FTS5 |
memory_summary |
Get session summary with key topics |
memo_save/recall/delete |
Long-term memory CRUD |
memo_pin/unpin |
Pin memories for always-loading |
task_* |
Kanban task management |
Agent Context Injection:
channel and agentId auto-injected into every tool callFile: src/worker-manager.ts
A background health monitor keeps agents responsive and recovers from stuck states:
Mechanism:
<agent_signal>[HEARTBEAT]</agent_signal> to wake them upConfiguration (in config.json):
"heartbeat": {
"enabled": true, // Enable monitor (default: true)
"intervalMs": 30000, // Check interval (default: 30000)
"processingTimeoutMs": 300000, // Cancel stuck agents after 5 min
"spaceIdleTimeoutMs": 60000 // Sub-agent idle timeout
}
Heartbeat Signal Protocol:
[HEARTBEAT] is sent as a user-role message wrapped in <agent_signal>[HEARTBEAT]</agent_signal>WebSocket Events (all broadcast as type: "agent_heartbeat" with event sub-field):
heartbeat_sent — Heartbeat injected into idle agentprocessing_timeout — Agent cancelled for exceeding processing timeoutspace_auto_failed — Sub-agent space failed after max heartbeat attemptsFile: src/agent/api/client.ts
Stream timeouts are state-based (not model-name-based) to handle different phases of LLM processing:
| State | Timeout | Meaning |
|---|---|---|
| CONNECTING | 30 seconds | Waiting for HTTP response headers (network/connection issues) |
| PROCESSING | 300 seconds | Headers received but no data yet (model thinking, extended reasoning) |
| STREAMING | 180 seconds | Active data streaming; timeout if pause between chunks exceeds limit |
State Transitions:
This approach accommodates slow models (Opus, o1, o3) with extended thinking without hardcoding model-specific timeouts.
Files: src/agent/agent.ts (getIterationModel), src/agent/api/factory.ts
Model Tiering (getIterationModel in agent.ts):
claude-haiku-4.5) when conditions are met:
config.fastModelTool Filtering (filterToolsByUsage in agent.ts):
Prompt Caching:
prompt-caching-2024-07-31 beta header enabledcache_control: { type: "ephemeral" }Recent optimizations across database, query, streaming, and memory systems improve latency, reduce resource usage, and enhance scalability.
SQLite Container-Aware Tuning:
Composite Index:
(channel, ts DESC) composite index on messages tablePeriodic Maintenance:
PRAGMA optimize run asynchronously after bulk operationscopilot_calls table pruned for entries >30 days oldCached Agent Lookup:
getAgent() results cached with 2-second TTLBatch Message Operations:
getMessageSeenBy() batches read tracking queriesResponse Optimization:
toSlackMessage() with safe JSON.parse error handlingToken Batching:
SSE Buffer Fix:
Consolidated Broadcasting:
agent_poll WebSocket messages into single broadcast (3→1 messages)Instruction Caching:
loadClawdInstructions() cached with invalidation on file changesFile List Caching:
listAgentFiles() cached with 60-second TTLTool Name Caching:
Content-Based Token Cache:
Adaptive Interrupt Polling:
getRecentContext Fix:
Browser Bridge Heartbeat:
.unref() to not keep process aliveAsync File Upload:
FTS5 Optimization:
PRAGMA optimize run after bulk knowledge base insertionsContext Tracker Map Caps:
The Chrome browser extension is the primary mechanism for agent browser automation. It connects to the clawd server via WebSocket and executes browser commands on behalf of agents.
flowchart LR
subgraph Ext["Chrome Browser Extension (MV3)"]
SW["service-worker.js (~2700 lines)\nCommand dispatcher\nCDP mode (chrome.debugger API)\nStealth mode (scripting API)"]
OS["offscreen.js\nWebSocket connection maintainer\nWS ping every 20s\nSW keepalive every 25s"]
CS["content-script.js\nDOM extraction + interaction"]
SH["shield.js\n(MAIN world, document_start)\nAnti-detection patches"]
SW --> OS
OS -->|WebSocket| CS
end
Server["Claw'd Server (browser-bridge.ts)\n/browser/ws endpoint"]
OS -->|WebSocket| Server
Communication flow:
offscreen.js maintains a persistent WebSocket to the server at /browser/wsservice-worker.js dispatches commands to the appropriate handler (CDP or stealth)25+ command types are supported: navigate, screenshot, click, type, execute, scroll, hover, select, drag, upload, accessibility tree, tab management, and more.
Normal mode uses the Chrome DevTools Protocol via chrome.debugger API for precise,
full-featured browser control.
Capabilities:
| Feature | Implementation |
|---|---|
| Screenshots | CDP Page.captureScreenshot — full page or viewport |
| Accessibility tree | CDP Accessibility.getFullAXTree — structured page content |
| Click | CDP Input.dispatchMouseEvent — precise coordinate clicks |
| Type | CDP Input.dispatchKeyEvent — keystroke simulation |
| File upload | CDP DOM.setFileInputFiles — programmatic file picker |
| Drag and drop | CDP Input.dispatchDragEvent — native drag simulation |
| Touch events | CDP Input.dispatchTouchEvent — mobile simulation |
| Device emulation | CDP Emulation.setDeviceMetricsOverride — viewport + UA |
| JavaScript execution | CDP Runtime.evaluate — arbitrary JS in page context |
Trade-off: CDP attaches a debugger to the tab, which is detectable by anti-bot systems (Cloudflare, DataDome, PerimeterX, etc.).
Stealth mode uses chrome.scripting.executeScript() instead of CDP, making automation
invisible to anti-bot detection systems.
How it works:
navigator.webdriver stays falseel.click() produces isTrusted=true events (native browser behavior)buttons, pointerType, view properties_valueTracker resetpointerdown → mousedown → pointerup → mouseup → clickAvailable in stealth mode:
| Feature | Status |
|---|---|
| Navigate | ✅ |
| Screenshot | ✅ |
| Click | ✅ (isTrusted=true) |
| Type/input | ✅ (native setter + event dispatch) |
| Scroll | ✅ |
| Hover | ✅ |
| JavaScript execution | ✅ |
| Select dropdown | ✅ |
| Tab management | ✅ |
NOT available in stealth mode:
| Feature | Reason |
|---|---|
| File upload | Requires CDP DOM.setFileInputFiles |
| Accessibility tree | Requires CDP Accessibility.getFullAXTree |
| Drag and drop | Requires CDP Input.dispatchDragEvent |
| Touch events | Requires CDP Input.dispatchTouchEvent |
| Device emulation | Requires CDP Emulation.setDeviceMetricsOverride |
File: packages/browser-extension/src/shield.js
The shield runs in the MAIN world at document_start — before any page JavaScript
executes. It patches browser APIs to prevent detection of automation:
| Patch | What It Does |
|---|---|
navigator.webdriver |
Forces false via property redefinition |
| DevTools detection | Patches console.clear as no-op; spoofs outerHeight/outerWidth |
Function.prototype.toString |
Returns original native function strings for patched APIs |
performance.now() timing |
Normalizes to prevent timing-based detection fingerprinting |
Date.now() / Date constructor |
Patches to prevent timing-based detection |
requestAnimationFrame |
Patches to prevent frame-timing detection |
| Debugger trap neutralization | Prevents debugger statement traps from detecting automation |
chrome.csi / chrome.loadTimes |
Spoofs Chrome-specific API fingerprints |
The browser extension is not installed from a store. Instead:
scripts/zip-extension.ts packs the extension directory into a zip archivesrc/embedded-extension.ts/browser/extensionFiles: packages/ui/src/artifact-*.tsx, packages/ui/src/chart-renderer.tsx
Agents output structured content using <artifact> tags for rich visualization in the UI:
8 Artifact Types:
| Type | Rendering | Security |
|---|---|---|
html |
Sandboxed iframe | DOMPurify sanitization |
react |
Babel + Tailwind sandbox | No direct DOM access |
svg |
Inline with sanitization | DOMPurify + rehype-sanitize |
chart |
Recharts (line/bar/pie/area/scatter/composed) | No network access |
csv |
Sortable HTML table | Escaped content |
markdown |
Full markdown + syntax highlighting | rehype-sanitize |
code |
Prism syntax highlighting (32+ languages) | Read-only display |
Sandbox Model:
<iframe sandbox="allow-scripts"> (no external network, DOM access, or cookie leakage)<iframe> access isolated from parent page originRendering Locations:
Chart Format:
{
"type": "line",
"data": [{"month": "Jan", "value": 100}],
"xKey": "month",
"series": [{"key": "value", "name": "Series 1"}],
"title": "Title"
}
Max 1000 data points, 10 series per chart.
The Spaces system allows agents to delegate tasks to isolated sub-agents that run in parallel.
sequenceDiagram
participant PA as Parent Agent
participant SS as Spaces System
participant SA as Sub-Agent
PA->>SS: spawn_agent(task, agent="code-reviewer")
SS->>SA: Create isolated channel {parent}:{uuid}
SS->>SA: Load agent file config (model, tools, system prompt, directives)
SS->>SA: Start new WorkerLoop (inherits provider/model if not overridden)
Note over SA: ... working ...
SA->>SS: complete_task(result)
SS->>PA: Result posted to parent channel + space locked
Note over SS: Space status → completed
Key details:
agent="code-reviewer" loads a specific agent file (system prompt, model, tools, directives). Without it, sub-agent inherits parent’s configuration{parent}:{uuid}) so conversations don’t interferespawn_agent overrides to 600 secondscontext parameter to reduce sub-agent cold startcomplete_task(result) which posts the result to the parent channel and locks the space (preventing further messages)complete_task, chat_mark_processed, get_environment, today — no chat_send_message or other toolsParent tools for sub-agents: retask_agent allows re-tasking a completed sub-agent without cold start.
Space statuses: active → completed |
failed |
timed_out |
Files: src/spaces/worker.ts, src/spaces/manager.ts
Sub-agents are spawned via @anthropic-ai/claude-agent-sdk. The SDK is embedded in the compiled binary (gzip-compressed) and auto-extracted to ~/.clawd/bin/cli.js on first use.
| Aspect | Previous (Raw Bun.spawn) | Current (SDK) |
|---|---|---|
| Binary | claude CLI installed separately |
Embedded in clawd binary, auto-extracted |
| Dependencies | claude + Node.js on PATH |
Only bun required on PATH |
| Hooks | Temp scripts in /tmp |
Programmatic hooks (PreToolUse, PostToolUse) |
| Interrupts | proc.kill() signal |
AbortController-based cancellation |
| Session Management | Manual retry logic | SDK auto-retry on stale sessions |
| Model Caps | No restrictions | Sonnet max; opus prohibited |
| Session Resets | Manual on provider change | Auto-reset on provider or model change |
When a sub-agent wakes from sleep with >3 accumulated messages, the SDK:
Sub-agents respect the parent’s provider configuration but can be overridden per sub-agent:
// Parent uses OpenAI
const subAgent = await spawnAgent({
agent: "code-reviewer",
provider: "openai", // Optional override
model: "gpt-4" // Optional override
});
Files: src/scheduler/manager.ts, src/scheduler/runner.ts, src/scheduler/parse-schedule.ts
The scheduler creates and manages recurring or one-time jobs:
| Job Type | Behavior |
|---|---|
cron |
Runs on a cron schedule (e.g., 0 9 * * 1-5 for weekday 9 AM) |
interval |
Runs every N seconds/minutes/hours |
once |
Runs once at a specific time |
| Reminder | Posts a message without creating a sub-space |
| Tool call | Executes a tool directly without agent involvement |
Execution flow:
parse-schedule.ts (e.g., “every weekday at 9am”)Files: src/spaces/worker.ts, src/agent/agents/identity.ts
Claude Code agents integrate with @anthropic-ai/claude-agent-sdk for sub-agent spawning via Claw’d’s main channel:
Sub-agents receive full identity context matching Copilot agents:
~/.claude/CLAUDE.md) — Lowest priority{project}/.claude/CLAUDE.md)src/agent/agents/config/claude-code.yaml)~/.clawd/agents/{name}.md or {project}/.clawd/agents/{name}.md) — Highest priorityAuto-refresh: When any CLAWD.md file is modified (mtime check), identity automatically refreshes.
System prompt injection: PROJECT ROOT path injected into sub-agent system prompt.
SDK receives settings from Claw’d config:
skip_co_author — Don’t append Co-Authored-By trailersattribution — Include “Generated with [Claude Code](…” attributionpermissions — Tool access restrictionsSupport for custom claude-code providers (e.g., "claude-code-2" with type "claude-code"):
Claude Code uses PascalCase native tool names (e.g., Read, Write, Edit). CC sub-agents spawn with a CLAWD_TOOL_NAME_MAP that automatically translates these to Claw’d MCP equivalents before the SDK call:
| CC Native Name | Claw’d MCP Name |
|---|---|
Read |
mcp__clawd__file_view |
Write |
mcp__clawd__file_create |
Edit |
mcp__clawd__file_edit |
MultiEdit |
mcp__clawd__file_multi_edit |
Bash |
mcp__clawd__bash |
Grep |
mcp__clawd__file_grep |
Glob |
mcp__clawd__file_glob |
The map also accepts Claw’d short names (e.g., view, edit, multi_edit, create, glob, grep, bash, web_fetch, web_search, custom_script) — these resolve to their mcp__clawd__* equivalents.
Native CC tools not in this map (e.g., TaskOutput, TaskStop, AskUserQuestion, EnterPlanMode, ExitPlanMode, EnterWorktree, ExitWorktree, RemoteTrigger) are blocked via disallowedTools in runSDKQuery.
| Issue | Fix | Impact |
|---|---|---|
| False “error result: success” | Check subtype vs is_error flag | Proper error detection |
| Stale streaming cleanup | onActivity refreshes streaming_started_at | No zombie streams |
| Heartbeat timeout | onActivity refreshes processingStartedAt | Better responsiveness |
| Sleeping agent polling | userSleeping flag stops polling | No wasted cycles |
Tool execution is sandboxed to prevent agents from accessing sensitive host resources. The sandbox implementation differs by platform.
Enforcement rules (Wave 1–2 hardening):
sandboxRequired: true — Tools that set this flag in their ToolDefinition will refuse to execute if called outside the sandbox. The executeTool dispatcher checks this flag before invocation.chat_upload_local_file — Uses realpathSync to resolve the full real path before checking it against a project-root allowlist. Prevents symlink-traversal uploads./api/* and /mcp routes enforce channel-scoped auth. When a ?channel= param is present, only its matching patterns are checked. Without ?channel=, auth is only enforced if a global "*" catch-all pattern is configured. /mcp/agent/ and /mcp/space/ are exempt (agent-internal paths). The internal service token (INTERNAL_SERVICE_TOKEN, generated at startup) bypasses auth entirely for in-process self-calls."*" catch-all is configured. Channel-scoped-only deployments allow WS without a token, since WS has no channel context at upgrade time.Uses bubblewrap (bwrap) for
filesystem isolation via bind mounts and a clean environment:
Uses sandbox-exec with Seatbelt profiles:
| Access | Paths |
|---|---|
| Read + Write | {projectRoot}, /tmp |
| Read + Write (macOS only) | ~/.clawd |
| Read only | /usr, /bin, /lib, /etc, ~/.bun, ~/.cargo, ~/.deno, ~/.nvm, ~/.local |
| Read only (Linux bwrap) | ~/.clawd/bin, ~/.clawd/.ssh, ~/.clawd/.gitconfig |
| Blocked | {projectRoot}/.clawd/ (agent config directory) |
| Blocked | Home directory (except explicitly allowed tool directories) |
Environment handling:
~/.clawd/.env are passed throughTMPDIR/TEMP/TMP set to /tmp for Bun compatibilityDEBIAN_FRONTEND=noninteractive, HOMEBREW_NO_AUTO_UPDATE=1, PIP_NO_INPUT=1, CONDA_YES=1~/.bun/install mounted read-write (overrides the read-only ~/.bun mount).clawd/skills/, .clawd/tools/, .clawd/files/ re-mounted read-only as exceptions to the blocked .clawd/ ruleMulti-agent file isolation via git worktrees. When enabled, each agent in a channel gets its own isolated working directory with a dedicated branch (clawd/{randomId}), enabling concurrent edits without conflicts.
Files: src/agent/workspace/worktree.ts (lifecycle), src/api/worktree.ts (18 REST endpoints), src/worker-manager.ts (DB persistence), UI components (WorktreeDialog, worktree-diff-viewer, worktree-file-list)
Location: {projectRoot}/.clawd/worktrees/{agentId}/
Branch Naming: clawd/{6-char-hex} (e.g., clawd/a1b2c3)
Creation Flow:
createWorktree(projectPath, agentId) checks if valid git repo existsgit worktree add -b {branchName} {worktreePath}{ path, branch }Reuse on Restart:
channel_agents.worktree_path + worktree_branch persisted in DBgit rev-parse --abbrev-ref HEAD)Deletion:
safeDeleteWorktree() checks for uncommitted changes first{ deleted: true } or { deleted: false, reason: "has_uncommitted_changes" }.git is corruptCritical: Agents are NOT aware they’re in a worktree.
sectionWorktree === sectionGit).git/ is mounted read-only"worktree": true in config.json → enable for all channels"worktree": ["ch1", "ch2"] → enable only for listed channels"worktree": false or omitted → disabled (use project root directly)Humans control integration:
isGitRepo(path) returns false → git worktree isolation skipped{ enabled: false } or 404Read Endpoints (GET):
app.worktree.enabled?channel=X — Returns { enabled: true|false }
true if worktree enabled for channel OR any agent has git repoapp.worktree.status?channel=X — Returns array of agents with:
[{
"agent_id": "agent1",
"branch": "clawd/a1b2c3",
"base_branch": "main",
"worktree_path": "/project/.clawd/worktrees/agent1",
"original_project": "/project",
"clean": true,
"ahead": 3,
"behind": 0,
"has_conflicts": false,
"merge_in_progress": false,
"files": {
"staged": ["file1.ts"],
"modified": ["file2.ts"],
"untracked": [],
"deleted": [],
"conflicted": []
}
}]
app.worktree.diff?agent_id=X&file_path=path&source=unstaged|staged — Returns FileDiff:
{
"path": "src/app.ts",
"status": "M",
"binary": false,
"additions": 10,
"deletions": 2,
"hunks": [{
"header": "@@ -10,5 +10,7 @@",
"oldStart": 10,
"oldLines": 5,
"newStart": 10,
"newLines": 7,
"hash": "sha1_of_hunk_content",
"lines": [
{"type": "context", "content": "existing line"},
{"type": "addition", "content": "new line", "newNo": 11},
{"type": "deletion", "content": "old line", "oldNo": 12}
]
}]
}
app.worktree.log?agent_id=X[&lines=50] — Git log for agent’s branch (commit hash, author, date, message)Write Endpoints (POST):
File Staging:
app.worktree.stage — Stage file: { agent_id, file_path }app.worktree.unstage — Unstage file: { agent_id, file_path }app.worktree.discard — Discard working tree changes: { agent_id, file_path }Per-Hunk Granular Control:
app.worktree.stage_hunk — Stage single hunk: { agent_id, file_path, hunk_hash }
{ ok: true, remainingHunks: N } or { ok: false, error: "hunk_not_found" } (409-equivalent)git apply --cached to apply minimal patchapp.worktree.unstage_hunk — Unstage single hunk: { agent_id, file_path, hunk_hash }app.worktree.revert_hunk — Discard hunk from working tree: { agent_id, file_path, hunk_hash }Commit & Push:
app.worktree.commit — Create commit: { agent_id, message }
-c flags)app.worktree.push — Push branch: { agent_id }
Conflict Resolution:
app.worktree.merge — Merge base branch: { agent_id }
app.worktree.resolve — Mark file as resolved: { agent_id, file_path }app.worktree.abort — Abort merge: { agent_id }Stash:
app.worktree.stash — Stash changes: { agent_id }app.worktree.stash_pop — Pop stash: { agent_id }Integration:
app.worktree.apply — Apply worktree branch to base: { agent_id, strategy="merge"|"cherry-pick" }Per-hunk staging identifies hunks using SHA1 content hashing (not index-based):
GET .../app.worktree.diff?agent_id=X&file_path=path&source=unstaged|stagedhash = SHA1(hunkHeader + lines.join('\n'))
e.g., hunkRawLines = ["@@ -10,5 +10,7 @@", " context", "+new", "-old", ...]
hunk_hash in body
{ "agent_id": "agent1", "file": "src/app.ts", "hunk_hash": "abc123def456" }
{ ok: false, error: "hunk_not_found" } with HTTP 409 Conflictgit apply --cached (or git apply -R --unidiff-zero for revert){ ok: true, remainingHunks: N } where remainingHunks = other unstaged hunks in same fileConfiguration (~/.clawd/config.json):
{
"worktree": true, // or ["channel1", "channel2"]
"author": {
"name": "Claw'd Agent",
"email": "agent@clawd.local"
}
}
Priority:
config.author injected via git interpret-trailers --trailer "Co-Authored-By: ..."config.author injected via -c user.name=... -c user.email=... git flagsconfig.author missingTools running in agent sandbox are wrapped with guards:
| Tool | Guard | Behavior |
|---|---|---|
git commit |
Author validation | Automatically injects Co-Authored-By trailer or -c flags |
git push |
Branch protection | Blocks push to main, master, develop (only clawd/* allowed) |
git checkout |
Branch locking | Prevents checkout to other branches (agents stay on assigned branch) |
git pull |
Disabled | Blocks pull (worktrees ephemeral; use merge/apply instead) |
Mounted in sandbox:
.git/ directory: Read-only — agents can examine history but not modify.git/ directory: Fully writable — agents can commit, stage, stash in worktree.clawd/ directory: Blocked — protects agent config and identity filesWorktree constraints:
.clawd/ tmpfs barrier){projectRoot}’s git stateWorktreeDialog.tsx — “Git” dialog (unified interface for worktree or direct repo):
worktree-diff-viewer.tsx — Diff renderer:
worktree-file-list.tsx — File tree sidebar:
channel_agents table tracks:
worktree_path TEXT, -- Path to worktree dir (.clawd/worktrees/{agentId})
worktree_branch TEXT -- Assigned branch (clawd/a1b2c3)
Reused on server restart to avoid orphaned branches and recreating worktrees.
External machines can connect to the clawd server as remote tool providers, extending an agent’s capabilities across multiple hosts.
flowchart LR
subgraph RM["Remote Machine (worker)"]
CT["Custom tools"]
WT["worker_token auth"]
end
subgraph CS["Claw'd Server"]
RWB["RemoteWorkerBridge"]
TH["SHA256 token hash"]
CA["Channel authz"]
end
RM -->|"WebSocket\n(connect)"| CS
RM -->|"worker:registered event"| RWB
CS -->|"WebSocket\n(commands)"| RM
How it works:
worker_token is configured in channel_agents for a specific agent+channelRemoteWorkerBridge hashes the token (SHA256) and validates itFiles: packages/clawd-worker/typescript/remote-worker.ts, packages/clawd-worker/python/remote_worker.py, packages/clawd-worker/java/RemoteWorker.java
Remote worker scripts support cross-platform execution with special handling for Windows:
Commands executed on Windows hosts use -EncodedCommand with Base64 UTF-16LE encoding to avoid quoting issues:
powershell.exe -EncodedCommandPowerShell exit codes properly propagated via $LASTEXITCODE:
The remote_bash tool description includes platform hints for Windows hosts:
All real-time communication flows through the WebSocket connection at /ws.
| Event | Payload | Description |
|---|---|---|
message |
{ ts, channel, user, text, ... } |
New message posted |
message_changed |
{ ts, channel, text, ... } |
Message edited |
message (with deleted: true) |
{ ts, channel, deleted: true } |
Message removed (sent as regular message event with deleted flag) |
channel_cleared |
{ channel } |
Channel messages cleared |
agent_streaming |
{ agent_id, channel, streaming } |
Agent started/stopped thinking |
agent_token |
{ agent_id, channel, token, type } |
Real-time LLM output (content or thinking type) |
agent_tool_call |
{ agent_id, tool, status } |
Tool execution event (started / completed / error) |
reaction_added |
{ ts, channel, user, reaction } |
Emoji reaction added |
reaction_removed |
{ ts, channel, user, reaction } |
Emoji reaction removed |
message_seen |
{ ts, channel, user } |
Read receipt |
agent_heartbeat |
{ agent_id, channel, event, timestamp } |
Heartbeat events (event: heartbeat_sent, processing_timeout, space_auto_failed) |
Messages are sent via HTTP POST to /api/chat.postMessage. The WebSocket is primarily
a server-to-client push channel — clients send messages via the REST API.
All API endpoints are available at /api/{method} via POST (or GET where noted).
| Endpoint | Method | Description |
|---|---|---|
conversations.list |
GET | List all channels |
conversations.create |
POST | Create a new channel |
conversations.history |
GET | Message history (paginated) |
conversations.replies |
GET | Thread replies for a message |
conversations.search |
GET | Full-text message search |
conversations.around |
GET | Messages around a specific timestamp |
conversations.newer |
GET | Messages newer than a timestamp |
chat.postMessage |
POST | Send a message to a channel |
chat.update |
POST | Edit an existing message |
chat.delete |
POST | Delete a message |
| Endpoint | Method | Description |
|---|---|---|
files.upload |
POST | Upload a file attachment |
files/{id} |
GET | Download/serve a file by ID |
| Endpoint | Method | Description |
|---|---|---|
reactions.add |
POST | Add an emoji reaction to a message |
reactions.remove |
POST | Remove an emoji reaction |
| Endpoint | Method | Description |
|---|---|---|
agent.setStreaming |
POST | Set agent streaming state (thinking/idle) |
agent.streamToken |
POST | Push a streaming LLM token |
agent.streamToolCall |
POST | Push a tool call event |
| Endpoint | Method | Description |
|---|---|---|
agent.markSeen |
POST | Update agent’s read cursor |
agent.setSleeping |
POST | Hibernate or wake an agent |
agent.setStatus |
POST | Set agent status for a channel |
agent.getThoughts |
GET | Get agent’s current thinking/reasoning |
agents.list |
GET | List all registered agents |
agents.info |
GET | Get info about a specific agent |
agents.register |
POST | Register a new agent |
| Endpoint | Method | Description |
|---|---|---|
app.agents.list |
GET | List app-level agent configurations |
app.agents.add |
POST | Add a new agent configuration |
app.agents.remove |
POST | Remove an agent configuration |
app.agents.update |
POST | Update an agent configuration |
app.models.list |
GET | List available LLM models |
app.mcp.list |
GET | List configured MCP servers |
app.mcp.add |
POST | Add an MCP server configuration |
app.mcp.remove |
POST | Remove an MCP server configuration |
| Endpoint | Method | Description |
|---|---|---|
app.project.tree |
GET | Get project file tree |
app.project.listDir |
GET | List files in a directory |
app.project.readFile |
GET | Read a file’s contents |
| Endpoint | Method | Description |
|---|---|---|
analytics/copilot/calls |
GET | Raw API call log |
analytics/copilot/summary |
GET | Usage summary statistics |
analytics/copilot/keys |
GET | API key usage breakdown |
analytics/copilot/models |
GET | Per-model usage statistics |
analytics/copilot/recent |
GET | Recent API calls |
| Endpoint | Method | Description |
|---|---|---|
tasks.list |
GET | List tasks/projects |
tasks.get |
GET | Get a specific task |
tasks.create |
POST | Create a new task |
tasks.update |
POST | Update an existing task |
tasks.delete |
POST | Delete a task |
tasks.addAttachment |
POST | Add an attachment to a task |
tasks.removeAttachment |
POST | Remove an attachment from a task |
tasks.addComment |
POST | Add a comment to a task |
| Endpoint | Method | Description |
|---|---|---|
plans.* |
Various | Plan management CRUD |
| Endpoint | Method | Description |
|---|---|---|
user.markSeen |
POST | Mark messages as seen |
user.getUnreadCounts |
GET | Get unread message counts |
user.getLastSeen |
GET | Get last seen timestamps |
| Endpoint | Method | Description |
|---|---|---|
spaces.list |
GET | List spaces |
spaces.get |
GET | Get a specific space |
| Endpoint | Method | Description |
|---|---|---|
app.skills.list |
GET | List available skills (from 4 source directories) |
app.skills.get |
GET | Get a specific skill with full content |
app.skills.save |
POST | Create or update a skill |
app.skills.delete |
DELETE | Remove a custom skill |
| Endpoint | Method | Description |
|---|---|---|
custom_script |
POST | Create/edit/delete/execute custom scripts |
Read Endpoints (query params: ?channel=...&agent_id=...):
| Endpoint | Method | Description |
|---|---|---|
app.worktree.enabled |
GET | Check if worktree is enabled for this channel |
app.worktree.status |
GET | Get worktree info for all agents in channel (status, branch, diffs) |
app.worktree.diff |
GET | Get unified diff for a specific file (params: file, source=unstaged\|staged) |
app.worktree.log |
GET | Get commit log for agent’s worktree branch (param: limit=20) |
Write Endpoints (POST with channel, agent_id in body):
| Endpoint | Description |
|---|---|
app.worktree.stage |
Stage files for commit (paths: string[]) |
app.worktree.unstage |
Unstage files (paths: string[]) |
app.worktree.discard |
Discard changes (paths: string[], confirm: true) |
app.worktree.commit |
Create commit (message: string) |
app.worktree.merge |
Merge another agent’s branch (source_agent_id: string) |
app.worktree.resolve |
Resolve merge conflicts (path: string, resolution: “ours”|”theirs”|”both”) |
app.worktree.abort |
Abort in-progress merge/rebase |
app.worktree.apply |
Merge worktree branch into base branch (strategy: “merge”|”squash”) |
app.worktree.stash |
Stash changes (message?: string) |
app.worktree.stash_pop |
Pop stashed changes |
app.worktree.push |
Push worktree branch to remote (remote: “origin”) |
app.worktree.stage_hunk |
Stage a specific diff hunk (file: string, hunk_hash: string) |
app.worktree.unstage_hunk |
Unstage a specific hunk (file: string, hunk_hash: string) |
app.worktree.revert_hunk |
Revert a specific hunk (file: string, hunk_hash: string) |
| Endpoint | Method | Description |
|---|---|---|
/mcp |
GET/POST | MCP SSE endpoint for external clients |
/health |
GET | Liveness probe (returns 200) |
/browser/ws |
WS | Browser extension WebSocket bridge |
/browser/extension |
GET | Download packed browser extension |
/browser/files/* |
GET | Serve files for browser extension |
/worker/ws |
WS | Remote worker WebSocket bridge |
auth.test |
POST | Validate authentication |
auth.channel |
GET/POST | Pre-auth endpoint. GET: probe whether a channel requires a token. POST: validate a token for a channel. |
channel.status |
GET | Get channel status summary |
config/reload |
POST | Reload config.json without restart |
keys/status |
GET | API key health status |
keys/sync |
POST | Sync API keys |
admin.migrateChannels |
POST | Migrate channel data |
admin.renameChannel |
POST | Rename a channel |
articles.* |
Various | Knowledge article CRUD |
Claw’d is provider-agnostic — agents can use any supported LLM provider, configured per-channel or globally.
| Provider | API Type | Notes |
|---|---|---|
copilot |
GitHub Copilot API | Recommended default; uses GitHub token |
openai |
OpenAI API | GPT-4o, o1, o3, etc. |
anthropic |
Anthropic API | Claude Opus, Sonnet, Haiku |
ollama |
Ollama API | Local models via Ollama |
minimax |
Minimax API | Image generation and other capabilities |
Each provider is configured in the providers section of ~/.clawd/config.json:
{
"providers": {
"copilot": {
"model": "claude-sonnet-4-5",
"token": "ghp_..."
},
"openai": {
"base_url": "https://api.openai.com/v1",
"api_key": "sk-...",
"model": "gpt-4o"
},
"anthropic": {
"api_key": "sk-ant-...",
"model": "claude-opus-4-5"
},
"ollama": {
"base_url": "http://localhost:11434",
"model": "llama3"
},
"minimax": {
"api_key": "...",
"model": "image-01"
}
}
}
Agents can be assigned different providers per channel via channel_agents.provider and
channel_agents.model. This allows mixing providers — e.g., Claude for code tasks and
GPT for creative writing — in the same instance.
Vision operations (image analysis, generation, editing) use a separate provider
configuration. Supported vision providers: copilot, gemini, minimax.
{
"vision": {
"provider": "copilot",
"model": "gpt-4.1",
"read_image": { "provider": "gemini", "model": "gemini-2.0-flash" },
"generate_image": { "provider": "minimax", "model": "image-01" },
"edit_image": { "provider": "minimax", "model": "image-01" }
}
}
Gemini vision requires GEMINI_API_KEY in ~/.clawd/.env. The system uses a
Gemini → Minimax fallback chain for image generation.
The agent’s tool ecosystem has been expanded to provide complete feature parity between local and remote workers. 28 new tools are available across 5 categories:
schedule.list — List all scheduled jobsschedule.create — Create a cron/interval/once jobschedule.update — Update an existing jobschedule.delete — Delete a scheduled jobtmux.list_sessions — List active tmux sessionstmux.new_session — Create a new tmux sessiontmux.send_keys — Send keys to a session/panetmux.capture_pane — Capture pane outputtmux.kill_session — Terminate a sessiontmux.list_panes — List panes in a sessiontmux.split_window — Split a tmux windowskills.list — List available skills from 4 source directoriesskills.get — Get a specific skill with full contentskills.save — Create or update a skillskills.delete — Remove a custom skillskills.execute — Execute a skill synchronouslyarticles.list — List all knowledge articlesarticles.create — Create a new articlearticles.read — Read an article’s contentarticles.update — Update an articlearticles.delete — Delete an articlearticles.search — Full-text search articlesmemory.recall — Retrieve agent memories by keywordmemory.store — Save a new memory factenv.list — List environment variablespath.resolve — Resolve file pathsrandom.uuid — Generate UUIDstime.now — Get current timestampThese tools are automatically exposed to Claude Code sub-agents and remote worker connections via the MCP endpoint (/mcp), enabling:
Built with React + Vite + TypeScript, the UI is embedded into the server binary at build time and served as a single-page application.
| Component | File | Purpose |
|---|---|---|
App.tsx |
packages/ui/src/App.tsx |
Root: WebSocket connection, state management, channel routing, per-channel auth gate |
MessageList.tsx |
packages/ui/src/MessageList.tsx |
Message rendering, streaming output, space cards |
StreamOutputDialog |
(in MessageList) | Real-time display of agent tool execution output |
auth-fetch.ts |
packages/ui/src/auth-fetch.ts |
Per-channel token storage and authFetch() wrapper |
styles.css |
packages/ui/src/styles.css |
All application styles |
The UI connects to the server via WebSocket at /ws and handles:
agent_token events render LLM output character-by-characteragent_tool_call events show tool execution with started/completed/error statesagent_streaming events show when agents are processingmessage_seen events mark messages as readWhen auth is configured in config.json, the UI enforces access per channel:
App.tsx calls GET /api/auth.channel?channel=<ch> to check whether a token is required (unauthenticated endpoint).requires_auth: true, a token prompt is shown inline. The user enters their token; the UI validates it via POST /api/auth.channel.localStorage under clawd-channel-token-{channel} via setChannelToken() in auth-fetch.ts.authFetch() calls for that channel automatically attach the token as the Authorization header.validateChannels), the UI navigates back to the home page via window.location.replace("/").getChannelToken): channel-specific → global legacy key (clawd-auth-token) → any stored per-channel token (fallback for global endpoints).Startup gate (authGateCompleted) — On mount, App.tsx defers WebSocket connection and channel validation until the initial deep-link channel has been authenticated. If no initial channel is present, the gate completes immediately.
When an article is opened at /articles/{id}, the UI switches to article mode (isArticleMode):
{channel} | {article id} instead of the agent rosterThe article_create tool (available to all agent types) now accepts three mutually exclusive content sources:
content — raw markdown stringfile_id — file uploaded via chat_upload_local_file; file content used as article bodymessage_ts — timestamp of an existing chat message; message text used as article bodyOnly title is required. This applies to the MCP agent endpoint, the REST endpoint (/api/articles.create), and the SDK agent tool.
sanitizeText() to preserve whitespace in code blockswhite-space: pre ensures formatting retentionImage Lightbox:
Composer Icons:
The build process compiles everything into a single self-contained binary.
flowchart TD
Start["bun run build"]
S1["1. Vite builds UI\npackages/ui/ → packages/ui/dist/"]
S2["2. embed-ui.ts\npackages/ui/dist/ → base64 → src/embedded-ui.ts"]
S3["3. zip-extension.ts\npackages/browser-extension/ → zip → base64 → src/embedded-extension.ts"]
S4["4. bun build --compile\nsrc/index.ts → dist/clawd (single binary)"]
Start --> S1 --> S2 --> S3 --> S4
| Command | Output |
|---|---|
bun run dev |
Run server directly from TypeScript (no compile) |
bun run build |
Full build → single-platform binary |
bun run build:all |
Full build → all platform binaries |
bun run build:linux |
Linux x64 binary |
bun run install:local |
Copy binary to ~/.clawd/bin/clawd |
clawd [options]
--host <host> Bind address (default: 0.0.0.0)
-p, --port <n> Port number (default: 3456)
--no-open-browser Don't auto-open browser on start
--yolo Disable sandbox restrictions for agent tools
--debug Enable verbose debug logging
-h, --help Show help
The Dockerfile uses a two-stage build for minimal image size:
Stage 1 — Builder (oven/bun:1):
bun install)bun build --compile)Stage 2 — Runtime (debian:bookworm-slim):
clawd user/health endpoint# compose.yaml
services:
clawd:
image: ghcr.io/Tuanm/clawd:latest
build: .
restart: unless-stopped
ports:
- "3456:3456"
volumes:
- clawd-data:/home/clawd/.clawd
security_opt:
- apparmor=unconfined # Required for bubblewrap sandbox
- seccomp=unconfined
volumes:
clawd-data:
The apparmor=unconfined and seccomp=unconfined security options are required because the bubblewrap sandbox
inside the container needs to create namespaces, which AppArmor and seccomp block by default.
Note: The healthcheck is defined in the Dockerfile (not compose.yaml) using curl -f http://localhost:3456/health.
src/config-file.ts watches ~/.clawd/config.json via fs.watch. When the file is saved,
the config cache is automatically invalidated (200ms debounce). Changes to providers, model
settings, browser auth tokens, and all other browser-side settings apply on the next request
without a server restart.
Location: ~/.clawd/config.json
{
// Server settings
"host": "0.0.0.0",
"port": 3456,
"debug": false,
"yolo": false,
"contextMode": true, // Note: hardcoded to true in code; not actually configurable at runtime
"dataDir": "~/.clawd/data",
"uiDir": "/custom/ui/path",
// Environment variables passed to agent sandbox
"env": {
"KEY": "VALUE"
},
// LLM provider configurations
"providers": {
"copilot": { "model": "claude-sonnet-4-5", "token": "ghp_..." },
"openai": { "base_url": "...", "api_key": "...", "model": "gpt-4o" },
"anthropic": { "api_key": "...", "model": "claude-opus-4-5" },
"ollama": { "base_url": "http://localhost:11434", "model": "llama3" },
"minimax": { "api_key": "...", "model": "image-01" }
},
// Image generation quotas
"quotas": {
"daily_image_limit": 50
},
// Override token limits for models (optional)
"model_token_limits": {
"copilot": { "gpt-4.1": 64000, "gpt-4o": 128000 },
"anthropic": { "claude-opus-4.6": 200000 }
},
// Heartbeat monitor configuration
"heartbeat": {
"enabled": true,
"intervalMs": 30000, // Check agent health every 30s
"processingTimeoutMs": 300000, // Cancel if processing >5min
"spaceIdleTimeoutMs": 60000 // Poke idle sub-agents after 60s
},
// Workspace plugin toggle
// true = all channels, false = disabled, ["channel1"] = specific channels
"workspaces": true,
// Remote worker configuration
// true = accept workers, { "channel": ["token1"] } = per-channel tokens
"worker": true,
// Vision model configuration
"vision": {
"provider": "copilot",
"model": "gpt-4.1",
"read_image": { "provider": "copilot", "model": "gpt-4.1" },
"generate_image": { "provider": "minimax", "model": "image-01" },
"edit_image": { "provider": "minimax", "model": "image-01" }
},
// Browser extension toggle
// true = all channels, false = disabled, ["channel"] = specific channels
// { "channel": ["auth_token"] } = per-channel with auth
"browser": true,
// Memory system configuration
// true = enabled with defaults
// { "provider": "...", "model": "...", "autoExtract": true } = custom config
"memory": true,
// API authentication (optional)
//
// Legacy (global): { "token": "abc" } — treated as { "*": ["abc"] }
// All channels and the WebSocket require this token.
//
// Channel-scoped: keys are glob patterns (* wildcard), values are token arrays.
// Channels not matched by any pattern are openly accessible.
// A "*" catch-all also gates the WebSocket and all global endpoints.
//
// Examples:
// Single global token: { "token": "secret" }
// Per-channel: { "private-*": ["tok1", "tok2"] }
// Mixed: { "private-*": ["tok1"], "*": ["global-tok"] }
//
// The UI prompts for a token when navigating to a protected channel.
// Tokens are stored per-channel in localStorage (key: clawd-channel-token-{ch}).
"auth": { "token": "your-secret-token" },
// Git isolated mode for multi-agent channels
// Each agent gets an isolated worktree branch to prevent file conflicts
// - true = all channels, false = disabled, ["channel"] = specific channels
"worktree": true,
// Author identity for worktree commits
// If git local config has user.name/email: those are main author, this becomes Co-Authored-By
// If git local config is missing: this becomes main author via git -c flags
"author": {
"name": "Claw'd Agent",
"email": "agent@clawd.local"
}
}
~/.clawd/
├── config.json # App configuration (see schema above)
├── .env # Agent environment variables (KEY=VALUE format)
├── .ssh/ # SSH keys for Git operations (id_ed25519)
├── .gitconfig # Git config for agent-initiated Git operations
├── bin/ # Custom binaries added to agent PATH
├── agents/ # Global agent files (Claude Code-compatible)
│ └── {name}.md # Agent definition (YAML frontmatter + system prompt)
├── data/
│ ├── chat.db # Chat messages, agents, channels, spaces
│ ├── kanban.db # Tasks, plans, phases
│ ├── scheduler.db # Scheduled jobs
│ └── attachments/ # Uploaded files & generated images
├── memory.db # Agent session memory, knowledge base, long-term memories
└── mcp-oauth-tokens.json # OAuth tokens for external MCP server connections
| File/Directory | Purpose |
|---|---|
config.json |
Primary configuration — providers, features, server settings |
.env |
Environment variables injected into agent sandbox (e.g., API keys) |
.ssh/ |
SSH keys used by agents for Git clone/push operations |
.gitconfig |
Git user config (name, email) for agent commits |
bin/ |
Custom executables available in agent’s PATH |
agents/ |
Global agent files — Claude Code-compatible markdown with YAML frontmatter |
data/chat.db |
All chat state — messages, agents, channels, spaces |
data/scheduler.db |
Scheduled jobs and execution state |
data/attachments/ |
File storage for uploads and generated images |
memory.db |
LLM session history, knowledge base, long-term agent memories |
mcp-oauth-tokens.json |
Cached OAuth tokens for authenticated MCP server connections |