OpenClaw Architecture Deep Dive
01Overview & first principles
Where Paperclip is a hosted multi-tenant SaaS that spawns external agent CLIs to do its work, OpenClaw is the opposite shape: one host, one operator, one Gateway process, with the agent runtime inside the process. The Gateway is not a router that hands tasks off to a separate model program — it loads the Pi agent core directly and runs the agent loop in-process.
Three commitments shape almost every design decision in the codebase. First, local-first: the Gateway, agent, sessions, memory files, secrets, and skills all live on the user's machine; remote access is opt-in through Tailscale or SSH tunnel. Second, plugin-heavy core: the src/ tree houses generic seams (channels, providers, tools, hooks, sessions), and almost every concrete capability — a model provider, a messaging surface, a channel-specific UI behaviour — lives in extensions/<name>. Third, trusted-operator trust model: there is no per-user authorization boundary inside one Gateway; the security work happens at ingress (pairing, DM policy, sandbox-by-default for non-main sessions) rather than between operators sharing one daemon.
The product story is that an end user connects their phone, their laptop, and their preferred messaging apps to one Gateway, and gets a single agent that answers them everywhere with the same memory and the same skills. The engineering story is what makes that possible: a WebSocket control plane (), an in-process Pi agent runtime (), a workspace contract that injects AGENTS.md, SOUL.md, USER.md into every system prompt (), AgentSkills-compatible skills loaded from six precedence-ordered roots, and a plugin runtime that can supply providers, channels, tools, hooks, context engines, memory backends, and slot-based extensions — all behind one typed WS handshake.
What this document covers
This is an engineering reference, not a marketing tour. Each chapter walks one subsystem from the source: the line numbers, the actual prompt strings, the protocol shape, the queue and lane behaviour, the failure modes. Every file reference is clickable — tap it and the modal shows the relevant excerpt and path so you can verify the claim yourself. A short comparison with Paperclip sits in the reference appendix, since you have its sibling document.
02Codebase map
OpenClaw checks out as roughly eighteen thousand files. The interesting structure is not the count but the shape: a lean core, an enormous extensions tree, a parallel skills tree, and a thick docs tree that is treated as a first-class deliverable.
Top-level directories
The repository decomposes into roughly eight first-class trees, each with a sharply different purpose:
| Directory | Purpose | Edited when… |
|---|---|---|
src/ | Core TypeScript: Gateway server, agent loop, channel registry, plugin loader, session store, queue. Around 70 sub-directories. | Changing protocol, agent loop, prompt assembly, queue, or core seams. |
extensions/ | Every concrete capability ships as a plugin: 53-ish model providers, 30-plus channels, plus browser, canvas, memory backends, observability, ACPX. | Adding a provider/channel; changing provider-specific behaviour. |
skills/ | Bundled AgentSkills-compatible skill folders the agent loads on demand (gh-issues, notion, obsidian, tmux, weather, coding-agent…). | Editing how the agent uses an external tool/service. |
packages/ | Public SDKs: plugin-sdk, sdk, memory-host-sdk, plugin-package-contract. | Changing the surface third-party plugins compile against. |
apps/ | Companion apps: macOS menu bar, iOS, Android, macOS MLX TTS, swabble. Connect to Gateway over WS. | Mobile/desktop client work. |
ui/ | WebChat and Control UI static assets served by the Gateway HTTP server. | Local web client work. |
docs/ | End-user and reference docs. Tested in CI; docs/concepts/* mirrors the architecture closely. | Any visible behaviour change. |
security/ | Opengrep rules and security scaffolding. | Lint/scan changes. |
Inside src/
The directories that account for almost all of the runtime behaviour are src/gateway (the daemon and WS protocol), src/agents (the embedded Pi runtime and prompt builder), src/channels (registry, routing, allowlists, transports), src/plugins (loader, hook runner, slot orchestration), src/sessions (session keys, transcripts, send policy), and src/mcp (MCP client/server bridges). Around them sit narrower trees for memory, voice/talk, TTS, canvas, cron, commitments, ACP, secrets, and various media generation pipelines.
Inside extensions/
The 130-plus plugin folders bucket into a few obvious families. Model providers dominate by count: Anthropic, OpenAI, Google, Mistral, Groq, Together, Cerebras, Moonshot, xAI, Qwen, DeepSeek, Ollama, Fireworks, MiniMax, Venice, HuggingFace, sglang, vLLM, LMStudio, NVIDIA, Cloudflare AI Gateway, Vercel AI Gateway, LiteLLM, Bedrock, Anthropic-Vertex, OpenRouter, Perplexity, Chutes, ByteDance, Qianfan, StepFun, DeepInfra, ZAI, Voyage, Tencent, Alibaba, Microsoft Foundry, plus search providers (Brave, Exa, Tavily, Firecrawl, SearXNG, DuckDuckGo). Channels: WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, IRC, Nostr, Nextcloud Talk, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, QQ, googlechat, msteams, webhooks. Voice and TTS: ElevenLabs, Deepgram, Inworld, Azure Speech, SenseAudio, tts-local-cli, sherpa-onnx-tts. Infrastructure: ACPX runtime, browser, canvas, memory-core/lancedb/wiki, image-generation-core, media-understanding-core, document-extract, file-transfer, diagnostics-otel, diagnostics-prometheus, copilot-proxy, cloudflare-ai-gateway, vercel-ai-gateway, litellm. Migration and tooling: migrate-claude, migrate-hermes, qa-channel, qa-lab, qa-matrix, skill-workshop.
Each plugin declares itself in openclaw.plugin.json — a manifest that tells the loader which slots it fills, which channels it owns, which provider IDs and model prefixes it claims, which config schema it accepts, and how it discovers auth profiles. The is a fair sample: it claims the anthropic provider, registers model prefix claude-, sets up provider endpoints, declares CLI backends, lists auth profile choices, and exposes a media-understanding contract. The runtime side (register.runtime.ts, provider-runtime.ts) wires concrete behaviour to those declarations.
What lives where — a navigator
When you need to find a specific behaviour, this is the rough mapping the AGENTS.md root spells out:
main WebSocket server, method dispatch (~1.7k LOC)
the
agent RPC entry point (~1.6k LOC)buildAgentSystemPrompt (~1.4k LOC)
runEmbeddedPiAgent core loop (~3.3k LOC)
workspace resolution & bootstrap files
load AGENTS.md / SOUL.md / USER.md
channel registration & routing
plugin loader, slot orchestration
public SDK barrel
TypeBox-defined WS wire protocol
per-plugin manifest
bundled skills
03The Gateway daemon
127.0.0.1:18789 (default) for clients and headless nodes, serves the canvas and A2UI hosts under HTTP, validates every inbound frame against a generated JSON schema, and dispatches into either the embedded agent runtime or one of the plugin-owned method handlers. Exactly one Gateway per host.Almost every architectural choice in OpenClaw flows from a single rule: the Gateway is the only place that holds a live messaging session. There is exactly one WhatsApp Baileys session per host because two would collide; the Gateway therefore has to be the single place that owns it. Clients (the macOS menu bar app, the iOS/Android nodes, the WebChat UI, the CLI) connect over WebSocket and either issue requests or subscribe to events. The product is the assistant; the Gateway is the control plane that makes the assistant always-on, multi-channel, and shared across surfaces.
Process model
The daemon boots out of , which exports a thin wrapper around server.impl.ts. The implementation file is roughly 1,700 lines and wires together: protocol validation, auth (token / password / Tailscale / trusted-proxy / none), connection-state machine, method dispatch, broadcast, channel registry, plugin bootstrap, control-UI HTTP routes, cron, hooks, model pricing cache, exec approval manager, MCP HTTP endpoints, and shutdown drain. The Gateway also runs supervised under launchd (macOS) or systemd (Linux) via openclaw onboard --install-daemon so it stays alive across reboots.
One Gateway process can host one or many agents. The default is single-agent mode with agentId = "main"; multi-agent setups configure agents.list[] with distinct agentDirs, separate auth profile stores, separate workspaces, and routing bindings from channels to agent IDs. The agent runtime lives inside the same Node process — there is no separate worker, no child process for the agent itself.
The WebSocket protocol
Wire format is JSON text frames, defined by TypeBox schemas under src/gateway/protocol/* and code-generated into JSON Schema and Swift models. The first frame on every socket must be a connect frame:
// connect.params shape (abridged) { type: "connect", params: { role: "client" | "node", deviceId: "<stable device identifier>", platform: "macos" | "ios" | "android" | ..., deviceFamily: ..., auth: { token?: "<gateway secret>", password?: "<gateway password>" }, challenge: "<nonce signed by device>", caps?: [...], // for role=node commands?: [...] } }
Anything else as the first frame is a hard close. After the handshake the socket exchanges three message kinds: req (request from client to server with an id and method name), res (the matching reply, carrying payload or error), and event (server-push: tick, presence, chat, agent, health, heartbeat, cron, shutdown). Side-effecting methods (send, agent) require idempotency keys, and a short-lived dedupe cache catches retries.
Method discovery is metadata, not introspection. The hello-ok response advertises features.methods and features.events, but those lists are curated, not a generated dump of every callable in the codebase. Plugins that contribute methods register through server-methods; method scopes are enforced by src/gateway/method-scopes.ts.
Auth modes
Auth is configured under gateway.auth.*. The supported modes:
- token — shared-secret bearer in
connect.params.auth.token. Default for non-loopback binds. The shared-secret pattern. - password — shared password in
connect.params.auth.password. Functionally similar but human-typable. - trusted-proxy — identity from request headers (e.g. behind nginx + mTLS).
- Tailscale — when
gateway.auth.allowTailscale: true, the Tailscale identity from the tnet satisfies auth. - none — disables shared-secret auth. Documented for private ingress only; never on public surfaces.
The gateway refuses to start if the token equals the documented placeholder. Auto-generated tokens are written on first start (). Rate limiting and the connection-state machine live next to auth (auth-rate-limit.ts, connection-auth.ts).
Pairing & device trust
Every WebSocket client (operator or node) presents a device identity at connect. New device IDs require pairing approval; on approval the Gateway issues a device token for subsequent connects. Direct localhost connects can be auto-approved for same-host UX; Tailnet and LAN connects always require explicit pairing approval. Pairing signatures (v3) bind platform and deviceFamily; metadata changes require repair pairing. Pairing data lives in the device pairing store (src/pairing/*).
Inbound DM safety
DMs from real messaging surfaces are treated as untrusted input. Telegram/WhatsApp/Signal/iMessage/Microsoft Teams/Discord/Google Chat/Slack default to dmPolicy: "pairing": unknown senders get a pairing-code prompt and their message is not processed by the agent until the operator runs openclaw pairing approve <channel> <code>. To open this up the operator must set dmPolicy: "open" and include "*" in the channel allowlist — explicit, two-step opt-in. The openclaw doctor command surfaces risky DM policies.
--dangerously-skip-permissions default was its biggest sharp edge, OpenClaw makes the equivalent decision the opposite way: untrusted DM senders cannot reach the agent without an operator approving them. This is a structural choice that survives upstream channel quirks.
Canvas & A2UI host
The Gateway's HTTP server (same port, default 18789) also hosts two interactive surfaces: /__openclaw__/canvas/ serves agent-editable HTML/CSS/JS for the Live Canvas product, and /__openclaw__/a2ui/ serves the A2UI host (Agent-to-UI protocol). Both share the Gateway auth. They exist so the agent can render a live workspace on the user's screen and accept tap-level interactions without a separate UI service.
HTTP compatibility endpoints
Alongside the WS API, the Gateway exposes OpenAI-compatible HTTP endpoints (POST /v1/chat/completions, POST /v1/responses) plus POST /tools/invoke. These exist so anything that speaks OpenAI's API can drive the agent. Critically, these endpoints authenticate the shared Gateway bearer secret and are not per-user scope boundaries — passing shared-secret bearer auth there is equivalent to full operator access. x-openclaw-scopes headers are ignored under shared-secret auth.
:18789 publicly without a tunnel is effectively giving anyone with the token full agent capability over HTTP. The documented operating model is loopback + Tailscale / SSH tunnel; deviating from that breaks the trust model the rest of the codebase relies on.
04Agent runtime (Pi-embedded)
runEmbeddedPiAgent — a roughly 3,300-line function that serializes runs through per-session and global queues, resolves the model + auth profile, builds the Pi session, subscribes to lifecycle events, streams assistant/tool deltas back out as gateway events, and enforces timeouts.This is the single largest departure from Paperclip's design. Paperclip spawns an external agent CLI (Claude Code, Codex, Gemini CLI) as a subprocess and pipes prompts into its stdin. OpenClaw imports the Pi agent core directly and runs the agent loop in-process. There is no subprocess, no stdin handshake, no --append-system-prompt-file. The agent is library code, not a binary.
The agent loop
agent method handler validates params, resolves session (by sessionKey or sessionId), persists session metadata, returns { runId, acceptedAt } to the client immediately. The run is queued, not run inline.runEmbeddedPiAgent, and emits lifecycle end/error as a fallback if the embedded loop forgets to.main lane defaults to concurrency 4, subagent to 8. Typing indicators fire immediately on enqueue so UX is unaffected.buildAgentSystemPrompt renders an OpenClaw-owned prompt (not the Pi default) from explicit inputs: tools list, workspace dir, skills prompt, bootstrap files, sandbox info, time zone, runtime info, provider contributions.stream:"assistant", tool events → stream:"tool", lifecycle → stream:"lifecycle".before_model_resolve, before_prompt_build, before_agent_start, before_agent_reply, before_tool_call/after_tool_call, agent_end, before_compaction/after_compaction, plus message_received/message_sending/message_sent and session_start/session_end.NO_REPLY/no_reply filtered, duplicate messaging-tool sends removed, and fallback error replies emitted only when no other payload was produced.end/error arrives on the agent stream; agent.wait resolves; the chat channel gets its final.What's actually in runEmbeddedPiAgent
The 3,300-line file at is the heart of the runtime. It handles: per-session queue enqueue, abort timer setup, auth profile rotation with cooldown-aware ordering, provider runtime selection (including Codex app-server harness mode as a separate path), Pi session creation, event subscription bridge, retry on context overflow with auto-compaction, prompt-cache observability, live-model-switch handling, plugin hook dispatch at every documented boundary, transcript write-lock acquisition, and result extraction with usage metadata.
One detail worth holding onto: the runner can also drive the Codex app-server harness as an alternative runtime. Resolved via OPENCLAW_AGENT_RUNTIME=codex-app-server (pi-embedded-runner/runtime.ts), this path treats Codex as a sub-runtime that produces app-server turns rather than Pi events. It is the closest OpenClaw gets to Paperclip's external-CLI shape, and even here the harness is selected through plugin runtime registration, not by spawning a CLI from the Gateway. The default remains pi — the in-process Pi core.
Queue and concurrency
Auto-replies across all channels are serialized through a tiny in-process lane queue. The reasons are operational: LLM calls collide, session files cannot be written by two writers, upstream rate limits get hit faster with parallelism. The defaults:
// docs/concepts/queue.md mode: "steer", // inject mid-run prompts into the active run debounceMs: 500, cap: 20, drop: "summarize"
Same-turn steering is default: a prompt that arrives mid-run is injected into the active Pi runtime after the current assistant turn finishes its tool calls and before the next LLM call. Other modes: followup (enqueue for a later turn), collect (coalesce into a single later turn), interrupt (abort the active run for the newest message). Queue mode is per-channel-overridable under messages.queue.
Timeouts and abort drains
- agent.wait — default 30 s; just the wait, not the run.
- Agent runtime —
agents.defaults.timeoutSecondsdefaults to 172,800 s (48 h). Enforced in the runner's abort timer. - Cron runtime — owns its own per-turn
timeoutSeconds; the scheduler starts the timer at execution start, aborts at the deadline, then runs bounded cleanup. - Model idle — aborts when no response chunks arrive before the idle window.
models.providers.<id>.timeoutSecondsextends this; otherwise default 120 s cap. - Stuck session diagnostics — with diagnostics on,
diagnostics.stuckSessionWarnMsclassifies longprocessingsessions assession.long_running,session.stalled, orsession.stuck; abort-drain kicks in afterdiagnostics.stuckSessionAbortMs(min 5 min, 3× warn).
Where things end early
A run can stop via: agent timeout (abort), AbortSignal (cancel), Gateway disconnect or RPC timeout, or agent.wait timeout (wait-only — does not stop the agent itself). Lifecycle end still fires for every terminated run, so subscribers don't hang.
05The system prompt, assembled
buildAgentSystemPrompt renders an OpenClaw-owned prompt from explicit inputs. It is not the Pi default. The prompt is intentionally compact, uses fixed section headers, and splits content across an internal prompt-cache boundary so stable workspace context can be cached separately from volatile channel/session sections.If you want to know what the model actually sees, the answer is in . The builder is a roughly 1,380-line function with one huge parameter object and a hash-keyed cache for its stable prefix. Three layers compose: the renderer (pure), the config resolver (reads OpenClawConfig for owner display, TTS hints, alias lines, memory citation mode, sub-agent delegation), and runtime adapters (embedded, CLI, command export, compaction) that gather live facts and call the renderer.
Sections, in order
The fixed structure of the prompt:
- Identity line — “You are a personal assistant running inside OpenClaw.”
- Tooling — the policy-filtered tool list, plus a reminder that tool names are case-sensitive and TOOLS.md is guidance, not availability.
- Sub-Agent Delegation (when
delegationMode: "prefer") — instructs the main agent to act as a responsive coordinator and push anything bigger than a direct reply throughsessions_spawn. - Tool Call Style — narration discipline, approval guidance, allow-once semantics, full-command preservation for
/approve. - Execution Bias — act in-turn, continue until done or blocked, recover from weak tool results, verify before finalizing.
- Safety — no independent goals; safety/oversight over completion; never bypass safeguards.
- OpenClaw Control — prefer the
gatewaytool for config/restart, do not invent CLI commands. - Skills — how to load skill instructions on demand.
- Memory — MEMORY.md + daily files (when available).
- OpenClaw Self-Update — config.schema.lookup, config.get/patch/apply, update.run only on explicit request.
- Model Aliases — when configured.
- Workspace — working directory + treatment guidance.
- Documentation — local path to OpenClaw docs/source.
- Sandbox (when enabled) — sandbox paths, elevated exec state.
- User identity — owner numbers (raw or hashed).
- Current Date & Time — time zone only; the live clock comes from
session_statusso the prompt stays cache-stable. - Workspace Files (injected) — marker; the actual files appear under Project Context below.
- Assistant Output Directives — attachment/voice-note/reply-tag syntax.
- Reasoning Format (when
reasoningTagHint) —<think>/<final>rules. - Project Context — AGENTS.md, SOUL.md, IDENTITY.md, USER.md, TOOLS.md, BOOTSTRAP.md, MEMORY.md, in that fixed precedence order.
- Silent Replies —
NO_REPLYtoken rules. - Below the cache boundary: Control-UI embed guidance, Messaging, Voice, Group Chat Context, Reactions, Heartbeats, Runtime info.
The prompt-cache boundary
Stable content (workspace dir, tools, skills prompt, owner line, project context files) sits above a hashed prefix-cache boundary. Volatile content (channel name, session-specific runtime info, group-chat context, heartbeat status) sits below it. Local providers with prefix caches can reuse the stable prefix across channel turns. The cache key is computed from a structured hash over every input that affects the stable prefix; the implementation caches up to 64 entries in an LRU map ().
Provider contributions
Provider plugins can shape the prompt without replacing it. The contribution API supports three moves:
- Override a small set of named core sections —
interaction_style,tool_call_style,execution_bias. - Inject a stable prefix above the cache boundary.
- Inject a dynamic suffix below the cache boundary.
The OpenAI GPT-5 family overlay uses this to keep the core execution rule small while adding model-specific guidance for persona latching, concise output, tool discipline, parallel lookup, verification, and terminal-tool hygiene (src/agents/gpt5-prompt-overlay.ts).
Tool descriptions, verbatim
The default tool summaries shipped in the prompt — these are the strings the model actually sees next to each tool name when it's enabled:
read Read file contents write Create or overwrite files edit Make precise edits to files apply_patch Apply multi-file patches grep Search file contents for patterns find Find files by glob pattern ls List directory contents exec Run shell commands (pty available for TTY-required CLIs) process Manage background exec sessions web_search Search the web using the configured provider web_fetch Fetch and extract readable content from a URL browser Control web browser canvas Present/eval/snapshot the Canvas nodes List/describe/notify/camera/screen on paired nodes cron Manage cron jobs and wake events message Send messages and channel actions gateway Restart, apply config, or run updates on the running OpenClaw process agents_list List OpenClaw agent ids allowed for sessions_spawn sessions_list List other sessions (incl. sub-agents) with filters/last sessions_history Fetch history for another session/sub-agent sessions_send Send a message to another session/sub-agent sessions_spawn Spawn an isolated sub-agent session sessions_yield End this turn and wait for spawned sub-agent completion events subagents On-demand list/steer/kill sub-agent runs for this requester session_status Show a /status-equivalent status card image Analyze an image with the configured image model image_generate Generate images with the configured image-generation model
Heartbeats and dynamic context
When heartbeats are enabled for the default agent, the prompt acquires a Heartbeats section below the cache boundary and a HEARTBEAT.md entry in dynamic context. The default heartbeat prompt — used when there's no explicit override — is verbatim:
"Read HEARTBEAT.md if it exists (workspace context). Follow it strictly.
Do not infer or repeat old tasks from prior chats. If nothing needs attention,
reply HEARTBEAT_OK."
This is the OpenClaw equivalent of Paperclip's heartbeat tick — but where Paperclip used its heartbeat to drive the entire run loop, OpenClaw heartbeats are an auxiliary tick on top of normal session activity. They exist mainly to let agents wake up on schedule and check workspace state, not to run the conversation.
06Workspace & bootstrap files
The workspace is the agent's only cwd for tools and context. By default it sits at ~/.openclaw/workspace (or ~/.openclaw/workspace-<profile> when OPENCLAW_PROFILE is set, or a per-agent directory in multi-agent setups). Tools resolve relative paths against this directory; absolute paths can reach other host locations unless sandboxing is enabled. The workspace is the default cwd, not a hard sandbox.
The six bootstrap files
| File | Role | Order |
|---|---|---|
| AGENTS.md | Operating instructions and durable “memory”. The workhorse — most behaviour customization lives here. | 10 |
| SOUL.md | Persona, voice, boundaries, default bluntness, tone, humour. Short beats long; sharp beats vague. | 20 |
| IDENTITY.md | Agent name, vibe, emoji. Used for ack reactions and identity-bearing surfaces. | 30 |
| USER.md | User profile, preferred address, language preferences, work context. | 40 |
| TOOLS.md | User-maintained tool notes (imsg, sag, conventions). Guidance, not availability — the prompt explicitly states this. | 50 |
| BOOTSTRAP.md | One-time first-run ritual file. Only created for a brand-new workspace; deleted after the ritual completes. | 60 |
| MEMORY.md | Durable long-term memory. Loaded at the start of every DM session. | 70 |
The constant CONTEXT_FILE_ORDER at the top of system-prompt.ts defines this precedence verbatim. Blank files are skipped. Large files are trimmed and marker-truncated to keep the prompt budget lean. Missing files inject a single marker line, and openclaw setup can create safe default templates.
BOOTSTRAP.md — the first-run ritual
For brand-new workspaces with no other bootstrap files present, OpenClaw creates a BOOTSTRAP.md and adds bootstrap-mode guidance to the system prompt instead of copying it into the user message. The bootstrap stays pinned to Project Context while the ritual is pending; if you delete it after completing the ritual, it should not be recreated on later restarts. Bootstrap can be disabled entirely with agents.defaults.skipBootstrap: true for pre-seeded workspaces.
Bootstrap completion is detected by scanning the tail of the session transcript for a marker of type openclaw:bootstrap-context:full (). This avoids re-injecting the bootstrap context once the user has gone through it.
Dynamic context files
Most bootstrap files are stable — they live above the cache boundary. Two filenames are recognised as dynamic and placed below the boundary so they don't invalidate the cache when they change:
- heartbeat.md — fired by the heartbeat tick; can change frequently.
- memory/*.md daily files — running notes that change throughout the day.
The session transcript
Session transcripts are JSONL files at ~/.openclaw/agents/<agentId>/sessions/<SessionId>.jsonl. The session ID is stable and chosen by OpenClaw — legacy session folders from other tools (e.g. Codex's ~/.codex/sessions/) are not read. Writes are guarded by a process-aware file-based session write lock; default acquire timeout is 60 s (session.writeLock.acquireTimeoutMs). Non-reentrant by default; helpers can opt into reentrant acquisition with allowReentrant: true.
Per-agent isolation in multi-agent setups
Each agent has its own workspace, its own auth profile store (~/.openclaw/agents/<agentId>/agent/auth-profiles.json), and its own sessions directory (~/.openclaw/agents/<agentId>/sessions/). Auth profiles are per-agent; agents can read through to the default agent's profiles when they lack a local one, but OAuth refresh tokens are not cloned across agents. Re-using agentDir across agents causes auth and session collisions and is documented as a strict don't.
SOUL.md is your assistant's most important file.
07Skills, tools & the plugin SDK
Skills
A skill is a folder containing a SKILL.md with YAML frontmatter and instructions. The frontmatter names the skill, describes it, declares required binaries, points at install metadata, and lists any agent restrictions. The agent loads the skill text on demand — skills are not prompt-injected wholesale; instead the system prompt tells the agent how to discover and load them via the read tool.
OpenClaw resolves skills from six roots in precedence order:
| # | Source | Path |
|---|---|---|
| 1 | Workspace skills | <workspace>/skills |
| 2 | Project agent skills | <workspace>/.agents/skills |
| 3 | Personal agent skills | ~/.agents/skills |
| 4 | Managed/local skills | ~/.openclaw/skills |
| 5 | Bundled skills | shipped with the install |
| 6 | Extra skill folders | skills.load.extraDirs |
If a skill name conflicts across roots, the highest source wins. Bundled skills include 58 entries at last count — Apple Notes, Apple Reminders, Bear, Notion, Obsidian, gh-issues, tmux, weather, healthcheck, model-usage, session-logs, things-mac, trello, taskflow, meme-maker, peekaboo, and the all-important coding-agent skill. Skills can also ship with plugins (skills: ["./skills"] in the plugin manifest), which is how the browser plugin ships its browser-automation skill.
Per-agent allowlists separate visibility from location. agents.defaults.skills sets a shared baseline; agents.list[].skills replaces it (does not merge); agents.list[].skills: [] means no skills at all.
The coding-agent skill — verbatim
The skills/coding-agent/SKILL.md file is worth quoting because it pins down the boundary between OpenClaw and external coding agents. The skill declares dependencies anyBins: ["claude", "codex", "opencode", "pi"] and provides explicit install entries for Claude Code and Codex via npm. Its operating rules:
# Coding Agent Use for background feature builds, PR reviews, large refactors, and issue-to-PR loops. Do not use for simple edits, read-only lookup, ACP thread-bound work, or any run inside ~/.openclaw, $OPENCLAW_STATE_DIR, or active OpenClaw state dirs. ## Hard rules - Always launch with background:true. - Codex, Pi, OpenCode: use pty:true. - Claude Code: no PTY; use claude --permission-mode bypassPermissions --print. - Capture a real notification route before spawning. - Worker must send completion/failure via openclaw message send. - Do not rely on heartbeat, system events, or notify-on-exit. - Monitor with process; do not kill slow workers without cause. - If user asked for a specific agent, use that agent. - If worker fails/hangs, respawn or ask; do not silently hand-code instead. - Never checkout branches or run background coding agents in ~/Projects/openclaw; use an isolated checkout.
Note that this is the only place where OpenClaw shells out to Claude Code or Codex as a binary — and even then it's framed as a worker the agent delegates to, not a runtime replacement.
Tools
Tools are the core verbs the embedded agent has, separate from skills. The set is policy-filtered per-session and lives in src/agents/openclaw-tools.ts (registration) and src/agents/pi-tools.ts (Pi-shape definitions). The most consequential tools are read, write, edit, apply_patch, exec, process, web_search, web_fetch, browser, canvas, nodes, cron, message, gateway, plus the sub-agent family (sessions_spawn, sessions_yield, subagents, sessions_list, sessions_history, sessions_send).
apply_patch is optional and gated by tools.exec.applyPatch. TOOLS.md does not change which tools exist — the prompt explicitly states this — it's guidance for how the user wants them used.
Plugin manifest shape
Every plugin lives in extensions/<id>/ and declares itself in openclaw.plugin.json. The keys that matter:
{
"id": "anthropic",
"activation": { "onStartup": false },
"enabledByDefault": true,
"providers": ["anthropic"],
"providerCatalogEntry": "./provider-discovery.ts",
"modelSupport": { "modelPrefixes": ["claude-"] },
"providerEndpoints": [
{ "endpointClass": "anthropic-public",
"hosts": ["api.anthropic.com"] }
],
"cliBackends": ["claude-cli"],
"providerAuthEnvVars": {
"anthropic": ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"]
},
"providerAuthChoices": [ ... ],
"contracts": { "mediaUnderstandingProviders": ["anthropic"] }
}
The runtime wires concrete behaviour through register.runtime.ts (plugin entry) and various *-api.ts contract files. Plugin prod code is forbidden from importing core src/** directly — it has to go through openclaw/plugin-sdk/* public barrels. Core code, in turn, is forbidden from importing deep plugin internals. The boundary is enforced by lint rules and architecture checks.
The two plugin styles
Plugins come in two shapes:
- Code plugins — full runtime extension; can register providers, channels, tools, hooks, slot fills. Appropriate when behaviour requires in-process integration.
- Bundle-style plugins — package stable external surfaces (skills, MCP servers, configuration). Smaller surface, better security boundaries; preferred when capability can be expressed that way.
The bar for adding new core capability is intentionally high — VISION.md explicitly says “Core stays lean; optional capability should usually ship as plugins.” Plugin discovery, official-publisher status, provenance, and security review live in ClawHub, not in core.
MCP support
OpenClaw speaks Model Context Protocol in both directions. As an MCP client it can talk to remote MCP servers via stdio or HTTP transports (src/agents/mcp-stdio.ts, src/agents/mcp-http.ts). As an MCP server it exposes its own tools (via the Gateway's MCP HTTP handlers, src/gateway/mcp-http.*) so external MCP-aware agents can drive OpenClaw. MCP is treated as a complementary protocol, not a replacement for the existing tool/plugin paths.
08Channels & the multi-channel inbox
extensions/<channel>/. The core src/channels tree provides the generic seams (registry, routing, allowlists, transports, status reactions, typing, threads); each plugin owns its specific transport, auth, payload shaping, and channel-specific quirks.Around two dozen messaging channels ship in the box. The full set: WhatsApp, Telegram, Slack, Discord, Signal, iMessage, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat (QQ), WebChat, plus headless surfaces (macOS/iOS/Android nodes, cron, webhooks). All of them route through the same in-process queue and the same agent runtime; the channel plugin's job is to translate between the channel's native idioms and OpenClaw's session/payload model.
Channel registration
A plugin claims one or more channels through its manifest:
{
"id": "whatsapp",
"activation": { "onStartup": false },
"channels": ["whatsapp"],
"configSchema": {
"type": "object",
"properties": {
"pluginHooks": {
"messageReceived": { "type": "boolean" }
}
}
}
}
The channel registry (src/channels/registry.ts) consumes these declarations, builds a lookup keyed by channel ID, and routes inbound messages to the registered plugin's runtime. Each plugin contributes action-runtime.runtime.ts (outbound actions), light-runtime-api.ts (fast-path send), setup-entry.ts (account configuration), and several contract APIs (channel-plugin-api, channel-config-api, secret-contract-api, security-contract-api, doctor-contract-api).
Routing inbound messages
The inbound path is, in order: channel plugin receives a raw event, normalizes it into an OpenClaw inbound-event, the channel core resolves a binding (channel account → agent), the binding routes to a session via session.dmScope + session.identityLinks, the queue admits the run, the agent runtime is called. Allowlist gates (allowFrom, mention gating, DM pairing) fire at the binding/session resolution step — before the agent runtime is reached.
Session routing
Where a message lands depends on its source:
| Source | Default behaviour |
|---|---|
| Direct messages | Shared session (with dmScope: "main") — fine for single-user setups. |
| Group chats | Isolated per group. |
| Rooms/channels | Isolated per room. |
| Cron jobs | Fresh session per run. |
| Webhooks | Isolated per hook. |
The recommended DM-isolation setting for any multi-user case is dmScope: "per-channel-peer", which isolates by channel + sender. Without it, Alice's DMs would be visible to Bob inside the same shared DM session. openclaw security audit calls out unsafe combinations.
Channel docking
A user can dock the current direct-chat session's reply route to another linked channel without starting a new session. So if you've been chatting on Telegram and you want the reply to come through iMessage instead, the dock command moves the reply route without a session reset. This is the cross-channel continuity story OpenClaw uses to make “same agent on every surface” actually feel coherent.
Channel-specific quirks
Each channel plugin handles its own messy reality. The Discord plugin handles thread bindings, reactions, native command sessions, and the “native-approval-prompt” surface where Discord buttons can resolve approvals. WhatsApp via Baileys handles pairing QR flow, legacy session migrations, and exactly-one-session-per-host enforcement. Slack handles block streaming permissions and lifecycle. iMessage on macOS uses the local SAG helper. Some plugins ship runtime-only paths (Zalo Personal, Synology Chat) that are smaller; others are huge (Discord, Telegram, Slack).
Status reactions and typing
OpenClaw fires platform-native ACK signals as soon as a run is enqueued — typing indicators, status reactions, lifecycle marks. The default ACK reaction is 👀 (configurable per-channel and per-account via messages.ackReaction and channel-level overrides). The status-reactions pipeline (src/channels/status-reactions.ts) abstracts over channel-specific implementations so the same reaction model works across Slack, Discord, Telegram, etc.
Voice channels
Voice Wake (macOS/iOS) and Talk Mode (Android) are separate surfaces wired through src/talk/ and src/realtime-transcription/. They use ElevenLabs for premium voice, with system TTS as fallback, and Bhashini-style local STT/TTS through the Sherpa-ONNX skills. The voice surface is one of the few cases where OpenClaw mixes real-time streaming with the agent loop — the talk session relays through a separate src/gateway/talk-realtime-relay.ts path.
The Canvas surface
The Canvas is a Gateway-served HTML/CSS/JS workspace the agent can render into and the user can interact with on a Mac, iPad, or web. The canvas tool exposes canvas.present, canvas.eval, canvas.snapshot. Channel docking can target the canvas; Discord/Slack approvals can use native inline buttons (when the channel supports them). Together with A2UI it's how OpenClaw delivers “agent-driven visual workspace” without a separate app process.
09Sessions, queue & steering
/new. The Gateway owns every session's state; clients query through it. Concurrency inside a session is serialized by a per-session lane; concurrency across sessions is capped by the global lane.Session keys
A session key is the routing identifier. It is computed from (agentId, channel, peer, scope) by src/sessions/session-id.ts and src/gateway/server-session-key.ts. The DM scope decides how aggressively to namespace:
main(default) — all DMs share one session.per-peer— isolate by sender (across channels).per-channel-peer— isolate by channel + sender (recommended for multi-user).per-account-channel-peer— isolate by account + channel + sender (most aggressive).
If the same person contacts you from multiple channels and you want a unified session, session.identityLinks links their identities so they share one session despite the scope rules.
Lifecycle
Sessions live in three time domains:
- Daily reset (default) — new session at 04:00 local time on the Gateway host. Daily freshness is based on when the current
sessionIdstarted (sessionStartedAt), not on later metadata writes. Heartbeat/cron/exec system events write metadata but do not extend freshness. - Idle reset (optional) — new session after a configurable inactivity window (
session.reset.idleMinutes). Based onlastInteractionAt— the last real user/channel interaction. - Manual reset —
/newor/resetin chat./new <model>also switches model.
When both daily and idle resets are configured, whichever fires first wins. When a reset rolls the session, queued system-event notices for the old session are discarded so stale background updates don't prepend to the new session's first prompt. Provider-owned CLI sessions (the harness path) are not cut by the implicit daily default — they require explicit /reset or configured session.reset.
Where state lives
// Per-agent layout under ~/.openclaw/ agents/<agentId>/ ├── agent/ │ ├── auth-profiles.json # per-agent model auth │ └── config/ └── sessions/ ├── sessions.json # index of all sessions └── <sessionId>.jsonl # transcript per session
The sessions.json store keeps separate lifecycle timestamps: sessionStartedAt, lastInteractionAt, updatedAt. The daily reset path uses sessionStartedAt; idle uses lastInteractionAt; the listing UIs use updatedAt. Older rows missing these fields fall back to the JSONL session header.
Queue lanes & serialization
The lane queue lives in src/process/command-queue.ts and the lane logic in src/agents/lanes.ts. There are two layers: a per-session lane (session:<key>) with concurrency 1, and a global lane (default main) with configurable concurrency. agents.defaults.maxConcurrent caps overall parallelism (defaults: main=4, subagent=8). When verbose logging is on, runs emit a notice if they waited more than ~2 s before starting.
Steering
This is one of OpenClaw's distinctive features. When a new prompt arrives while a session has an active run, the default is steer — inject the new message into the active runtime rather than starting a second run or queueing for later. Steering is delivered after the current assistant turn finishes its tool calls, before the next LLM call. If the run can't accept steering (e.g. it's not actively streaming), OpenClaw waits for the active run to end before starting the new prompt.
Other queue modes:
followup— don't steer; enqueue for a later turn after the current run ends.collect— don't steer; coalesce queued messages into a single later turn after the quiet window. Different channels/threads drain individually.interrupt— abort the active run for that session and run the newest message.
Queue mode is configurable globally (messages.queue) and per-channel. The exact streaming/dependency timing details live in docs/concepts/queue-steering.md.
Sub-agents and sessions_spawn
OpenClaw supports sub-agents through the sessions_spawn tool. The main agent can delegate any non-trivial work to a child session — file inspection, web work, long reads, debugging, coding tasks, multi-step analysis. sessions_spawn with runtime: "subagent" creates an isolated child; with runtime: "acp" it spins up an ACP-harness coding agent (Codex, Claude Code, OpenCode). Each child can be given a stable taskName handle, a clear objective, expected output, relevant files/inputs, write scope, and a verification ask.
After spawning, the main agent either continues directly or calls sessions_yield to wait for completion events before answering. Child completion is push-based and returns as a runtime event — no polling. The main agent treats child outputs as reports/evidence, not instructions that can override the user, developer, or system policy. Sub-agent runs are tracked by the sub-agent registry (src/agents/subagent-registry.ts and friends — a substantial subsystem on its own), with lifecycle events, depth limits (to prevent runaway nesting), and announce capture for delivering completion replies back to the requester.
Session maintenance
OpenClaw bounds session storage automatically. By default it runs in warn mode (reports what would be cleaned). Set session.maintenance.mode: "enforce" for automatic cleanup. Maintenance applies retention rules to transcripts and the store index.
10Memory, dreaming & compaction
Three files
- MEMORY.md — long-term, curated, durable facts. Loaded at the start of every DM session. Compact summary layer.
- memory/YYYY-MM-DD.md (or memory/YYYY-MM-DD-<slug>.md) — daily notes. Today's and yesterday's files are loaded automatically; older ones are indexed for
memory_search/memory_get. - DREAMS.md (optional) — dream-diary output and phase summaries for human review.
The agent is expected to distill useful material from daily notes into MEMORY.md over time, removing stale entries. The heartbeat flow and dreaming sweeps can do this periodically; you don't manually edit MEMORY.md for every detail.
If MEMORY.md grows past the bootstrap file budget, OpenClaw keeps the file on disk intact but truncates the copy injected into the model context. The truncation is a signal: move detailed material back into daily files and keep only the durable summary in MEMORY.md, or raise the bootstrap limits if you explicitly want to spend more prompt budget.
Memory backends (plugin slot)
Memory is a special plugin slot — only one memory plugin can be active at a time. Three options ship:
- memory-core (default) — file-based, the contract above. Simple, durable, no external dependencies.
- memory-lancedb — adds a local LanceDB vector index for semantic search over notes.
- memory-wiki — wiki-style indexed retrieval over a knowledge-base structure.
Over time the project plans to converge on one recommended default. Today the simple file-based default suits most setups.
Dreaming — background consolidation
Dreaming is opt-in and disabled by default. It runs in three cooperative phases:
| Phase | Purpose | Durable write |
|---|---|---|
| Light | Sort and stage recent short-term material; dedupe; record reinforcement signals for later ranking. | No |
| Deep | Score and promote durable candidates. Requires minScore, minRecallCount, minUniqueQueries to pass. Rehydrates snippets from live daily files; stale ones are skipped. | Yes (MEMORY.md) |
| REM | Reflect on themes and recurring ideas; extract patterns. | No |
Dreaming machine state lives in memory/.dreams/ (recall store, phase signals, ingestion checkpoints, locks). Human-readable output lives in DREAMS.md and optional memory/dreaming/<phase>/YYYY-MM-DD.md. Long-term promotion writes only to MEMORY.md.
Context engine
The context engine is the pluggable layer that builds model context for each run: which messages to include, how to summarize history, how to manage context across sub-agent boundaries. OpenClaw ships a legacy engine by default; alternative engines (Lossless Claw, custom plugins) plug in through plugins.slots.contextEngine. Most users don't need to change this.
Auto-compaction
Auto-compaction is on by default. It runs when the session nears the model's context limit, or when the model returns a context-overflow error (in which case OpenClaw compacts and retries the same turn). Recognised overflow signatures include request_too_large, context length exceeded, input exceeds the maximum number of tokens, and several provider-specific variants.
The pipeline:
- OpenClaw reminds the agent to save important notes to memory files (so context loss is recoverable).
- Older turns are summarized into a single compact entry.
- The summary is saved in the session transcript.
- Recent messages are kept intact.
- The full conversation history stays on disk; compaction only changes what the model sees next turn.
When splitting history into chunks, OpenClaw keeps assistant tool calls paired with their matching toolResult entries. If a split point lands inside a tool block, the boundary is moved so the pair stays together. /compact forces a manual compaction; you can add instructions to guide the summary (e.g. /compact Focus on the API design decisions). When agents.defaults.compaction.keepRecentTokens is set, manual compaction honors that cut-point and keeps the recent tail.
Memory tools
The memory toolset is exposed when a memory backend is active. The core operations: memory_search, memory_get, memory_save, plus the file-based path via read/write directly into MEMORY.md or daily files. Wiki and LanceDB backends expose richer search semantics. The bundled session-memory hook can write a slugged daily file on /new or /reset so a fresh session inherits relevant context without polluting MEMORY.md.
11Security model & sandboxing
The operator trust model
This is the single most important thing to internalize about OpenClaw security:
- Authenticated Gateway callers are trusted operators for that gateway instance.
- Direct localhost / loopback Control UI and Gateway WebSocket sessions authenticated with the shared gateway secret are in the same trusted-operator bucket.
- The HTTP compatibility endpoints (
/v1/chat/completions,/v1/responses,/tools/invoke) are also trusted-operator. Shared-secret bearer auth there is equivalent to full operator access — narrowerx-openclaw-scopesheaders are ignored under shared-secret auth. - Session identifiers (
sessionKey, session IDs, labels) are routing controls, not per-user authorization boundaries. - If one operator can view data from another operator on the same gateway, that is expected in this trust model.
The recommended deployment shape: one user per machine/host (or VPS), one gateway per user, one or more agents inside that gateway. Multi-user setups should use one VPS (or host/OS user boundary) per user, not one gateway with multiple operators.
What's not a vulnerability in this model
SECURITY.md is unusually explicit about what does not count as a security bug — useful both for understanding the threat model and for filing accurate reports:
- Prompt injection without a policy, auth, approval, sandbox, or tool-boundary bypass.
- A trusted operator using an intentional local feature (local shell, browser eval, etc.).
- A malicious plugin after a trusted operator installs/enables it.
- Multiple adversarial users sharing one Gateway host expecting per-user isolation.
- Operator-control surfaces (canvas eval, browser script execution) treated as vulnerabilities without showing a boundary bypass.
- Authorized user-triggered local actions presented as privilege escalation.
- Public exposure or risky deployment choices already warned against in the docs.
Sandboxing
OpenClaw can run tools inside a sandbox backend to reduce blast radius. Optional — controlled by agents.defaults.sandbox or agents.list[].sandbox. The Gateway itself stays on the host; tool execution moves into the sandbox.
Modes (agents.defaults.sandbox.mode):
off— no sandboxing (host-first default, matching the trusted-operator model).non-main— sandbox only non-mainsessions (recommended when normal DMs stay on host but groups/channels are isolated).all— every session runs in a sandbox.
Scope (agents.defaults.sandbox.scope): "agent" (default — one container per agent), "session" (one per session), "shared" (one shared container). Backend: "docker" (default when enabled), "ssh" (generic remote sandbox), "openshell" (OpenShell runtime).
The default sandboxed tool policy: allow bash, process, read, write, edit, sessions_list, sessions_history, sessions_send, sessions_spawn; deny browser, canvas, nodes, cron, discord, gateway. The denied set is what would let a sandboxed session reach back into the host.
Elevated exec
Elevated exec is an explicit escape hatch from the sandbox. When sandboxing is on and elevated exec is enabled, the agent can request elevated execution that runs on the host (or a configured escape target) with approvals. The user controls the level with /elevated on|off|ask|full. full auto-approves; ask requires per-command approval; off blocks. The flag in the prompt is real: the agent is told whether elevated is available, what the current level is, and whether /elevated full is allowed in this session.
Approvals on elevated exec preserve and show the full command (including chained operators like &&, ||, |, ;, multiline shell) so the user approves what will actually run. Allow-once is single-command only — another elevated command needs a fresh /approve, no claiming prior approval covered it.
Secrets storage
- Channel/provider credentials live in
~/.openclaw/credentials/. - Model auth profiles live in
~/.openclaw/agents/<agentId>/agent/auth-profiles.json— per-agent. - Docker setups can store auth-profile encryption key material outside the mounted state dir via
OPENCLAW_AUTH_PROFILE_SECRET_DIR, mounted into the container — so a stolen state dir copy does not yield decryptable credentials.
Reasonable mistakes the docs flag
- The Gateway refuses to start if its token equals the documented placeholder — explicit guard against copy-paste errors.
OPENCLAW_ALLOW_INSECURE_PRIVATE_WSexists but is intentionally awkward to enable.- Tailscale Serve and trusted-proxy modes accept identity from headers — but only if the appropriate config flags are set; otherwise loopback-only.
- The HTTP compat endpoints (
/v1/*,/tools/invoke) look like they support scoped auth viax-openclaw-scopesheaders, but under shared-secret auth those headers are ignored. The docs say this loudly because researchers have tried to file it as a bug.
AMental model
Hold three pictures simultaneously. Shape: OpenClaw is a single long-lived daemon that owns every messaging surface, runs one embedded agent runtime inside the same process, and exposes a typed WebSocket control plane that clients and headless nodes plug into. Pluggability: ~130 plugins in extensions/ supply every concrete capability — providers, channels, hooks, context engines, memory backends, voice — while src/ stays as generic seams. Trust: one Gateway, one operator; the security work happens at ingress (pairing, allowlists, sandbox) rather than between operators sharing one daemon.
Where Paperclip orchestrates external agent CLIs as subprocesses, OpenClaw imports the agent runtime in-process. The differences flow from there: OpenClaw is local-first because the agent is local; OpenClaw owns its session transcripts because the runtime owns its session model; OpenClaw's prompt is OpenClaw-authored (not the Pi default) because the surrounding context — workspace files, owner identity, channel name, time zone, sandbox state — is OpenClaw's to assemble.
BEnd-to-end lifecycle
Trace a single inbound WhatsApp message all the way through:
The interesting transitions sit in the Gateway lane: allowlist gate, DM policy gate (pairing if needed), session-key resolution, queue enqueue, and the call into the in-process agent runtime. Once inside the agent, the loop is the one in chapter four — prompt build → Pi session → event subscription → reply shaping → lifecycle end.
Cvs. Paperclip
Since you already have the Paperclip deep dive, here is the side-by-side at a level of abstraction that matters for product decisions:
| Dimension | Paperclip | OpenClaw |
|---|---|---|
| Shape | Hosted multi-tenant SaaS | Local-first daemon, one operator |
| Agent execution | Spawns external CLIs (Claude Code, Codex, Gemini) as subprocesses | Imports Pi agent core; runs the loop in-process |
| Prompt delivery | Stdin sections + --append-system-prompt-file + AGENTS.md | Direct buildAgentSystemPrompt renderer with cache boundary + provider contributions |
| Skills | Filesystem-mounted via --add-dir; agent loads on demand | Six precedence-ordered roots; allowlists per agent; agent loads on demand via read |
| Tools | Whatever the spawned CLI provides | Core OpenClaw tools + plugin-contributed tools + MCP tools |
| Auth | HS256 JWT, 48 h TTL | Shared-secret token/password + pairing + device tokens; Tailscale or trusted-proxy modes |
| Multi-tenancy | Per-row companyId; 75 DB tables | Single trusted operator per Gateway; multi-agent is multi-persona, not multi-tenant |
| Channels | None (it's a runner) | ~30 messaging channels + canvas + voice |
| Database | Postgres, many tables, normalized | JSON files in ~/.openclaw/; transcripts as JSONL |
| Permission model | --dangerously-skip-permissions default true | DM-pairing default; sandbox-by-default for non-main; elevated exec gated |
| Heartbeats | Core control loop (heartbeat.ts ~9k LOC) | Auxiliary tick on top of session activity (HEARTBEAT.md default prompt) |
| Extensibility | Adapters (~11) for different agent CLIs | Plugin SDK (~130 extensions) for providers, channels, hooks, context engines, memory |
The cleanest way to think about it: Paperclip is a container/process orchestrator for agent CLIs. OpenClaw is a runtime that happens to have channels glued on. Both run agents, but the agent is in different places relative to the platform code.
DRisks & sharp edges
Six things to keep in mind when running OpenClaw in any context that matters:
POST /v1/chat/completions, POST /v1/responses, and POST /tools/invoke ignore x-openclaw-scopes. A bearer token is full operator access on those routes. Only identity-bearing modes (trusted proxy, auth.mode: "none" on private ingress) honour per-request scopes. Researchers have repeatedly tried to file this as a bug; the docs treat it as documented behaviour.
agents.defaults.sandbox.mode defaults to off), tool execution runs on the host. The workspace is the agent's cwd, not an isolation boundary — absolute paths still reach other host locations. For DMs and shared channels, switch to sandbox.mode: "non-main" at minimum.
EConcrete improvements
Things that would make OpenClaw stronger as a product, not as a critique:
sandbox.mode currently defaults to off. For users who add a group channel or a public Slack workspace, the default of non-main would isolate the riskiest sessions automatically — and the trusted-operator model still works on the main DM session where it matters. The docs already recommend this; making it the default would catch users who don't read them.
FGlossary
- Gateway
- The single long-lived OpenClaw daemon process. Owns all messaging surfaces, exposes the WebSocket protocol, runs the embedded agent in-process.
- Agent
- A fully scoped persona: workspace, auth profiles, model registry, session store. One Gateway hosts one or many agents.
- Pi agent core
- The underlying agent runtime (models, tools, prompt pipeline) OpenClaw embeds. Not a separate process — imported as a library.
- Session
- A routing primitive plus a transcript. Each message is routed to a session by source; sessions are reused until reset.
- Session key
- The routing identifier
(agentId, channel, peer, scope). Lanes are keyed on this. - Lane
- A FIFO queue with a concurrency cap. Per-session lanes have cap 1 (serialize within a session); the global lane caps overall parallelism.
- Steering
- Injecting a mid-run prompt into the active runtime instead of queueing for a later turn. Default queue mode.
- Workspace
- The agent's single working directory. Default
~/.openclaw/workspace. Bootstrap files live here. - Bootstrap files
- The seven user-editable Markdown files (AGENTS, SOUL, IDENTITY, USER, TOOLS, BOOTSTRAP, MEMORY) injected into Project Context.
- Skill
- An AgentSkills-compatible folder with SKILL.md and instructions. Loaded on demand, not pre-injected.
- Plugin
- An
extensions/<id>/folder with openclaw.plugin.json. Adds providers, channels, hooks, slot fills. - Slot
- A plugin extension point with cardinality 1 (only one plugin can fill it). Examples: memory backend, context engine.
- Heartbeat
- An auxiliary tick that wakes the agent for scheduled checks. Reads HEARTBEAT.md; replies
HEARTBEAT_OKwhen nothing needs attention. - Dreaming
- Optional background consolidation that runs Light/Deep/REM phases over recent memory signals; promotes durable candidates into MEMORY.md.
- Compaction
- Summarizing older session messages into a single compact entry so the agent stays within the model's context window.
- Canvas
- Live HTML/CSS/JS workspace the agent can render. Served by the Gateway HTTP at
/__openclaw__/canvas/. - A2UI
- Agent-to-UI protocol surface, served at
/__openclaw__/a2ui/. - Node
- A device connected over WebSocket with
role: "node"— typically a phone or laptop that exposes camera, screen, location commands. - Owner / operator
- The single trusted human running the Gateway. Authentication establishes operator identity; there is no per-user split inside one Gateway.
- Sandbox
- Optional Docker/SSH/OpenShell-backed isolation for tool execution. Modes:
off,non-main,all. - Elevated exec
- Escape hatch from the sandbox: agent can request host-level execution with approvals. Controlled by
/elevated. - Sub-agent
- A child session spawned via
sessions_spawn. Isolated by default; runs in parallel to the main session. - ACP / ACPX
- The Agent Communication Protocol. ACPX is OpenClaw's embedded ACP runtime that lets the agent spawn Codex/Claude Code/OpenCode harness sessions.
- MCP
- Model Context Protocol. OpenClaw speaks both client and server roles.
- Pairing
- Device approval flow. A new device gets a pairing code; the operator approves it before the device can connect.
- DM policy
- How an unknown DM sender is treated. Default
pairingrequires explicit approval;openwith"*"allowlist accepts all. - Trusted-operator model
- The trust model. Authenticated callers are trusted; there is no per-user authorization inside one Gateway.
GFile index
The files referenced inline through the document, with one-line descriptions. Click any file pill anywhere in the document to view the relevant excerpt.
daemon entry point
WS server implementation
auth modes and token handling
hook dispatch
TypeBox protocol schemas
agent RPC entry
runEmbeddedPiAgent
runtime selection
buildAgentSystemPrompt
workspace resolution
bootstrap file loader
skill snapshot & allowlist
sandbox resolution
agent identity & ack
auto-compaction pipeline
sub-agent lifecycle
channel registration
session resolution
plugin loader, slots
public SDK barrel
session key computation
lane queue
provider plugin manifest sample
channel plugin manifest sample
ACP runtime plugin
delegate-to-CLI skill
workspace contract
loop lifecycle
prompt structure
queue modes
session lifecycle
memory model
dreaming phases
compaction pipeline
multi-agent routing
sandbox modes
trust model and report rules
root contributor guide
project direction