Phase 6 — Deployment
Run OpenClaw as a system service that starts at boot, survives reboots, and is locked down at the network level. Assumes Phase 3 security baseline complete.
- Coming from Phase 1 quick start? Each isolation model section below covers migrating your existing config to the dedicated user/VM — stop the personal gateway first, then follow the migration steps in your chosen section.
- Fresh dedicated machine? Each section installs OpenClaw from scratch in the right place — no prior installation needed.
Choose your deployment method and skip the others — each section is self-contained:
- Docker Containerized — official Docker setup, simplest path
- Pragmatic Single Agent — two-agent (main + search), guard plugins, full native OS access, no Docker
- Docker Isolation (recommended) — macOS or Linux, dedicated OS user with Docker sandboxing
- VM: macOS VMs — macOS hosts, stronger host isolation, no Docker inside
- VM: Linux VMs — any host, strongest combined (VM + Docker)
Shared sections (apply to all methods): Secrets Management (read first) | Firewall | Tailscale ACLs | Signal Setup | Verification | Emergency
Deployment Methods Overview
| Method | Isolation | Sandboxing | Best for |
|---|---|---|---|
| Pragmatic Single Agent | OS user or VM | Guard plugins (no Docker) | Full native OS access, simplicity |
| Docker Containerized | Container boundary | Docker (gateway runs inside container) | VPS, cloud, fastest path |
| Docker Isolation (recommended) | OS user boundary | Docker (per-agent sandboxing) | Dedicated hardware, full control |
| VM: macOS VMs | Kernel-level VM | Tool policy only (no Docker) | macOS hosts, strongest host isolation |
| VM: Linux VMs | Kernel-level VM | Docker inside VM | Any host, strongest combined |
Hosting Options
- Local/dedicated hardware (Mac Mini, NUC, etc.) — this guide’s primary focus
- Cloud VPS (Hetzner, DigitalOcean, Linode) — use Docker Containerized or Linux VM method
- GCP/AWS/Azure — works with any small VM; use Docker Containerized for simplest setup
- Hosted Mac (MacStadium, AWS EC2 Mac) — use macOS VM or Docker Isolation method
Decision: Foreground vs Service
Foreground (openclaw start) | System service | |
|---|---|---|
| Starts at boot | No | Yes |
| Survives logout | No | Yes |
| Log management | Terminal | Log files |
| Best for | Development, testing | Production |
For anything beyond testing, run as a system service.
Deployment: Choose Your Method
Before setting up the service, choose your deployment method. See Security: Deployment Isolation Options for the full trade-off analysis of the isolation models.
- Pragmatic Single Agent — two-agent setup (main + search), all five guard plugins, no Docker. Full native OS access. See the dedicated guide for setup.
- Docker Containerized — official
docker-setup.sh, gateway runs inside a Docker container. Simplest path. - Docker Isolation (recommended) — multi-agent gateway as
openclawuser with Docker sandboxing. macOS or Linux. - VM: macOS VMs (Lume / Parallels) — single macOS VM, multi-agent gateway, no Docker inside VM. macOS hosts only.
- VM: Linux VMs (Multipass / KVM / UTM) — Linux VM with Docker inside. Strongest combined posture (VM boundary + Docker sandbox). macOS or Linux hosts.
The multi-agent isolation models (Docker Isolation, macOS VMs, Linux VMs) all use the same multi-agent architecture with sessions_send delegation. They differ in the outer boundary and internal sandboxing. The Pragmatic Single Agent is a simpler alternative that trades container isolation for full native OS access — it still uses a search agent for web delegation (and content-guard at that boundary) but runs without Docker. See Phase 3: Deployment Isolation Options
for the full comparison.
- Docker Isolation: OS user boundary + Docker sandbox. LaunchDaemon/systemd on host (LaunchAgent for auto-login setups).
- VM: macOS VMs: Kernel-level VM boundary + standard user (no sudo). LaunchAgent inside VM (auto-login required). No Docker.
- VM: Linux VMs: Kernel-level VM boundary + Docker sandbox inside VM. systemd inside VM.
Secrets Management (All Methods)
Keep openclaw.json secrets-free — use ${ENV_VAR} references in config, store actual values in the service plist (macOS) or environment file (Linux). This applies to all deployment methods.
Choose your secrets method:
- Docker isolation (macOS) → LaunchDaemon
EnvironmentVariablesblock (or LaunchAgent for auto-login setups) - Docker isolation (Linux) →
/etc/openclaw/secrets.envfile - VM: macOS/Linux → SSH-load secrets before gateway start
Secrets to externalize
| Secret | Env var | Notes |
|---|---|---|
| Gateway token | OPENCLAW_GATEWAY_TOKEN | Included in all plist/systemd examples below |
| Anthropic API key | ANTHROPIC_API_KEY | SDK reads from env directly |
| Brave search key | BRAVE_API_KEY | Referenced as ${BRAVE_API_KEY} in config |
| OpenRouter key | OPENROUTER_API_KEY | If using Perplexity via OpenRouter |
| GitHub token | GITHUB_TOKEN | Fine-grained PAT — see GitHub token setup below |
| (channel-guard and content-guard both require OPENROUTER_API_KEY) | See plugin setup |
Empty env vars cause startup failure. If a
${VAR}reference resolves to an empty string, the gateway exits withEX_CONFIG(exit 78). For optional keys not yet provisioned (e.g.,BRAVE_API_KEYwhen using Perplexity instead), use a non-empty placeholder like"not-configured"rather than leaving the variable empty or unset.Version note (2026.2.16): Telegram bot tokens are now auto-redacted from gateway logs (same mechanism as
redactSensitive: "tools"for other secrets).External Secrets Management (2026.2.26+): OpenClaw now supports a dedicated
openclaw secretsworkflow for centralized credential management. This lets you store secrets outside your config entirely — using a secrets provider or vault — and reference them viaSecretRefplaceholders in config. Key commands:openclaw secrets audit(find hardcoded secrets),openclaw secrets configure(set up provider),openclaw secrets apply(write SecretRefs to config),openclaw secrets reload(activate at runtime without restart). See official docs for provider-specific setup. The${ENV_VAR}method documented below remains fully supported.
GitHub token setup
Use a fine-grained personal access token (not a classic token). Fine-grained PATs let you scope access to specific repositories with minimal permissions.
Token name: e.g.,
openclaw-workspace-syncExpiration: set a reasonable expiry (e.g., 90 days) and create a reminder to rotate
Repository access: select “Only select repositories” → pick only your workspace repos
Permissions:
Permission Access Why Contents Read and write Push/pull workspace commits Metadata Read Automatically selected (read-only) No other permissions needed. Do not grant “All repositories” access.
Click Generate token — the value starts with
github_pat_
Set the token in your LaunchAgent plist (macOS) or environment file (Linux) as GITHUB_TOKEN. The gh CLI and git push/pull over HTTPS both read from this env var.
Rotation: When the token expires, generate a new one with the same settings and update the plist/env file. Restart the gateway to pick up the new value.
Config references
In openclaw.json, reference secrets with ${...} — OpenClaw substitutes at startup:
{
"tools": {
"web": { "search": { "apiKey": "${BRAVE_API_KEY}" } }
},
"gateway": {
"auth": { "token": "${OPENCLAW_GATEWAY_TOKEN}" }
}
}Result: openclaw.json contains zero plaintext secrets — safe to copy between VMs, diff, or version-control.
CLI commands and env vars
The ${VAR} substitution happens at config load time — not just for the gateway service, but for every openclaw command that reads openclaw.json (e.g., openclaw doctor, openclaw memory index, openclaw config get). If the referenced env vars aren’t available, the command exits with EX_CONFIG (exit 78).
The problem: Plist EnvironmentVariables are only injected by launchd into the service process. When you run sudo -u openclaw openclaw doctor from an admin shell, those vars aren’t present.
Solutions (pick one):
~/.openclaw/.envfile (recommended) — OpenClaw loads this automatically regardless of how it’s invoked. Put non-secret config vars here and keep actual secrets in the plist. Or duplicate secrets here with locked-down permissions (chmod 600):sudo -u openclaw tee /Users/openclaw/.openclaw/.env > /dev/null << 'EOF' ANTHROPIC_API_KEY=sk-ant-... OPENCLAW_GATEWAY_TOKEN=... BRAVE_API_KEY=BSA... EOF sudo chmod 600 /Users/openclaw/.openclaw/.envNote:
.envfiles are non-overriding — plist env vars take precedence if both are set.Pass vars inline — for one-off commands:
sudo -u openclaw env ANTHROPIC_API_KEY=sk-ant-... openclaw doctorenv.shellEnv— import missing vars from the user’s login shell (if secrets are set there):{ "env": { "shellEnv": { "enabled": true } } }
Env var precedence (highest wins): process environment → CWD
.env→~/.openclaw/.env→ configenvblock → shell import. See official docs for details.
Docker Containerized Gateway
Simplest path. The official Docker setup runs the entire gateway inside a container with persistence, auto-restart, and basic security. No per-agent sandboxing — all agents share the container.
When to use: Cloud VPS, quick evaluation on any Docker-capable host, or when per-agent Docker sandboxing isn’t needed.
Quick start:
curl -fsSL https://openclaw.ai/docker-setup.sh | bashFor production, review the script before piping to shell, or download and inspect first:
curl -fsSL https://openclaw.ai/docker-setup.sh -o docker-setup.sh && less docker-setup.sh && bash docker-setup.sh
This creates a Docker Compose setup with:
- Persistent data volume (survives container restarts)
- Auto-restart on crash (
restart: unless-stopped) - Loopback binding (not exposed to network by default)
- Environment variable passthrough for secrets
What it doesn’t provide (compared to Docker Isolation below):
- No dedicated OS user — the container runs as whatever user Docker assigns
- No per-agent sandboxing — all agents share the same container filesystem and network
- No LaunchAgent/systemd integration — relies on Docker’s restart policy
For production deployments on dedicated hardware where you want per-agent isolation and OS-level service management, use Docker Isolation below instead.
Timezone support (2026.3.13-1+): Set
OPENCLAW_TZin the container environment to control the gateway’s timezone (e.g.,OPENCLAW_TZ=Europe/Stockholm). Without this, the container uses UTC. This affects daily memory file naming, session reset times, and cron job scheduling. Pass it via your Docker Composeenvironment:block ordocker run -e OPENCLAW_TZ=....
Security — Docker build context token leak (2026.3.13-1+): Do not pass
OPENCLAW_GATEWAY_TOKENor other secrets as Docker build arguments (--build-arg). Build args are embedded in the image layer cache and visible viadocker history. Use runtime environment variables (-e/ Composeenvironment:) or Docker secrets for all credential injection.
Official docs: See docs.openclaw.ai for the latest Docker setup instructions and options.
Docker Isolation
Recommended approach. Works on both macOS and Linux. Single gateway, multi-agent, Docker sandboxing for internal isolation.
Automated setup: For a fresh dedicated macOS machine, see
scripts/docker-isolation/— three scripts that automate everything below.
Installation Scope
The OpenClaw installer (curl ... | bash) runs npm install -g openclaw, placing files in the global npm prefix. On a service deployment, global install is preferred:
- Global install (recommended): Admin installs OpenClaw once. The
openclawuser — in thestaffgroup by default on macOS — can run/opt/homebrew/bin/openclawwithout its own Node.js install. The LaunchAgent plist references these paths directly. - Per-user install: Alternative if you can’t modify global packages. Requires updating
ProgramArgumentsin the plist to point at the user’s local npm prefix (e.g.,/Users/openclaw/.npm-global/...).
On Linux, global install places the binary at /usr/local/bin/openclaw — accessible to all users by default.
Warning: On macOS, the
staffgroup has write access to/opt/homebrewby default. Any user instaff(including theopenclawuser) can modify binaries there — a compromisedopenclawuser could trojan/opt/homebrew/bin/node, affecting all users who run it. Mitigations: (1)sudo chown root:wheel /opt/homebrew/bin/nodeto remove group write (re-apply afterbrew upgrade), or (2) install Node.js per-user via nvm so each user runs their own copy. This is lower risk for single-user deployments where only theopenclawuser runs Node.js.
Dedicated OS User
If you haven’t already (from Phase 3 ), create a dedicated non-admin user:
VM isolation: Skip this section — you’ll create a dedicated user inside the VM instead. macOS VMs: see Dedicated user (inside VM) . Linux VMs: see Dedicated user (inside Linux VM) .
macOS:
sudo sysadminctl -addUser openclaw -fullName "OpenClaw" -password "<temp>" \
-home /Users/openclaw -shell /bin/zsh
sudo passwd openclaw
# Create home directory if not auto-created
sudo mkdir -p /Users/openclaw
sudo chown -R openclaw:staff /Users/openclawThe user is automatically in the staff group, which gives read access to /opt/homebrew (where Node.js and OpenClaw are installed). No admin group membership needed.
Linux:
sudo useradd -m -s /bin/bash openclaw
sudo passwd openclawMixed-use machine (personal data on host)? Creating a dedicated
openclawuser doesn’t automatically protect your personal files. Lock down your admin home directory:chmod 700 /Users/youradmin # macOS — replace with your username chmod 700 /home/youradmin # LinuxWithout this, the
openclawuser may be able to read world-readable files in your home directory (macOS doesn’t always default home directories to700). Also be aware of residual multi-user exposure that no permission change fixes:
- Process listings —
ps auxshows all users’ processes and command-line arguments. Never run commands with secrets in arguments (e.g.,curl -H "Authorization: Bearer sk-...")- Shared temp directories —
/tmpand/var/tmpare accessible by all users- Mounted volumes — external drives and NAS mounts are typically world-readable
These are standard multi-user OS risks, not OpenClaw-specific. On a dedicated machine with no personal data, these are non-issues — see the dedicated machine note .
Docker Group Membership
The openclaw user needs access to the Docker socket for agent sandboxing:
macOS:
sudo dseditgroup -o edit -a openclaw -t user dockerLinux:
sudo usermod -aG docker openclawSecurity note: On Linux, the
dockergroup grants effective root access on the host via the Docker socket. For bare Linux deployments, this is an accepted risk — see the warning at Linux VM isolation . The dedicated machine posture or VM boundary contains this risk. On macOS, Docker Desktop manages access through its own application model — thedockergroup controls CLI access but doesn’t grant the same host-level root equivalent.
Verify access:
sudo -u openclaw docker psThis should list running containers (or show an empty list if none running) without errors. If you see “permission denied”, the group membership hasn’t taken effect — log out and back in, or restart the daemon.
Install OpenClaw
If OpenClaw is already installed globally (e.g., via Homebrew or by the admin user), skip the install and verify the openclaw user can access it:
sudo -u openclaw openclaw --version
sudo -u openclaw openclaw doctorTwo sudo -u openclaw patterns:
- Bare form:
sudo -u openclaw <cmd>— for simple commands bash -cform:sudo -u openclaw bash -c 'cd ... && HOME=... <cmd>'— when command requires specific working directory or HOME
Otherwise, install as the openclaw user:
sudo -u openclaw bash -c 'curl -fsSL https://openclaw.ai/install.sh | bash'
sudo -u openclaw openclaw --version
sudo -u openclaw openclaw doctorDownload and review the script before running, or verify the source URL.
Then either migrate from your personal user (below) or create a fresh config:
# Fresh install (skip if migrating)
sudo -u openclaw openclaw setupDo not use
openclaw onboard --install-daemonoropenclaw gateway install— these install a LaunchAgent with labelai.openclaw.gatewayunder the current user. We create our own LaunchAgent (labelai.openclaw.gateway) under the dedicatedopenclawuser, with explicit secrets and path control. Thedisable-launchagentmarker prevents OpenClaw from auto-installing its own plist.The
ProgramArgumentsin the plist must match where OpenClaw is actually installed. The “Verify paths” step before creating the plist covers this.
Required config: gateway.mode
The gateway refuses to start unless gateway.mode is set in openclaw.json. Add this to the config (via openclaw setup or manually):
{
"gateway": {
"mode": "local"
}
}Without this, the gateway exits immediately. Run openclaw doctor to diagnose startup failures — it checks config validity, API key availability, Docker access, and file permissions. Fix any issues it reports before proceeding.
Move OpenClaw data
If you were running as your own user, move the data instead of running openclaw setup:
sudo cp -r ~/.openclaw /Users/openclaw/.openclaw
sudo chown -R openclaw:staff /Users/openclaw/.openclawUpdate all paths in openclaw.json to use /Users/openclaw/.openclaw/... (macOS) or /home/openclaw/.openclaw/... (Linux).
Log directory
The gateway auto-creates agent, workspace, and session directories on startup. The log directory must exist before the service starts (launchd won’t create it):
sudo -u openclaw mkdir -p /Users/openclaw/.openclaw/logsFile permissions
After the first successful gateway start (which creates the directory tree), set ownership and lock down permissions:
# Set ownership first (critical — setup/copy commands run as root)
sudo chown -R openclaw:staff /Users/openclaw/.openclaw
# Then restrict permissions
sudo chmod 700 /Users/openclaw # Lock down home directory itself (on macOS, this may exclude it from Spotlight indexing and Time Machine backups — acceptable for a service account)
sudo chmod 700 /Users/openclaw/.openclaw
sudo chmod 600 /Users/openclaw/.openclaw/openclaw.json
sudo chmod 600 /Users/openclaw/.openclaw/credentials/*.json
sudo chmod 600 /Users/openclaw/.openclaw/agents/*/agent/auth-profiles.json
sudo chmod 600 /Users/openclaw/.openclaw/identity/*.json
sudo chmod -R 600 /Users/openclaw/.openclaw/credentials/whatsapp/default/*
sudo chmod 700 /Users/openclaw/.openclaw/credentials/whatsappmacOS: LaunchDaemon
VM isolation: macOS VMs — use the LaunchAgent (Inside VM) section below (VMs typically configure auto-login). Linux VMs — use Linux: systemd .
A LaunchDaemon runs in the system domain — starts at boot before any user logs in, no GUI session required. The UserName and GroupName keys make launchd run the process as the dedicated openclaw user. This is the correct pattern for dedicated headless service accounts.
After starting, verify the process UID:
ps aux | grep openclaw— confirm theUSERcolumn showsopenclaw, notroot. TheUserNamekey is Apple’s official mechanism for privilege drop in system daemons.
| LaunchDaemon (recommended) | LaunchAgent (auto-login only) | |
|---|---|---|
| Starts at boot | Yes — no GUI session required | Only if user has GUI auto-login configured |
openclaw gateway restart | Does not work — use launchctl instead | Works |
| OrbStack Docker socket | Via /var/run/docker.sock (OrbStack must be running) | Automatic — same user session |
| Agent persistence via LaunchAgents | Blocked (no gui/<uid> domain) | Possible |
| When to use | Dedicated headless service account (most deployments) | Personal Mac or VM with auto-login |
Label convention: OpenClaw’s built-in service installer (
openclaw gateway install) uses the labelai.openclaw.gateway. Our manual plist also usesai.openclaw.gateway— they share the same label. Thedisable-launchagentmarker prevents OpenClaw from auto-installing its own plist and conflicting with ours.
Create the plist
Verify paths first:
which openclaw
which node
readlink -f $(which openclaw)sudo tee /Library/LaunchDaemons/ai.openclaw.gateway.plist > /dev/null << 'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ai.openclaw.gateway</string>
<key>UserName</key>
<string>openclaw</string>
<key>GroupName</key>
<string>staff</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>Umask</key>
<integer>63</integer>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/node</string>
<string>/opt/homebrew/lib/node_modules/openclaw/dist/index.js</string>
<string>gateway</string>
<string>--port</string>
<string>18789</string>
</array>
<key>StandardOutPath</key>
<string>/Users/openclaw/.openclaw/logs/gateway.log</string>
<key>StandardErrorPath</key>
<string>/Users/openclaw/.openclaw/logs/gateway.err.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/openclaw</string>
<key>OPENCLAW_HOME</key>
<string>/Users/openclaw</string>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>OPENCLAW_GATEWAY_PORT</key>
<string>18789</string>
<key>DOCKER_HOST</key>
<string>unix:///var/run/docker.sock</string>
<key>OPENCLAW_GATEWAY_TOKEN</key>
<string><!-- See Secrets Management section above --></string>
<key>ANTHROPIC_API_KEY</key>
<string><!-- See Secrets Management section above --></string>
<key>BRAVE_API_KEY</key>
<string><!-- See Secrets Management section above --></string>
<key>GITHUB_TOKEN</key>
<string><!-- See Secrets Management section above --></string>
<key>OPENCLAW_SERVICE_MARKER</key>
<string>openclaw</string>
<key>OPENCLAW_SERVICE_KIND</key>
<string>gateway</string>
</dict>
</dict>
</plist>
PLIST
Umaskkey:<integer>63</integer>is octal077— owner-only permissions for all gateway-created files (sessions, transcripts, etc.). Without this, npm upgrades can reset the process umask to the system default (022), making files group/world-readable. Added in OpenClaw 2026.3.2’s auto-generated plists; apply manually to existing plists.
If using Docker (OrbStack)
OrbStack runs in a user’s GUI session and exposes Docker via a user-specific socket (~/.orbstack/run/docker.sock). OrbStack’s privileged helper also creates a system-wide symlink at /var/run/docker.sock when OrbStack starts — this is what the LaunchDaemon uses via the DOCKER_HOST env var in the plist above. The socket is world-connectable (mode 755), so the openclaw user can reach it without any bootstrapping step.
Requirement: OrbStack must be running in some user’s GUI session before the gateway performs Docker operations. For reliable boot behavior, configure auto-login for the admin user so OrbStack starts automatically on boot.
Verify Docker is accessible from the openclaw user:
sudo -u openclaw docker psIf this fails, OrbStack is not running yet. Start OrbStack from the admin account and retry.
Manage the daemon
# Start
sudo launchctl bootstrap system /Library/LaunchDaemons/ai.openclaw.gateway.plist
# Stop
sudo launchctl bootout system/ai.openclaw.gateway
# Restart
sudo launchctl bootout system/ai.openclaw.gateway && sleep 1 && \
sudo launchctl bootstrap system /Library/LaunchDaemons/ai.openclaw.gateway.plist
# Check status
sudo launchctl print system/ai.openclaw.gateway 2>&1 | head -10
# Check it's listening
sudo lsof -i :18789
# View logs
tail -f /Users/openclaw/.openclaw/logs/gateway.logNote: Use
launchctl bootout/launchctl bootstrap(notopenclaw gateway restart) — theopenclaw gatewaylifecycle commands target thegui/<uid>launchd domain, which does not exist for headless service accounts.
Config reload without restart
OpenClaw watches openclaw.json for changes automatically. The default reload mode is hybrid — safe changes (tool policies, agent definitions) are hot-applied, while critical changes trigger an in-process restart. No manual action needed for most config edits.
To force an immediate reload:
sudo kill -USR1 $(pgrep -f "openclaw.*gateway")Use a full launchctl restart only for binary updates or when auto-reload doesn’t pick up your changes. Disable auto-reload with gateway.reload.mode: "off" if you prefer manual control.
Alternative: LaunchAgent (auto-login sessions only)
A LaunchAgent running in the openclaw user’s gui/<uid> domain gives openclaw gateway restart CLI compatibility and automatic OrbStack Docker socket access — but only if the user has an active GUI session. The gui/<uid> domain is created by loginwindow on GUI login and destroyed on logout; it cannot be bootstrapped for a headless service account that never logs in.
Use LaunchAgent when:
- Running on a personal Mac where you’re always logged in, or
- Inside a macOS VM with auto-login configured for the service user
Prerequisite: The
openclawuser must be logged in via the GUI (auto-login or Fast User Switching without logging out). Logging out destroys thegui/<uid>domain and stops the service. Without auto-login, the service does not start at reboot — use LaunchDaemon instead.
Create LaunchAgents directory
sudo -u openclaw mkdir -p /Users/openclaw/Library/LaunchAgentsCreate the plist
Verify paths first:
which openclaw
which node
readlink -f $(which openclaw)sudo -u openclaw tee /Users/openclaw/Library/LaunchAgents/ai.openclaw.gateway.plist > /dev/null << 'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ai.openclaw.gateway</string>
<key>Comment</key>
<string>OpenClaw Gateway (Docker isolation)</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>Umask</key>
<integer>63</integer>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/node</string>
<string>/opt/homebrew/lib/node_modules/openclaw/dist/index.js</string>
<string>gateway</string>
<string>--port</string>
<string>18789</string>
</array>
<key>StandardOutPath</key>
<string>/Users/openclaw/.openclaw/logs/gateway.log</string>
<key>StandardErrorPath</key>
<string>/Users/openclaw/.openclaw/logs/gateway.err.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/openclaw</string>
<key>OPENCLAW_HOME</key>
<string>/Users/openclaw</string>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>OPENCLAW_GATEWAY_PORT</key>
<string>18789</string>
<key>OPENCLAW_GATEWAY_TOKEN</key>
<string><!-- See Secrets Management section above --></string>
<key>ANTHROPIC_API_KEY</key>
<string><!-- See Secrets Management section above --></string>
<key>BRAVE_API_KEY</key>
<string><!-- See Secrets Management section above --></string>
<key>GITHUB_TOKEN</key>
<string><!-- See Secrets Management section above --></string>
<key>OPENCLAW_SERVICE_MARKER</key>
<string>openclaw</string>
<key>OPENCLAW_SERVICE_KIND</key>
<string>gateway</string>
</dict>
</dict>
</plist>
PLISTBootstrap and manage the LaunchAgent
Get the openclaw user’s UID (needed for gui/ domain):
OPENCLAW_UID=$(id -u openclaw)The user must be logged in via GUI before running this. If the gui/<uid> domain doesn’t exist yet (no active GUI session), bootstrap fails with error 125.
# Start (requires active GUI session for openclaw user)
sudo launchctl bootstrap gui/$OPENCLAW_UID /Users/openclaw/Library/LaunchAgents/ai.openclaw.gateway.plist
# Stop
sudo launchctl bootout gui/$OPENCLAW_UID/ai.openclaw.gateway
# Restart
sudo launchctl bootout gui/$OPENCLAW_UID/ai.openclaw.gateway
sudo launchctl bootstrap gui/$OPENCLAW_UID /Users/openclaw/Library/LaunchAgents/ai.openclaw.gateway.plist
# Check status
sudo launchctl print gui/$OPENCLAW_UID/ai.openclaw.gateway 2>&1 | head -10
openclaw gateway restartworks in this mode — the LaunchAgent runs in thegui/<uid>domain thatopenclaw gatewaytargets.
Docker Sandboxing
Docker provides an additional isolation layer for agents.
VM isolation: macOS VMs — skip this section (no Docker inside macOS VMs). Linux VMs — follow this section (Docker works inside the VM).
- macOS: OrbStack is recommended over Docker Desktop — lighter, faster, and integrates well with macOS networking.
- Linux: Docker Engine (via
apt/dnf) is all you need — no Docker Desktop required. See Docker Engine install docs .
Warning (bare Linux hosts): Adding a user to the
dockergroup grants effective root access on the host. For bare-metal Linux deployments, consider rootless Docker or Podman as alternatives.
Build the sandbox image first
The default sandbox image (openclaw-sandbox:bookworm-slim) is not pre-built — it must be built locally before enabling sandboxing. Without this, exec tool calls inside the sandbox fail immediately (sh: 1: git: not found):
cd $(npm root -g)/openclaw
./scripts/sandbox-setup.sh
# Verify
docker run --rm openclaw-sandbox:bookworm-slim git --versionFor agents that need additional tools (Node.js, Go, Rust, build tools), build the common image instead:
./scripts/sandbox-common-setup.sh # → openclaw-sandbox-common:bookworm-slimSee Custom Sandbox Images for details on all available images and custom image builds.
{
"agents": {
"defaults": {
"sandbox": {
"mode": "non-main",
"scope": "session",
"workspaceAccess": "none"
}
}
}
}For stronger isolation, use an internal Docker network so agents can only reach the gateway:
# Internal network — no internet access from containers
docker network create --internal openclaw-sandbox
# Gateway joins both internal and external networks
docker network connect openclaw-sandbox gatewayAgents on the internal network can communicate with the gateway but have no route to the internet. This is particularly useful for the search agent — the gateway mediates all external access.
See OpenClaw sandboxing docs for full Docker configuration. For agents that need additional tools beyond the default image, see Custom Sandbox Images .
Sandbox the Main Agent
The recommended configuration sandboxes the main agent with Docker on an egress-allowlisted network. This roots main’s filesystem inside Docker for channel sessions while preserving workspace access, and restricts outbound traffic to pre-approved hosts.
{
"agents": {
"list": [{
"id": "main",
// ... other config ...
"sandbox": {
"mode": "non-main",
"scope": "agent",
"workspaceAccess": "rw",
"docker": { "network": "openclaw-egress" }
}
}]
}
}mode: "non-main" sandboxes all channel sessions (WhatsApp DMs, groups, cron runs) in Docker while leaving the operator’s Control UI / HTTP API session (agent:main:main) unsandboxed on the host. This matches the threat model: channel sessions receive untrusted external input and should be sandboxed; the Control UI is the trusted operator interface and benefits from direct host access (cron management, service restarts, workspace tasks). Sandboxed sessions can no longer read openclaw.json or auth-profiles.json. Workspace data (SOUL.md, memory, workspace files) remains accessible via the mount. Outbound network is restricted to pre-approved hosts via the openclaw-egress Docker network and host-level firewall rules.
Prerequisites:
- Docker network created:
docker network create openclaw-egress - Egress allowlist configured — see
scripts/network-egress/for setup
macOS with Docker Desktop or OrbStack: Egress allowlisting via pf rules does not work — these tools run containers inside a Linux VM where the bridge interface is inaccessible to host-level pf. Options: (1) use a Linux VM deployment with
apply-rules-linux.shinside the VM, (2) use colima with bridged networking, or (3) accept no egress filtering and rely on tool policy as the primary defense.
Trade-off: Sandboxed channel sessions (WhatsApp DMs, groups, cron) run inside Docker — host-native tools (Xcode, Homebrew binaries) are unavailable there. The Control UI session runs on host and has full access. For an even more isolated architecture with a dedicated computer agent, see Hardened Multi-Agent .
Multi-Gateway Options
For running multiple gateway instances — profiles, multi-user separation, or VM variants — see Multi-Gateway Deployments .
VM Isolation
Run OpenClaw inside a VM for kernel-level host isolation. Your host is untouched — no access to personal files, external drives, or other host resources. Two sub-variants: macOS VMs (macOS hosts) and Linux VMs (any host).
VM Isolation: macOS VMs
macOS hosts only.
Run OpenClaw inside a macOS VM. Your host macOS is untouched.
macOS Host (personal use, untouched)
└── VM — "openclaw-vm"
└── openclaw user (standard, non-admin)
└── Gateway (port 18789): main + search (+ optional channel agents)Same multi-agent architecture as Docker isolation (main + search, plus optional channel agents, sessions_send delegation), but with a VM boundary instead of an OS user boundary. No Docker inside the VM (macOS doesn’t support nested virtualization). For adding macOS-native tooling (Xcode, iOS Simulator, macOS apps) via Lume VMs, see Phase 8: Computer Use
.
Two hypervisor options:
| Lume | Parallels | |
|---|---|---|
| Cost | Free | ~$100/yr |
| Interface | CLI-only | GUI + CLI (prlctl) |
| Hypervisor | Apple Virtualization.framework | Own hypervisor |
| macOS VM limit | 2 per host (Apple’s limit) | 2 per host (same limit) |
| Best for | Headless/server deployments | GUI management, advanced snapshots |
Install
Lume:
brew install --cask lumeParallels:
brew install --cask parallelsOr download from parallels.com .
Create the VM
Lume:
lume create openclaw-vm --os macos --ipsw latest \
--cpu 8 --memory 16384 --disk-size 100 --unattendedParallels — create via GUI (File > New > Download macOS, recommended) or CLI:
prlctl create openclaw-vm --ostype macos
prlctl set openclaw-vm --cpus 8 --memsize 16384The CLI creates an empty VM shell — you still need to install macOS (attach an IPSW or use the GUI installer). The GUI workflow handles this automatically.
Resource guidance:
- CPU 8 — adjust based on your machine (leave cores for the host)
- Memory 16GB — minimum 8GB recommended for OpenClaw
- Disk 100GB — Lume uses sparse disks (grow on demand); Parallels: configure in VM settings
Start and connect
Lume:
lume run openclaw-vm --no-display
# Wait for boot, then SSH in:
lume ssh openclaw-vmParallels:
prlctl start openclaw-vm
# SSH in (enable Remote Login in VM's System Settings first):
ssh user@$(prlctl exec openclaw-vm ipconfig getifaddr en0)Inside the VM: install dependencies
Same regardless of hypervisor:
# Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install Node.js
brew install node
# Install Xcode Command Line Tools (for coding agents)
xcode-select --installDedicated user (inside VM)
The default VM user is typically admin with a known password (e.g., Lume creates lume/lume). A compromised agent with exec access could escalate via echo <password> | sudo -S <command>. Create a dedicated standard (non-admin) user to run OpenClaw:
Why not just use the default user? Three reasons: (1) admin users can
sudo— a compromised agent could gain root access to the VM, (2) the dedicated user has no interactive shell sessions — auto-login creates thegui/<uid>domain for the LaunchAgent but RC files (.zshrc) are never sourced, so environment injection is neutralized, (3) a non-default username is less predictable to an attacker enumerating the system.
# Create a standard user (NOT admin — no sudo access)
sudo sysadminctl -addUser openclaw -fullName "OpenClaw" -password "<temp>" \
-home /Users/openclaw -shell /bin/zsh
sudo passwd openclaw
# Create home directory if not auto-created
sudo mkdir -p /Users/openclaw
sudo chown -R openclaw:staff /Users/openclawKeep the default admin user for management. You need an admin user to SSH in, modify plists, restart services, and install updates. The dedicated
openclawuser only runs the gateway. Change the default admin password (sudo passwd lumeor equivalent) — the default is well-known and a compromised agent could use it for local SSH escalation.
Install OpenClaw
If already installed globally (e.g., by the admin user), skip the install and verify access:
sudo -u openclaw openclaw --versionOtherwise:
sudo -u openclaw bash -c 'curl -fsSL https://openclaw.ai/install.sh | bash'
sudo -u openclaw openclaw --version
sudo -u openclaw openclaw doctorDownload and review the script before running, or verify the source URL.
Then either migrate from your personal user or create a fresh config:
# Fresh install (skip if migrating)
sudo -u openclaw openclaw setupRequired config: gateway.mode
The gateway refuses to start unless gateway.mode is set in openclaw.json:
{
"gateway": {
"mode": "local"
}
}Run openclaw doctor to diagnose startup failures.
Log directory
sudo -u openclaw mkdir -p /Users/openclaw/.openclaw/logsThe gateway auto-creates agent, workspace, and session directories on startup. The log directory must exist before the service starts.
File permissions
After the first successful gateway start, set ownership and lock down permissions:
# Set ownership first (critical — setup/copy commands run as root)
sudo chown -R openclaw:staff /Users/openclaw/.openclaw
# Then restrict permissions
sudo chmod 700 /Users/openclaw
sudo chmod 700 /Users/openclaw/.openclaw
sudo chmod 600 /Users/openclaw/.openclaw/openclaw.json
sudo chmod 600 /Users/openclaw/.openclaw/credentials/*.json
sudo chmod 600 /Users/openclaw/.openclaw/agents/*/agent/auth-profiles.json
sudo chmod 600 /Users/openclaw/.openclaw/identity/*.json
sudo chmod -R 600 /Users/openclaw/.openclaw/credentials/whatsapp/default/*
sudo chmod 700 /Users/openclaw/.openclaw/credentials/whatsappThen follow Phase 4
and Phase 5
to configure the multi-agent gateway. Use examples/openclaw.json
as a starting point.
LaunchAgent (Inside VM)
A LaunchAgent in the openclaw user’s gui/<uid> domain. With auto-login configured for the openclaw user, loginwindow creates the gui/<uid> domain automatically at boot — no manual bootstrap required.
Prerequisite: Auto-login must be configured for the
openclawuser (System Settings → Users & Groups → automatically log in asopenclaw). Without auto-login, thegui/<uid>domain doesn’t exist after a reboot and the service doesn’t start.
Create LaunchAgents directory
sudo -u openclaw mkdir -p /Users/openclaw/Library/LaunchAgentsCreate the plist
Verify paths first:
which openclaw
which node
readlink -f $(which openclaw)sudo -u openclaw tee /Users/openclaw/Library/LaunchAgents/ai.openclaw.gateway.plist > /dev/null << 'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ai.openclaw.gateway</string>
<key>Comment</key>
<string>OpenClaw Gateway (macOS VM)</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>Umask</key>
<integer>63</integer>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/node</string>
<string>/opt/homebrew/lib/node_modules/openclaw/dist/index.js</string>
<string>gateway</string>
<string>--port</string>
<string>18789</string>
</array>
<key>StandardOutPath</key>
<string>/Users/openclaw/.openclaw/logs/gateway.log</string>
<key>StandardErrorPath</key>
<string>/Users/openclaw/.openclaw/logs/gateway.err.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/openclaw</string>
<key>OPENCLAW_HOME</key>
<string>/Users/openclaw</string>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>OPENCLAW_GATEWAY_PORT</key>
<string>18789</string>
<key>OPENCLAW_GATEWAY_TOKEN</key>
<string><!-- See Secrets Management section above --></string>
<key>ANTHROPIC_API_KEY</key>
<string><!-- See Secrets Management section above --></string>
<key>BRAVE_API_KEY</key>
<string><!-- See Secrets Management section above --></string>
<key>GITHUB_TOKEN</key>
<string><!-- See Secrets Management section above --></string>
<key>OPENCLAW_SERVICE_MARKER</key>
<string>openclaw</string>
<key>OPENCLAW_SERVICE_KIND</key>
<string>gateway</string>
</dict>
</dict>
</plist>
PLISTBootstrap and manage
OPENCLAW_UID=$(id -u openclaw)
# Start (bootstrap into the user's GUI domain)
sudo launchctl bootstrap gui/$OPENCLAW_UID /Users/openclaw/Library/LaunchAgents/ai.openclaw.gateway.plist
# Stop
sudo launchctl bootout gui/$OPENCLAW_UID/ai.openclaw.gateway
# Restart
sudo launchctl bootout gui/$OPENCLAW_UID/ai.openclaw.gateway
sudo launchctl bootstrap gui/$OPENCLAW_UID /Users/openclaw/Library/LaunchAgents/ai.openclaw.gateway.plist
# Check status
sudo launchctl print gui/$OPENCLAW_UID/ai.openclaw.gateway 2>&1 | head -10
# Check it's listening
sudo lsof -i :18789
# View logs
tail -f /Users/openclaw/.openclaw/logs/gateway.logHardened alternative: LaunchDaemon
For stronger isolation inside the VM, use a LaunchDaemon instead — the openclaw user has no gui/<uid> domain, blocking agent persistence via LaunchAgents, login items, or shell RC files. See the Docker isolation section’s macOS: LaunchDaemon
for the plist format (already includes UserName/GroupName keys; place in /Library/LaunchDaemons/, use system domain for launchctl commands).
Trade-off: The VM boundary is the real isolation layer — the
openclawuser’s writable~/Library/LaunchAgents/is acceptable risk since a compromised VM doesn’t affect the host. Use LaunchDaemon only when you want defense-in-depth within the VM.
VM management
Lume:
lume clone openclaw-vm openclaw-vm-backup # Snapshot
lume stop openclaw-vm # Stop
lume run openclaw-vm --shared-dir ~/shared:~/shared # Share files
lume list # List VMsParallels:
prlctl snapshot openclaw-vm -n "pre-update" # Snapshot
prlctl stop openclaw-vm # Stop
# Share files: configure via GUI (Parallels Tools > Sharing)
prlctl list # List VMsAuto-start after host reboot: Neither hypervisor auto-starts VMs by default. For Lume, create a LaunchDaemon on the host that runs lume run openclaw-vm --no-display. For Parallels, enable Start Automatically in VM settings, or use prlctl start openclaw-vm in a LaunchDaemon.
Config reload
OpenClaw watches openclaw.json for changes automatically — same behavior as Docker isolation. Safe changes (tool policies, agent definitions) are hot-applied; critical changes trigger an in-process restart. Force a reload with sudo kill -USR1 $(pgrep -f "openclaw.*gateway").
Key differences from Docker isolation
- No Docker —
sandboxblocks inopenclaw.jsonhave no effect. Tool policy + SOUL.md provide internal isolation. Theread→exfiltratechain is open within the VM (channel agents can read~/.openclaw/openclaw.json), but only OpenClaw data is at risk. - Standard user — the
openclawuser has no sudo access. Even within the VM, privilege escalation is blocked. - No interactive login — the
openclawuser never logs in interactively. Auto-login creates thegui/<uid>domain for the LaunchAgent automatically at boot, but shell RC files (.zshrc) are never sourced in non-interactive sessions. For defense-in-depth against LaunchAgent persistence within the VM, use the hardened LaunchDaemon alternative. - VM is the outer boundary — your host macOS is untouched. A full compromise of the VM doesn’t affect the host.
For channel separation with two macOS VMs, see Multi-Gateway: VM Variants .
VM Isolation: Linux VMs
Works on macOS and Linux hosts. Combines VM host boundary with Docker sandbox inside.
Run OpenClaw inside a Linux VM with Docker. This gives the strongest combined isolation posture — kernel-level VM boundary from the host, plus Docker sandboxing for internal agent isolation.
Host (macOS or Linux, untouched)
└── Linux VM — "openclaw-vm"
└── openclaw user (no sudo, docker group)
└── Gateway (port 18789): main + search (+ optional channel agents)
├── main (Docker sandbox, egress-allowlisted network)
└── search (unsandboxed — no filesystem/exec tools)Same multi-agent architecture as Docker isolation, but running inside a VM. Docker closes the read→exfiltrate chain; the VM boundary protects the host. No macOS 2-VM limit — run as many Linux VMs as resources allow.
Hypervisor options
| Multipass | UTM | KVM/libvirt | |
|---|---|---|---|
| Host OS | macOS or Linux | macOS only | Linux only |
| Interface | CLI | GUI + CLI | CLI (virsh) |
| Best for | Headless/server (recommended) | macOS users wanting GUI | Linux-native deployments |
| Install | brew install multipass / snap install multipass | brew install --cask utm | apt install qemu-kvm libvirt-daemon-system |
Create the VM
Multipass (recommended):
multipass launch --name openclaw-vm --cpus 4 --memory 4G --disk 40G
multipass shell openclaw-vmKVM/libvirt (Linux hosts):
# Download Ubuntu Server ISO first
curl -LO https://releases.ubuntu.com/24.04/ubuntu-24.04-live-server-amd64.iso
virt-install --name openclaw-vm --os-variant ubuntu24.04 \
--vcpus 4 --memory 4096 --disk size=40 \
--cdrom ubuntu-24.04-live-server-amd64.isoFor headless (no GUI) installs, use autoinstall with a cloud-init seed instead of
--cdrom.
Resource guidance:
- CPU 4 — adjust based on your host (leave cores for host workloads)
- Memory 4GB — sufficient for headless Linux + Node.js + Docker. Increase to 8GB if agents do heavy coding
- Disk 40GB — sparse/thin-provisioned by default on most hypervisors
Inside the VM: install dependencies
# Update packages
sudo apt update && sudo apt upgrade -y
# Install Node.js (LTS via NodeSource)
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
# Install Docker
curl -fsSL https://get.docker.com | sudo sh
# Install OpenClaw
curl -fsSL https://openclaw.ai/install.sh | bash
openclaw --versionFor production, verify GPG signatures per each project’s install docs rather than piping to shell.
Dedicated user (inside Linux VM)
Create a non-sudo user with Docker access:
sudo useradd -m -s /bin/bash openclaw
sudo passwd openclaw
sudo usermod -aG docker openclawWhy docker group? The
openclawuser needs Docker socket access for sandboxing but has no sudo. Note:dockergroup grants root-equivalent access to the Docker daemon — but the VM boundary contains this. Even if an attacker escapes Docker to VM root, they’re still inside the VM, not on your host.
Install OpenClaw as dedicated user
If already installed globally (e.g., by the admin user), skip the install and verify access:
sudo -u openclaw openclaw --versionOtherwise:
sudo -u openclaw bash -c 'curl -fsSL https://openclaw.ai/install.sh | bash'
sudo -u openclaw openclaw --version
sudo -u openclaw openclaw doctor
# Fresh install (skip if migrating)
sudo -u openclaw openclaw setupDownload and review the script before running, or verify the source URL.
Required config: gateway.mode
Same as all other models — the gateway refuses to start without it:
{
"gateway": {
"mode": "local"
}
}Log directory and permissions
sudo -u openclaw mkdir -p /home/openclaw/.openclaw/logs
# After first successful gateway start:
# Set ownership first (critical — setup/copy commands run as root)
sudo chown -R openclaw:openclaw /home/openclaw/.openclaw
# Then restrict permissions
sudo chmod 700 /home/openclaw
sudo chmod 700 /home/openclaw/.openclaw
sudo chmod 600 /home/openclaw/.openclaw/openclaw.json
sudo chmod 600 /home/openclaw/.openclaw/credentials/*.json
sudo chmod 600 /home/openclaw/.openclaw/agents/*/agent/auth-profiles.json
sudo chmod 600 /home/openclaw/.openclaw/identity/*.jsonService: systemd
Use the Linux: systemd
section below — it applies identically inside a Linux VM. The openclaw user is already set up with Docker access.
Key differences from Docker isolation
- VM is the outer boundary — your host is untouched. A full compromise of the VM doesn’t affect the host.
- Docker works inside —
sandboxblocks inopenclaw.jsonwork normally. Both isolation chains (VM + Docker) are active. - No macOS tooling — Xcode, Homebrew-native macOS tools, and Swift aren’t available. Use macOS VMs if agents need these.
- Lighter weight — a headless Ubuntu VM uses 2-4GB RAM vs 8-16GB for a macOS VM.
Firewall and Tailscale
Same configuration as Docker isolation — see macOS Firewall and Tailscale ACLs . Apply inside the Linux VM (UFW/iptables) and optionally install Tailscale inside the VM for remote access.
For multiple Linux VMs, see Multi-Gateway: VM Variants .
Service Management Comparison
| LaunchDaemon (macOS) | LaunchAgent (macOS, auto-login) | systemd (Linux) | |
|---|---|---|---|
| Runs as | Dedicated user (UserName key) | Dedicated user | Dedicated user |
| Starts at | Boot — no GUI session required | Boot (requires GUI auto-login configured) | Boot |
openclaw gateway compat | No (use launchctl instead) | Yes | N/A |
| OrbStack Docker | Via /var/run/docker.sock (OrbStack must run) | Automatic — same user session | N/A |
| Security | Strongest (macOS) | Standard | Strongest (Linux) |
Linux: systemd
Applies to: Docker isolation on Linux hosts and inside Linux VMs (same systemd unit, same user setup).
Prerequisite: Create the secrets file first — see Secrets Management below — before enabling this unit.
Create the service file:
sudo tee /etc/systemd/system/openclaw-gateway.service > /dev/null << 'EOF'
[Unit]
Description=OpenClaw Gateway
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=openclaw
Group=openclaw
ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789
Restart=always
RestartSec=5
# Hardening
NoNewPrivileges=true
PrivateTmp=yes
ProtectSystem=strict
ReadWritePaths=/home/openclaw/.openclaw
Environment=HOME=/home/openclaw
Environment=OPENCLAW_HOME=/home/openclaw
Environment=OPENCLAW_GATEWAY_PORT=18789
EnvironmentFile=/etc/openclaw/secrets.env
StandardOutput=append:/home/openclaw/.openclaw/logs/gateway.log
StandardError=append:/home/openclaw/.openclaw/logs/gateway.err.log
[Install]
WantedBy=multi-user.target
EOFVerify your Node.js path:
which node && readlink -f $(which node). UpdateExecStartif your path differs (e.g., nvm/asdf installs use~/.nvm/or~/.asdf/paths, not/usr/bin/node).
Manage the service:
# Enable + start
sudo systemctl enable --now openclaw-gateway
# Stop
sudo systemctl stop openclaw-gateway
# Restart
sudo systemctl restart openclaw-gateway
# Status
sudo systemctl status openclaw-gateway
# Logs
sudo journalctl -u openclaw-gateway -fVM isolation: secrets via SSH
macOS VMs: Push secrets from the host to the VM’s LaunchAgent plist (or LaunchDaemon if using the hardened alternative). Uses lume ssh — for Parallels, replace with prlctl exec or regular ssh user@<vm-ip>.
Linux VMs: Push secrets to the systemd environment file inside the VM:
# Multipass
multipass exec openclaw-vm -- sudo tee /etc/openclaw/secrets.env > /dev/null << 'EOF'
OPENCLAW_GATEWAY_TOKEN=your-gateway-token
ANTHROPIC_API_KEY=sk-ant-...
BRAVE_API_KEY=BSA...
GITHUB_TOKEN=github_pat_...
EOF
multipass exec openclaw-vm -- sudo chmod 600 /etc/openclaw/secrets.env
multipass exec openclaw-vm -- sudo systemctl restart openclaw-gateway
# Or via SSH (any hypervisor)
ssh user@<vm-ip> 'sudo tee /etc/openclaw/secrets.env > /dev/null' < secrets.env
ssh user@<vm-ip> 'sudo chmod 600 /etc/openclaw/secrets.env && sudo systemctl restart openclaw-gateway'Single VM (LaunchAgent — default):
OC_UID=$(lume ssh openclaw-vm -- id -u openclaw)
PLIST=/Users/openclaw/Library/LaunchAgents/ai.openclaw.gateway.plist
# Set a secret
lume ssh openclaw-vm -- sudo -u openclaw /usr/libexec/PlistBuddy \
-c "Set :EnvironmentVariables:ANTHROPIC_API_KEY sk-ant-..." "$PLIST"
# Restart the service
lume ssh openclaw-vm -- sudo launchctl bootout "gui/$OC_UID/ai.openclaw.gateway" 2>/dev/null
lume ssh openclaw-vm -- sudo launchctl bootstrap "gui/$OC_UID" "$PLIST"
# Lock down the plist
lume ssh openclaw-vm -- chmod 600 "$PLIST"Single VM (LaunchDaemon — hardened alternative):
PLIST=/Library/LaunchDaemons/ai.openclaw.gateway.plist
# Set a secret
lume ssh openclaw-vm -- sudo /usr/libexec/PlistBuddy \
-c "Set :EnvironmentVariables:ANTHROPIC_API_KEY sk-ant-..." "$PLIST"
# Restart the daemon
lume ssh openclaw-vm -- sudo launchctl bootout system/ai.openclaw.gateway 2>/dev/null
lume ssh openclaw-vm -- sudo launchctl bootstrap system "$PLIST"
# Lock down the plist
lume ssh openclaw-vm -- sudo chmod 600 "$PLIST"For multi-VM secrets automation, see Multi-Gateway: VM Variants .
Docker isolation: Single plist
No deploy script needed — one LaunchAgent (or LaunchDaemon) plist holds all secrets. Lock it down:
# LaunchAgent (default)
sudo chmod 600 /Users/openclaw/Library/LaunchAgents/ai.openclaw.gateway.plist
# Or LaunchDaemon (if using the hardened alternative)
sudo chmod 600 /Library/LaunchDaemons/ai.openclaw.gateway.plistLinux: Environment file
The systemd unit references EnvironmentFile instead of inline secrets:
sudo mkdir -p /etc/openclaw
sudo tee /etc/openclaw/secrets.env > /dev/null << 'EOF'
OPENCLAW_GATEWAY_TOKEN=your-gateway-token
ANTHROPIC_API_KEY=sk-ant-...
BRAVE_API_KEY=BSA...
GITHUB_TOKEN=github_pat_...
# channel-guard and content-guard both require OPENROUTER_API_KEY
EOF
sudo chmod 600 /etc/openclaw/secrets.env
sudo chown root:root /etc/openclaw/secrets.envWhat stays in openclaw.json
Channel config (allowFrom, dmPolicy), agent definitions, tool policies, workspace paths — structural config, not secrets. Channel credentials (WhatsApp session, Signal auth) are managed by their plugins in ~/.openclaw/credentials/.
macOS Firewall
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode onAlso disable unneeded sharing services in System Settings > General > Sharing:
- Remote Management
- Screen Sharing (unless used for remote access)
- File Sharing
- AirDrop
Linux (ufw):
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw enableTailscale ACLs
If using Tailscale, configure ACLs to prevent the OpenClaw machine from initiating connections to other devices on your tailnet. This blocks lateral movement if the agent is compromised. Internet traffic (APIs, WhatsApp, Signal) is unaffected — Tailscale ACLs only control tailnet traffic.
Tag the device
Add to your Tailscale ACL config at https://login.tailscale.com/admin/acls :
{
"tagOwners": {
"tag:openclaw": ["autogroup:admin"]
}
}Then tag the machine in the Tailscale admin console under Machines.
ACL rules
{
"grants": [
{
"src": ["autogroup:member"],
"dst": ["*"],
"ip": ["*"]
}
],
"ssh": [
{
"action": "check",
"src": ["autogroup:member"],
"dst": ["autogroup:self"],
"users": ["autogroup:nonroot", "root"]
},
{
"action": "check",
"src": ["autogroup:member"],
"dst": ["tag:openclaw"],
"users": ["autogroup:nonroot", "root"]
}
]
}The key: tag:openclaw has no outbound grant — it can’t reach other tailnet devices. But all your personal devices (autogroup:member) can reach it (including via SSH).
Important: Test SSH/screen sharing still works after applying ACLs. If locked out, use physical access (or out-of-band management) to fix.
If You Need LAN Access
Prefer Tailscale Serve or an SSH tunnel over binding to 0.0.0.0. If LAN binding is unavoidable:
Set auth —
gateway.auth.mode: "token"or"password"(required for non-loopback; gateway enforces this)Firewall to source IPs — restrict port 18789 to specific trusted IPs:
macOS (pf):
# /etc/pf.conf — allow only your admin machine block in on en0 proto tcp to any port 18789 pass in on en0 proto tcp from 192.168.1.100 to any port 18789Linux (ufw):
sudo ufw deny in on eth0 to any port 18789 sudo ufw allow in on eth0 from 192.168.1.100 to any port 18789Never port-forward broadly — don’t expose 18789 on your router
What’s exposed on port 18789: Control UI, WebSocket protocol, HTTP API (
/v1/chat/completions), and all webhook endpoints. Binding to0.0.0.0without a source-IP firewall exposes all of these to every device on your network.
Reverse Proxy Configuration
If terminating TLS with a reverse proxy (Caddy, nginx, Cloudflare Tunnel):
Set
trustedProxiesinopenclaw.json:{ "gateway": { "trustedProxies": ["127.0.0.1"] } }Proxy must OVERWRITE
X-Forwarded-For— not append. Appending allows clients to spoof their IP.Caddy (overwrites by default — no action needed).
nginx:
proxy_set_header X-Forwarded-For $remote_addr; # overwrites, not appendsStrip Tailscale identity headers if
gateway.auth.allowTailscaleis enabled:proxy_set_header Tailscale-User-Login ""; proxy_set_header Tailscale-User-Name "";Forwarding these headers from your proxy allows authentication bypass.
macOS Companion App
If you install the OpenClaw macOS app (Docker isolation — on host), create this marker file before installing to prevent it from starting its own gateway:
sudo -u openclaw touch /Users/openclaw/.openclaw/disable-launchagentThe app will attach to the existing gateway in read-only mode.
Alternative (per-launch): open -a OpenClaw --args --attach-only
VM isolation: The companion app runs on your host macOS. Recommended: use Tailscale Serve inside the VM (
tailscale serve --bg --https 8443 http://127.0.0.1:18789) and connect via--gateway-url https://<tailscale-ip>:8443. Alternative: SSH tunnel (ssh -N -L 18789:127.0.0.1:18789 user@<vm-ip>). Avoid binding the gateway to0.0.0.0— see If You Need LAN Access .
Signal Setup
Signal requires signal-cli (Java-based) linked as a device.
Prerequisite: Signal requires Java 21+. Install via
brew install openjdk@21(macOS) or your distro’s package manager (Linux, e.g.,sudo apt install openjdk-21-jre).
Install
macOS:
brew install signal-cli # requires Java 21Linux (Debian/Ubuntu):
sudo apt-get install -y default-jre
# Check https://github.com/AsamK/signal-cli/releases for latest version
SIGNAL_CLI_VERSION=0.13.12 # Update to latest
curl -L -o signal-cli.tar.gz \
"https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}-Linux.tar.gz"
sudo tar xf signal-cli.tar.gz -C /opt
sudo ln -sf /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli /usr/local/bin/signal-cliLink to Signal account
Run as the openclaw user so credentials are stored in its home:
sudo -u openclaw signal-cli link -n "OpenClaw"This outputs a URI. Generate a QR code to scan:
pip install qrcode pillow
python3 -c "
import qrcode
img = qrcode.make('sgnl://linkdevice?uuid=...&pub_key=...')
img.save('signal-link-qr.png')
"
open signal-link-qr.png # macOS
# xdg-open signal-link-qr.png # LinuxScan with Signal > Settings > Linked Devices > Link New Device. Do this quickly — links expire fast.
Configure Signal channel
Add to openclaw.json:
{
"channels": {
"signal": {
"enabled": true,
"account": "+46XXXXXXXXX",
"cliPath": "signal-cli",
"dmPolicy": "pairing",
"allowFrom": ["+46XXXXXXXXX"],
"groupPolicy": "allowlist",
"groups": {
"*": { "requireMention": false }
},
"mediaMaxMb": 8
}
}
}Signal groups (2026.3.13-1+): The
groupsconfig key is now supported in Signal’s channel schema. Configuregroupsexactly as you would for WhatsApp — use group IDs as keys or"*"for all groups. Note: Signal has no native @mention support, so setrequireMention: falseand usementionPatternsin the agent’sgroupChatconfig instead.
Slow JVM starts:
signal-cliis Java-based and can take 10–30s to start. If you manage the daemon separately, setautoStart: falseand pointhttpUrlat your running instance (e.g.,"httpUrl": "http://127.0.0.1:8080"). For auto-spawned daemons, increasestartupTimeoutMsif you see timeouts.
Approve senders
openclaw pairing list signal
openclaw pairing approve signal <CODE>Note: The primary Signal device receives all messages too. Use a dedicated phone number for the bot, or mute notifications on the primary device.
Verification Checklist
Config validation is strict. OpenClaw rejects unknown keys, malformed types, or invalid values — the gateway refuses to start. If the service starts but exits immediately, run
openclaw doctorto diagnose. Useopenclaw doctor --fixto auto-apply safe repairs.
After deployment, verify everything works:
# Service is running
sudo launchctl print gui/$(id -u openclaw)/ai.openclaw.gateway 2>&1 | head -10 # macOS (LaunchAgent)
sudo launchctl print system/ai.openclaw.gateway 2>&1 | head -10 # macOS (LaunchDaemon, hardened)
sudo systemctl status openclaw-gateway # Linux
# Gateway is listening
sudo lsof -i :18789
# Health check (replace <token> with your OPENCLAW_GATEWAY_TOKEN value)
curl -s -H "Authorization: Bearer <token>" http://127.0.0.1:18789/health
# Recent logs
tail -20 /Users/openclaw/.openclaw/logs/gateway.log # macOS (VM / Docker isolation)
tail -20 /home/openclaw/.openclaw/logs/gateway.log # Linux- Gateway starts at boot (both options)
- Health endpoint responds
- WhatsApp/Signal messages get responses
- Logs are written to the expected location
- File permissions are 600/700 on sensitive files
- Gateway only listens on loopback
- Tailscale ACLs block outbound from openclaw machine (if applicable)
- Security audit passes:
openclaw security audit --deep
Ongoing Management
# Edit config (VM and Docker isolation use the same dedicated user)
sudo -u openclaw vi /Users/openclaw/.openclaw/openclaw.json
# Restart (macOS — LaunchDaemon, Docker isolation)
sudo launchctl bootout system/ai.openclaw.gateway && sleep 1 && \
sudo launchctl bootstrap system /Library/LaunchDaemons/ai.openclaw.gateway.plist
# Restart (macOS — LaunchAgent, VM with auto-login)
OPENCLAW_UID=$(id -u openclaw)
sudo launchctl bootout gui/$OPENCLAW_UID/ai.openclaw.gateway
sudo launchctl bootstrap gui/$OPENCLAW_UID /Users/openclaw/Library/LaunchAgents/ai.openclaw.gateway.plist
# Restart (Linux)
sudo systemctl restart openclaw-gatewayUpdating OpenClaw
# Backup before upgrading
sudo cp -r /Users/openclaw/.openclaw /Users/openclaw/.openclaw.bak
# Update (either method works)
openclaw update # Built-in updater
# or: curl -fsSL https://openclaw.ai/install.sh | bash
# Verify
openclaw --version
openclaw doctor
# Restart the daemon (see commands above)If something breaks, restore the backup. Run openclaw doctor --fix to apply any config migrations needed after the update.
Log Rotation
Gateway logs grow indefinitely. Set up rotation:
macOS — add to /etc/newsyslog.d/openclaw.conf:
/Users/openclaw/.openclaw/logs/gateway.log openclaw:staff 640 7 1024 * J
/Users/openclaw/.openclaw/logs/gateway.err.log openclaw:staff 640 7 1024 * J
640restricts log access to owner and group only. Gateway logs may contain sensitive data.
Linux — add to /etc/logrotate.d/openclaw:
/home/openclaw/.openclaw/logs/*.log {
weekly
rotate 4
compress
missingok
notifempty
}Session Transcript Pruning
Session files (agents/<id>/sessions/*.jsonl) contain full message history including tool output. Prune old sessions periodically:
Test before scheduling. Run the
findcommand without-deletefirst to verify what would be pruned:sudo -u openclaw find /Users/openclaw/.openclaw/agents/*/sessions -name "*.jsonl" -mtime +30
Delete sessions older than 30 days:
sudo -u openclaw find /Users/openclaw/.openclaw/agents/*/sessions -name "*.jsonl" -type f -mtime +30 -deleteSchedule with cron (simplest):
macOS:
# Add to openclaw user's crontab
sudo -u openclaw crontab -e
# Add line (weekly, Sunday at 3am):
0 3 * * 0 find /Users/openclaw/.openclaw/agents/*/sessions -name "*.jsonl" -type f -mtime +30 -deleteLinux:
# Add to openclaw user's crontab
sudo crontab -u openclaw -e
# Add line (weekly, Sunday at 3am):
0 3 * * 0 find /home/openclaw/.openclaw/agents/*/sessions -name "*.jsonl" -type f -mtime +30 -deletemacOS LaunchDaemon alternative (more Mac-native):
<!-- /Library/LaunchDaemons/com.openclaw.session-pruning.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.openclaw.session-pruning</string>
<key>UserName</key>
<string>openclaw</string>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>find /Users/openclaw/.openclaw/agents/*/sessions -name "*.jsonl" -type f -mtime +30 -delete</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key>
<integer>0</integer>
<key>Hour</key>
<integer>3</integer>
</dict>
</dict>
</plist>Load the LaunchDaemon:
sudo launchctl bootstrap system /Library/LaunchDaemons/com.openclaw.session-pruning.plistRetention policy: 30 days balances audit trail (ability to review recent sessions) with storage (transcript files can be large). Adjust
mtimevalue based on your needs.
Deployment Gotchas
Operational issues discovered during real deployments. Most are macOS-specific.
macOS Service User Setup
sysadminctldoesn’t create home directories —sysadminctl -addUserassigns a home path but doesn’t create it. After creating the user:sudo mkdir -p /Users/openclaw && sudo chown openclaw:staff /Users/openclaw- Home dir ownership —
sudo mkdir -pcreates directories owned by root. Alwayschown user:staffexplicitly after. - Admin access to dedicated user files — dedicated user home dirs are
drwx------. Use macOS ACLs for admin read/write access:# Traverse-only on home dir (minimal — just enough to reach .openclaw) sudo chmod +a "youradmin allow list,search,execute" /Users/openclaw # Full read+write with inheritance on .openclaw sudo chmod -R +a "youradmin allow \ read,write,append,delete,add_file,add_subdirectory,delete_child,\ readattr,writeattr,readextattr,writeextattr,readsecurity,\ list,search,execute,\ file_inherit,directory_inherit" \ /Users/openclaw/.openclaw - NOPASSWD sudo — automated setup tools may need
NOPASSWDin/etc/sudoers.d/. Remove immediately after setup:sudo rm /etc/sudoers.d/<file>
Running Commands as Service User
sudo -u preserves the caller’s working directory. Simple commands (--version, --help) typically work, but commands that access the filesystem can fail if the current directory isn’t accessible to the target user:
# Works for simple commands
sudo -u openclaw openclaw --version
# Required for commands that access files or the working directory
sudo -u openclaw bash -c 'cd /Users/openclaw && HOME=/Users/openclaw openclaw doctor'Use the bash -c pattern for interactive setup, openclaw doctor, openclaw setup, or any command that reads/writes files.
Docker/OrbStack
- OrbStack docker CLI not in PATH — OrbStack installs at
/usr/local/bin/docker, which may not be in PATH for dedicated users or non-interactive shells. Use the full path, ensure the engine is running withorbctl start, or symlink:sudo ln -sf /Applications/OrbStack.app/Contents/MacOS/orbctl /usr/local/bin/docker
Playwright
- Per-user install requires correct environment —
npx -y playwright install chromiumas another user needsHOMEandPATHset correctly, and mustcdto the user’s home first. The npm cache must be writable by that user.
Signal
- JAVA_HOME stale after brew upgrade — signal-cli needs Java 21. After brew upgrades,
JAVA_HOMEmay point to a removed version. Set explicitly in plistEnvironmentVariables:JAVA_HOME=/opt/homebrew/Cellar/openjdk@21/<version>/libexec/openjdk.jdk/Contents/Home
Migration Between Hosts
For a complete migration guide covering config, credentials, memory, channels, services, and scheduled tasks, see Phase 7 — Migration .
Emergency Procedures
Immediate Shutdown
# VM: macOS VMs — stop the VM from host (fastest, kills everything)
lume stop openclaw-vm # Lume
prlctl stop openclaw-vm # Parallels
# VM: Linux VMs — stop the VM from host
multipass stop openclaw-vm # Multipass
virsh shutdown openclaw-vm # KVM/libvirt
# VM isolation — inside the VM (graceful)
OPENCLAW_UID=$(id -u openclaw)
sudo launchctl bootout gui/$OPENCLAW_UID/ai.openclaw.gateway # macOS VM (LaunchAgent)
sudo launchctl bootout system/ai.openclaw.gateway # macOS VM (LaunchDaemon, hardened)
sudo systemctl stop openclaw-gateway # Linux VM
# Docker isolation (LaunchAgent on host — default)
OPENCLAW_UID=$(id -u openclaw)
sudo launchctl bootout gui/$OPENCLAW_UID/ai.openclaw.gateway
# Docker isolation (LaunchDaemon on host — hardened)
sudo launchctl bootout system/ai.openclaw.gateway
# Linux (Docker isolation on Linux host)
sudo systemctl stop openclaw-gatewayRemote Shutdown (via Tailscale SSH)
# VM: macOS VMs — stop VM from host
ssh user@<tailscale-ip> 'lume stop openclaw-vm' # Lume
ssh user@<tailscale-ip> 'prlctl stop openclaw-vm' # Parallels
# VM: Linux VMs — stop VM from host
ssh user@<tailscale-ip> 'multipass stop openclaw-vm' # Multipass
ssh user@<tailscale-ip> 'virsh shutdown openclaw-vm' # KVM/libvirt
# Docker isolation (macOS host, LaunchAgent — default)
ssh user@<tailscale-ip> 'OPENCLAW_UID=$(id -u openclaw); sudo launchctl bootout gui/$OPENCLAW_UID/ai.openclaw.gateway'
# Docker isolation (macOS host, LaunchDaemon — hardened)
ssh user@<tailscale-ip> 'sudo launchctl bootout system/ai.openclaw.gateway'
# Docker isolation (Linux host)
ssh user@<tailscale-ip> 'sudo systemctl stop openclaw-gateway'Session Reset
sudo -u openclaw openclaw sessions resetIncident Response
If you suspect compromise, follow this sequence:
- Contain — stop the gateway immediately (see Immediate Shutdown above)
- Rotate credentials:
- Gateway token — rotate in the LaunchAgent plist (macOS, or LaunchDaemon for hardened setup) or
/etc/openclaw/secrets.env(Linux) - API keys — rotate Anthropic, Perplexity/Brave keys in the same plist or env file; also update
auth-profiles.jsonif used - Channel credentials — re-pair WhatsApp (scan new QR) or re-link Signal
- Gateway token — rotate in the LaunchAgent plist (macOS, or LaunchDaemon for hardened setup) or
- Audit — review logs and session transcripts for unauthorized actions:
# Recent gateway logs (macOS: /Users/openclaw, Linux: /home/openclaw) tail -100 ~openclaw/.openclaw/logs/gateway.log # Session transcripts (look for unexpected tool calls) ls -lt ~openclaw/.openclaw/agents/*/sessions/*.jsonl | head -20 - Restart the gateway with rotated credentials
- Report vulnerabilities to security@openclaw.ai
See the official security docs for additional context on known attack patterns.
Next Steps
Your OpenClaw deployment is production-ready.
→ Phase 7 — Migration — moving a deployment to a new machine → Reference — config cheat sheet, tool list, gotchas, emergency procedures
Or review:
- Hardened Multi-Agent — optional: add a dedicated computer agent for exec isolation
- Examples — complete config and security audit