A field manual · openclaw.ai

OpenClaw Architecture Deep Dive

A close reading of the personal-assistant gateway, its embedded agent runtime, and the 130-plus plugins that make it speak twenty-some channels — from the source up.
Revision May 2026 Repository openclaw/openclaw For internal reference
Chapter one

01Overview & first principles

OpenClaw is a single, long-lived daemon — the Gateway — that owns every messaging surface you connect to it (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, twenty-odd more), runs a single embedded agent runtime inside the same process, and exposes a typed WebSocket protocol that clients, companion apps, and headless nodes plug into. Capability is delivered through ~130 plugins; the core stays plugin-agnostic.

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.

A personal assistant that is easy to use, supports a wide range of platforms, and respects privacy and security — the assistant runs on your devices, in your channels, with your rules.

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.

Chapter two

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 repository layout
src/ core TypeScript (~70 dirs) extensions/ ~130 plugins skills/ ~58 bundled skills docs/ user + ref packages/ SDK + contracts apps/ macOS, iOS, Android, swabble ui/ WebChat / Control UI scripts/ dev, e2e, k8s, podman security/ opengrep rules qa/ scenarios + lab Inside src/: gateway/ agents/ channels/ plugins/ tools/ plugin-sdk/ sessions/ memory/ talk/ tts/ cron/ commitments/ mcp/ node-host/ acp/ context-engine/ secrets/ media-*

Top-level directories

The repository decomposes into roughly eight first-class trees, each with a sharply different purpose:

DirectoryPurposeEdited 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.

Inference
Lean core, fat extensions, intentional. AGENTS.md (root) explicitly forbids bundling new capability into core if it can live behind the SDK: “Core stays plugin-agnostic. No bundled ids/defaults/policy in core when manifest/registry/capability contracts work.” The size of extensions/ versus src/ is a deliberate architectural outcome.

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:

src/gateway/server.impl.ts
main WebSocket server, method dispatch (~1.7k LOC)
src/agents/agent-command.ts
the agent RPC entry point (~1.6k LOC)
src/agents/system-prompt.ts
buildAgentSystemPrompt (~1.4k LOC)
src/agents/pi-embedded-runner/run.ts
runEmbeddedPiAgent core loop (~3.3k LOC)
src/agents/workspace.ts
workspace resolution & bootstrap files
src/agents/bootstrap-files.ts
load AGENTS.md / SOUL.md / USER.md
src/channels/registry.ts
channel registration & routing
src/plugins/*
plugin loader, slot orchestration
src/plugin-sdk/*
public SDK barrel
src/gateway/protocol/*
TypeBox-defined WS wire protocol
extensions/*/openclaw.plugin.json
per-plugin manifest
skills/*/SKILL.md
bundled skills
Chapter three

03The Gateway daemon

The Gateway is a single long-lived process. It maintains every messaging surface, exposes a typed WebSocket API on 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.

Improvement note
Where Paperclip's --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.

Risk R2 (cross-ref)
The HTTP compat surface inherits the trusted-operator model. An operator who exposes :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.
Chapter four

04Agent runtime (Pi-embedded)

OpenClaw runs one embedded agent runtime per Gateway, built on the Pi agent core (models, tools, prompt pipeline). When a message arrives, the Gateway validates it, resolves a session, and calls 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

1
RPC arrivesThe Gateway's 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.
2
agentCommand resolves run defaultsResolves model + thinking/verbose/trace defaults, loads a skills snapshot, calls runEmbeddedPiAgent, and emits lifecycle end/error as a fallback if the embedded loop forgets to.
3
runEmbeddedPiAgent serializes the runPer-session lane plus optional global lane. One active run per session; main lane defaults to concurrency 4, subagent to 8. Typing indicators fire immediately on enqueue so UX is unaffected.
4
Workspace + skills preparedWorkspace dir resolved (sandbox redirects allowed for non-main), skills loaded/reused from snapshot, bootstrap context files resolved.
5
System prompt assembledbuildAgentSystemPrompt 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.
6
Pi session built & subscribedModel + auth profile resolved (cooldown-aware rotation), Pi session opened, the runner subscribes to Pi events: assistant deltas → stream:"assistant", tool events → stream:"tool", lifecycle → stream:"lifecycle".
7
Hooks fire at known pointsbefore_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.
8
Reply shaping & suppressionFinal payload assembled from assistant text + reasoning + inline tool summaries, with NO_REPLY/no_reply filtered, duplicate messaging-tool sends removed, and fallback error replies emitted only when no other payload was produced.
9
Lifecycle end emittedLifecycle 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 runtimeagents.defaults.timeoutSeconds defaults 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>.timeoutSeconds extends this; otherwise default 120 s cap.
  • Stuck session diagnostics — with diagnostics on, diagnostics.stuckSessionWarnMs classifies long processing sessions as session.long_running, session.stalled, or session.stuck; abort-drain kicks in after diagnostics.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.

Chapter five

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:

  1. Identity line“You are a personal assistant running inside OpenClaw.”
  2. Tooling — the policy-filtered tool list, plus a reminder that tool names are case-sensitive and TOOLS.md is guidance, not availability.
  3. Sub-Agent Delegation (when delegationMode: "prefer") — instructs the main agent to act as a responsive coordinator and push anything bigger than a direct reply through sessions_spawn.
  4. Tool Call Style — narration discipline, approval guidance, allow-once semantics, full-command preservation for /approve.
  5. Execution Bias — act in-turn, continue until done or blocked, recover from weak tool results, verify before finalizing.
  6. Safety — no independent goals; safety/oversight over completion; never bypass safeguards.
  7. OpenClaw Control — prefer the gateway tool for config/restart, do not invent CLI commands.
  8. Skills — how to load skill instructions on demand.
  9. MemoryMEMORY.md + daily files (when available).
  10. OpenClaw Self-Update — config.schema.lookup, config.get/patch/apply, update.run only on explicit request.
  11. Model Aliases — when configured.
  12. Workspace — working directory + treatment guidance.
  13. Documentation — local path to OpenClaw docs/source.
  14. Sandbox (when enabled) — sandbox paths, elevated exec state.
  15. User identity — owner numbers (raw or hashed).
  16. Current Date & Time — time zone only; the live clock comes from session_status so the prompt stays cache-stable.
  17. Workspace Files (injected) — marker; the actual files appear under Project Context below.
  18. Assistant Output Directives — attachment/voice-note/reply-tag syntax.
  19. Reasoning Format (when reasoningTagHint) — <think>/<final> rules.
  20. Project ContextAGENTS.md, SOUL.md, IDENTITY.md, USER.md, TOOLS.md, BOOTSTRAP.md, MEMORY.md, in that fixed precedence order.
  21. Silent RepliesNO_REPLY token rules.
  22. 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:

  1. Override a small set of named core sections — interaction_style, tool_call_style, execution_bias.
  2. Inject a stable prefix above the cache boundary.
  3. 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.

Chapter six

06Workspace & bootstrap files

Every agent has a single working directory. Inside it sit a small set of user-editable Markdown files that are injected into Project Context on the first turn of every new session. The contract is fixed and stable, and OpenClaw goes out of its way to make these files the user's primary configuration surface — not JSON.

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

FileRoleOrder
AGENTS.mdOperating instructions and durable “memory”. The workhorse — most behaviour customization lives here.10
SOUL.mdPersona, voice, boundaries, default bluntness, tone, humour. Short beats long; sharp beats vague.20
IDENTITY.mdAgent name, vibe, emoji. Used for ack reactions and identity-bearing surfaces.30
USER.mdUser profile, preferred address, language preferences, work context.40
TOOLS.mdUser-maintained tool notes (imsg, sag, conventions). Guidance, not availability — the prompt explicitly states this.50
BOOTSTRAP.mdOne-time first-run ritual file. Only created for a brand-new workspace; deleted after the ritual completes.60
MEMORY.mdDurable 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.

Inference
The fact that seven Markdown files in a known order is the primary way to configure agent behaviour is the strongest signal that OpenClaw is built for one human operator iterating on their own assistant, not for a SaaS layer hiding configuration behind UI. Your SOUL.md is your assistant's most important file.
Chapter seven

07Skills, tools & the plugin SDK

Skills are AgentSkills-compatible directories of behaviour the agent can load on demand. Tools are the core verbs the agent always has (read, exec, web_fetch, sessions_spawn). Plugins are everything else — providers, channels, hooks, context engines, memory backends — declared in openclaw.plugin.json and loaded through a slot-based registry. The three abstractions are intentionally separate and stack cleanly.

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:

#SourcePath
1Workspace skills<workspace>/skills
2Project agent skills<workspace>/.agents/skills
3Personal agent skills~/.agents/skills
4Managed/local skills~/.openclaw/skills
5Bundled skillsshipped with the install
6Extra skill foldersskills.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.

Chapter eight

08Channels & the multi-channel inbox

A channel in OpenClaw is a messaging surface — WhatsApp, Slack, Discord, iMessage, twenty-some more — implemented as a plugin under 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:

SourceDefault behaviour
Direct messagesShared session (with dmScope: "main") — fine for single-user setups.
Group chatsIsolated per group.
Rooms/channelsIsolated per room.
Cron jobsFresh session per run.
WebhooksIsolated 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.

Chapter nine

09Sessions, queue & steering

A session is a routing primitive plus a transcript. Each message gets routed to a session based on its source (DM/group/room/cron/webhook), and a session is reused until it expires by daily reset, idle reset, or manual /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 sessionId started (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 on lastInteractionAt — the last real user/channel interaction.
  • Manual reset/new or /reset in 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.

Chapter ten

10Memory, dreaming & compaction

Memory in OpenClaw is plain Markdown files in the agent workspace. No hidden vector store, no opaque embeddings layer in the default path — the model only “remembers” what gets written to disk. On top of that simple base, OpenClaw layers a context engine (pluggable), an optional dreaming subsystem (light/deep/REM phases that promote signal into durable memory), and an auto-compaction pipeline that summarizes older messages when the context window fills up.

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:

PhasePurposeDurable write
LightSort and stage recent short-term material; dedupe; record reinforcement signals for later ranking.No
DeepScore and promote durable candidates. Requires minScore, minRecallCount, minUniqueQueries to pass. Rehydrates snippets from live daily files; stale ones are skipped.Yes (MEMORY.md)
REMReflect 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:

  1. OpenClaw reminds the agent to save important notes to memory files (so context loss is recoverable).
  2. Older turns are summarized into a single compact entry.
  3. The summary is saved in the session transcript.
  4. Recent messages are kept intact.
  5. 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.

Chapter eleven

11Security model & sandboxing

OpenClaw treats one Gateway as a single-operator trust boundary. There is no per-user authorization model inside the daemon: anyone with the gateway secret has full operator capability. The security work happens at ingress (DM pairing, channel allowlists, sandbox-by-default for non-main sessions, explicit elevated-exec gates) rather than between operators sharing one daemon. The threat model is documented in detail in SECURITY.md.

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 — narrower x-openclaw-scopes headers 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-main sessions (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_WS exists 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 via x-openclaw-scopes headers, but under shared-secret auth those headers are ignored. The docs say this loudly because researchers have tried to file it as a bug.
Improvement note
OpenClaw's security posture is unusually well-documented — explicit threat model, explicit non-vulnerabilities, explicit recommended deployment. The trade-off it makes is clear: power for the trusted operator at the cost of multi-user isolation inside one daemon. If you ever wrap OpenClaw for a multi-tenant product, that boundary is the one you have to add yourself; do not assume the Gateway will do it for you.
Appendix A

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.

Appendix B

BEnd-to-end lifecycle

Trace a single inbound WhatsApp message all the way through:

Inbound message → agent reply (simplified)
User WhatsApp plugin Gateway Agent runtime Provider DM message inbound-event allowlist + DM policy resolve session key agent() RPC handler queue: per-session lane build system prompt Pi → provider call assistant deltas agent stream events message.send reply delivered

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.

Appendix C

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:

DimensionPaperclipOpenClaw
ShapeHosted multi-tenant SaaSLocal-first daemon, one operator
Agent executionSpawns external CLIs (Claude Code, Codex, Gemini) as subprocessesImports Pi agent core; runs the loop in-process
Prompt deliveryStdin sections + --append-system-prompt-file + AGENTS.mdDirect buildAgentSystemPrompt renderer with cache boundary + provider contributions
SkillsFilesystem-mounted via --add-dir; agent loads on demandSix precedence-ordered roots; allowlists per agent; agent loads on demand via read
ToolsWhatever the spawned CLI providesCore OpenClaw tools + plugin-contributed tools + MCP tools
AuthHS256 JWT, 48 h TTLShared-secret token/password + pairing + device tokens; Tailscale or trusted-proxy modes
Multi-tenancyPer-row companyId; 75 DB tablesSingle trusted operator per Gateway; multi-agent is multi-persona, not multi-tenant
ChannelsNone (it's a runner)~30 messaging channels + canvas + voice
DatabasePostgres, many tables, normalizedJSON files in ~/.openclaw/; transcripts as JSONL
Permission model--dangerously-skip-permissions default trueDM-pairing default; sandbox-by-default for non-main; elevated exec gated
HeartbeatsCore control loop (heartbeat.ts ~9k LOC)Auxiliary tick on top of session activity (HEARTBEAT.md default prompt)
ExtensibilityAdapters (~11) for different agent CLIsPlugin 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.

Appendix D

DRisks & sharp edges

Six things to keep in mind when running OpenClaw in any context that matters:

R1 — One Gateway, one operator
There is no per-user authorization model inside a Gateway. Anyone with the gateway secret has full operator capability. If you ever wrap OpenClaw inside a multi-user product, you must add the boundary yourself — one Gateway per user, isolated by host/VPS, not one Gateway with multiple operators. The trust model bakes this assumption into HTTP compat endpoints, tool execution, exec approvals, and pairing.
R2 — HTTP compat surface bypasses scope headers
Under shared-secret auth, 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.
R3 — Workspace is default cwd, not a hard sandbox
Without sandboxing enabled (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.
R4 — Bootstrap files are also attack surface
The seven Markdown files in the workspace (AGENTS.md, SOUL.md, etc.) are injected into Project Context on every new session. If an attacker can write to the workspace, they can effectively rewrite the agent's behaviour on the next session — including its safety guidance, tool-call style, sub-agent delegation, and so on. The workspace is part of the trusted boundary; it should sit behind the same OS-level controls as your secrets.
R5 — Plugins are trusted code
Installing a plugin grants it the same trust level as local code running on the Gateway host. There is no plugin sandbox. Vetted plugins (official ClawHub entries) carry signals about ownership and review; unvetted plugins do not. The bar for adding plugins to your Gateway should match the bar for adding code to your machine.
R6 — Codebase weight
Eighteen thousand files, ~130 extensions, ~3,300-line agent runner, ~1,700-line server impl, ~1,400-line prompt builder. The architectural intent (lean core, fat extensions) is sound, but in practice the surface area is enormous. Audit your effective set of enabled plugins; the actual attack surface is determined by which extensions you load, not which exist in the repo.
Appendix E

EConcrete improvements

Things that would make OpenClaw stronger as a product, not as a critique:

I1 — A formal per-user boundary inside the Gateway
The “one operator per Gateway” model works for individuals, but pushes the entire multi-user story off to host-level isolation. A first-class per-user namespace inside one Gateway (scoped tokens, per-user session keys, per-user workspace, per-user auth profiles, hard boundary) would unlock small-team deployments without VPS-per-user overhead. The plumbing largely exists (multi-agent already gives per-agent isolation); what's missing is calling the boundary a security one.
I2 — Plugin signing & install-time review
ClawHub provides provenance and official-publisher status, but a Gateway has no built-in verification that an installed plugin matches the ClawHub-vetted artifact. A signature check on install (matching an official publisher key) and a hash check at load time would close the “did the plugin change since I trusted it” gap.
I3 — Workspace integrity
Given that the seven bootstrap files are effectively part of the trusted boundary (R4), the Gateway could emit a warning when those files are modified outside the agent's own writes — particularly when they change between sessions. A simple hash-on-load signal would surface drift caused by either user edits or unwanted writes.
I4 — More aggressive sandbox defaults for shared channels
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.
I5 — Codebase signposting
With 70 src/ subdirectories and 130 extensions, the largest cost for new contributors is finding the right file. Root AGENTS.md does a heroic amount of signposting; a per-subsystem one-page “map” doc with the actual entry points and contract boundaries would multiply that. Several already exist for narrow subsystems; broadening the coverage would help.
Appendix F

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_OK when 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 pairing requires explicit approval; open with "*" allowlist accepts all.
Trusted-operator model
The trust model. Authenticated callers are trusted; there is no per-user authorization inside one Gateway.
Appendix G

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.

src/gateway/server.ts
daemon entry point
src/gateway/server.impl.ts
WS server implementation
src/gateway/auth.ts
auth modes and token handling
src/gateway/hooks.ts
hook dispatch
src/gateway/protocol/*
TypeBox protocol schemas
src/agents/agent-command.ts
agent RPC entry
src/agents/pi-embedded-runner/run.ts
runEmbeddedPiAgent
src/agents/pi-embedded-runner/runtime.ts
runtime selection
src/agents/system-prompt.ts
buildAgentSystemPrompt
src/agents/workspace.ts
workspace resolution
src/agents/bootstrap-files.ts
bootstrap file loader
src/agents/skills.ts
skill snapshot & allowlist
src/agents/sandbox.ts
sandbox resolution
src/agents/identity.ts
agent identity & ack
src/agents/compaction.ts
auto-compaction pipeline
src/agents/subagent-registry.ts
sub-agent lifecycle
src/channels/registry.ts
channel registration
src/channels/session.ts
session resolution
src/plugins/*
plugin loader, slots
src/plugin-sdk/*
public SDK barrel
src/sessions/session-id.ts
session key computation
src/process/command-queue.ts
lane queue
extensions/anthropic/openclaw.plugin.json
provider plugin manifest sample
extensions/whatsapp/openclaw.plugin.json
channel plugin manifest sample
extensions/acpx/openclaw.plugin.json
ACP runtime plugin
skills/coding-agent/SKILL.md
delegate-to-CLI skill
docs/concepts/agent.md
workspace contract
docs/concepts/agent-loop.md
loop lifecycle
docs/concepts/system-prompt.md
prompt structure
docs/concepts/queue.md
queue modes
docs/concepts/session.md
session lifecycle
docs/concepts/memory.md
memory model
docs/concepts/dreaming.md
dreaming phases
docs/concepts/compaction.md
compaction pipeline
docs/concepts/multi-agent.md
multi-agent routing
docs/gateway/sandboxing.md
sandbox modes
SECURITY.md
trust model and report rules
AGENTS.md
root contributor guide
VISION.md
project direction