OpenClaw on Raspberry Pi 5
A step-by-step guide to installing and configuring OpenClaw on a Raspberry Pi 5, written for beginners who want to run an autonomous AI agent without compromising their home network.
Why a strict installation?
OpenClaw is powerful β it can execute shell commands, read and write files, browse the web, and interact with external services. That power comes with real risk. A misconfigured agent could be exploited through prompt injection, malicious skills, or misconfiguration, potentially exposing your home network and personal devices.
This guide follows an assume-breach security posture: we design the setup so that even if OpenClaw is compromised, the blast radius is contained to the Raspberry Pi and cannot reach your personal devices, accounts, or data.
OpenClaw on Raspberry Pi 5 β Installation & Security Hardening Guide
Hardware
| Component | Model | Key Specs |
|---|---|---|
| Single-board computer | Raspberry Pi 5 | 16 GB RAM |
| Storage (primary) | Official Raspberry Pi NVMe SSD | 1 TB, M.2 NVMe |
| Case | Argon NEO 5 NVMe | M.2 bottom-mount, passive cooling |
| Power supply | Official Raspberry Pi USB-C PSU | 27 W |
| Storage (recovery) | microSD card | For initial OS flashing / recovery |
| Isolation router | GL.iNET GL-MT300N-V2 (Mango) | Portable VPN router, OpenWrt, 100 Mbps Ethernet |
| Networking | Ethernet cables Γ 2 | Main router β Mango WAN, Mango LAN β Pi |
Phase 0 β Concepts & Context
What this phase covers: A conceptual foundation for running OpenClaw on a network-isolated Raspberry Pi 5. No hardware, no commands β just the mental model you need before building anything.
Prerequisites: None. This is the starting point.
1. What Is OpenClaw?
OpenClaw is an open-source AI agent you self-host on your own hardware. You interact with it through messaging apps (Telegram, Signal, WhatsApp, Discord, etc.) and it responds like a chatbot β but with a critical difference: it can take real actions on the computer it runs on.
Specifically, OpenClaw can:
- Execute shell commands on its host machine
- Read and write files
- Browse websites
- Use downloadable extensions called "skills"
- Send messages on your behalf
- Write code, run it, and iterate on it
2. What Is a Raspberry Pi, and Why Use One?
A Raspberry Pi is a small, inexpensive single-board computer (roughly credit-card-sized) that runs Linux. Your Pi 5 with 16 GB of RAM is a capable machine for this workload.
Why it's ideal for OpenClaw: It is a separate, dedicated device. It is not your laptop or your phone. If something goes wrong with OpenClaw, the damage is contained to this box β not spread across your personal life. Think of it as giving your AI assistant its own apartment rather than letting it move into your house.
3. Key Terminology
These terms will recur throughout every phase of the setup.
| Term | Definition | Analogy |
|---|---|---|
| IP address | A number that identifies a device on a network (e.g., 192.168.1.50). |
A postal address for a computer. |
| Local network | All devices connected to your home router (Wi-Fi and Ethernet). By default, they can see and communicate with each other. | Everyone living in the same house. |
| VLAN (Virtual LAN) | A way to create separate, isolated networks within one router. Devices on one VLAN cannot talk to devices on another. | Two buildings sharing a street address but with no connecting doors. |
| Guest network | A simpler form of network isolation available on many home routers. Provides internet access but blocks communication with the main network. | A guest house on the same property β Wi-Fi works, but no access to the main house. |
| SSH (Secure Shell) | A protocol for remotely controlling a computer via encrypted text commands. You'll use this from your laptop to administer the Pi without plugging in a keyboard and monitor. | A secure phone line to the Pi's command center. |
| Firewall (UFW) | Software that controls which network traffic is allowed in or out of a device. UFW = "Uncomplicated Firewall." | A bouncer at a door who checks every visitor against a list. |
Localhost (127.0.0.1) |
A special IP address meaning "this device only." When OpenClaw binds to localhost, it only accepts connections from the Pi itself β not from the network. | A door that only opens from the inside. |
| Port | A numbered endpoint on a device. A single IP can run many services, each on a different port. OpenClaw's gateway defaults to port 18789. |
If the IP address is a street address, the port is the apartment number. |
| API key | A secret credential that lets OpenClaw communicate with a cloud service (e.g., Anthropic's Claude API). Anyone with this key can use the service and incur charges. | A credit card number for an AI service β must be kept secret and have a spending limit. |
| NVMe SSD vs. SD card | Two storage options for the Pi. The SD card is small and slow; the NVMe SSD (1 TB, in the Argon NEO 5 case) is dramatically faster and more durable. We'll start on SD, then migrate to NVMe. | SD card = flash drive. NVMe = proper hard drive. |
| Prompt injection | An attack where hidden instructions are embedded in content that the AI reads (a webpage, a file, a message). The AI may follow those hidden instructions instead of yours. | Someone slipping a forged note into a stack of your legitimate mail. |
| Docker container | A lightweight, isolated environment running inside your computer. It has its own file system and programs, separate from the host. Containers can be created and destroyed in seconds. | A sealed plastic box on your workbench β tools inside can't reach the drawers. |
| Sandboxing | Running OpenClaw's tool commands inside Docker containers instead of directly on the Pi, so damage from a compromised action stays contained. | Making your assistant do all their work inside the plastic box rather than loose in the workshop. |
4. Why Network Isolation Is Non-Negotiable
The analogy to keep in mind throughout this project:
Your home network is your house. Your laptop, phone, smart TV, and NAS are your family living inside. OpenClaw is a capable but not fully trusted employee. You wouldn't give them a key to the house β you'd set them up in a separate office out back, with its own entrance and no connecting door to the main building.
That "separate office" is what we create through network isolation (VLAN or guest network). The Pi gets internet access (for the Anthropic API and approved websites) but cannot see or communicate with any device on your main network.
Why OpenClaw needs isolation but your phone doesn't
The Pi running OpenClaw has a categorically different risk profile from your personal devices:
| Factor | Your Phone | Pi Running OpenClaw |
|---|---|---|
| Designed to execute instructions from external content? | No | Yes β that is its core function |
| Frequency of adversarial input? | Occasional (phishing email, sketchy link) | Regular β every web browse, every file read, every message processed |
| Can run shell commands based on content it reads? | No β browser and apps are sandboxed by the OS | Yes β that is how it works |
| OS security investment? | Billions of dollars (Apple / Google) | Basic Linux + your manual configuration |
| Built-in app sandboxing? | Yes, by default | Optional β we must enable it |
Your phone might get compromised if something goes very wrong. OpenClaw will encounter adversarial content routinely β and it is designed to act on what it reads. The isolation ensures that even a successful attack is contained.
What your router does β and doesn't do
Your home router blocks inbound threats from the internet (via NAT and a basic firewall). But it does not protect devices on your network from each other. Once a device is connected to your Wi-Fi or Ethernet, the router treats it as trusted. Every device on the same local network can typically discover and communicate with every other device.
This means a compromised Pi on an un-isolated network could: scan for other devices, attempt to access file shares and admin panels, exploit vulnerabilities in smart home devices, or use your network as a launching point.
Network isolation closes this gap. The Pi can reach the internet but cannot reach your laptop, phone, NAS, or anything else on the main network.
SSH access does not break isolation
You will still need to administer the Pi from your laptop via SSH. This does not violate the isolation model. The concern is the Pi reaching your devices (outbound from Pi to main network), not you reaching the Pi. We'll configure a narrow firewall rule that allows your laptop to SSH into the Pi while blocking the Pi from initiating any connections to the main network.
Alternatively, Tailscale (a free encrypted overlay network) can provide SSH access from your laptop to the Pi regardless of network segmentation, while the OpenClaw gateway remains bound to localhost. The OpenClaw docs explicitly support this: "gateway.bind must stay loopback when Serve/Funnel is enabled."
5. The "Assume Breach" Mindset
"Assume breach" means: design your setup so that even if OpenClaw is compromised, the damage is limited and manageable.
This is not pessimism β it is standard practice in security engineering. You are running an AI agent with shell access, web access, and file access. The OpenClaw project publishes its own threat model (a good sign of transparency), and it documents specific, rated attack chains:
Critical and high-severity attack chains from OpenClaw's threat model
Attack Chain 1 β Prompt Injection β Remote Code Execution (Critical) Someone hides malicious instructions in a webpage β OpenClaw fetches and reads the page β the AI follows the hidden instructions β runs commands on the Pi.
- Threat IDs:
T-EXEC-001βT-EXEC-004βT-IMPACT-001 - OpenClaw's own residual risk rating: Critical
Attack Chain 2 β Malicious Skill β Credential Theft (Critical) A malicious skill (extension) is published to ClawHub β user installs it β hidden code harvests API keys or other credentials.
- Threat IDs:
T-PERSIST-001βT-EVADE-001βT-EXFIL-003 - OpenClaw's own residual risk rating: Critical
Attack Chain 3 β Indirect Injection via Fetched Content (High) A website OpenClaw visits for research contains hidden instructions β OpenClaw follows them β data is exfiltrated to an attacker-controlled server.
- Threat IDs:
T-EXEC-002βT-EXFIL-001 - OpenClaw's own residual risk rating: High
These are not hypothetical. They are documented in the project's own THREAT-MODEL-ATLAS.md and mapped to the MITRE ATLAS framework for AI/ML threats.
6. Defense in Depth β The Security Layers
No single layer is perfect. Together, they make a breach very hard to turn into real damage.
| Layer | What It Does | What It Mitigates |
|---|---|---|
| Network isolation (VLAN / guest network) | Pi cannot communicate with personal devices | Compromised Pi scanning/attacking your main network |
Localhost binding (gateway.bind: "loopback") |
OpenClaw only accepts connections from the Pi itself | Attackers on the network connecting to the gateway |
| Firewall (UFW) | Blocks all unexpected incoming connections at the OS level | Port scanning, unauthorized access attempts |
Docker sandboxing (sandbox.mode: "non-main") |
Sub-agent tool commands run in isolated containers; main session runs on host for browser access | Malicious commands accessing the Pi's real file system, API keys, or config |
Tool policy (tools.allow / tools.deny) |
OpenClaw can only use specifically permitted tools | Agent using tools you didn't intend to expose |
| Exec approvals | Destructive or system-altering commands require your confirmation | Agent running dangerous commands without oversight |
DM allowlist (dmPolicy: "allowlist") |
Only your account can message OpenClaw | Strangers sending commands or prompt injections via DM |
| Dedicated accounts | GitHub, etc. accounts created solely for OpenClaw | If a token leaks, it's a throwaway β not your personal account |
| API billing limits | Spending cap configured in the Anthropic Console | Runaway costs if the API key is leaked or the agent loops |
7. Local Models vs. Cloud API
You will use two types of AI models:
Local model (via Ollama on the Pi): A small model (e.g., qwen3:1.7b or gemma3:1b) running directly on the Pi's hardware. Free, private, and fast for simple tasks β but limited in reasoning ability.
Cloud model (via Anthropic API with Claude): A large, powerful model accessed over the internet. Excellent at complex reasoning and coding but costs money per use and requires sending data to Anthropic's servers.
The key security nuance from OpenClaw's documentation: Smaller, less capable models are more susceptible to prompt injection. They have weaker "judgment" about distinguishing legitimate instructions from embedded attacks. Use the strongest available model (Claude) for any work that involves tools β i.e., any task where the AI is actually executing commands, writing files, or browsing. Reserve the local model for lightweight tasks like summarization or orchestration where it has no tool access.
8. Sandboxing in Detail
The problem sandboxing solves
Without sandboxing, every command OpenClaw runs executes directly on the Pi with full access to the Pi's file system, API keys, configuration, and anything else on the machine. If OpenClaw gets tricked by a prompt injection, the malicious command has the same access.
What Docker sandboxing does
With sandboxing enabled, OpenClaw runs tool commands inside a Docker container β an isolated environment with its own file system and no default access to the host's real files or network.
You: "Create a to-do app in React"
β
βΌ
OpenClaw decides to use exec and write tools
β
βΌ
βββββββββββββββββββββββββββββββββββ
β Docker Container (sandbox) β
β β
β β
Can create/edit code files β
β β
Can run npm install β
β β
Can start a dev server β
β β Cannot read Pi's real files β
β β Cannot access API keys β
β β Cannot access the network* β
β β Cannot modify OpenClaw configβ
βββββββββββββββββββββββββββββββββββ
* Sandbox containers default to no network access.
If the command turns out to be malicious, the damage is confined to the disposable container. Delete it and make a new one.
The three-layer security model
OpenClaw uses three stacking controls for tool execution:
| Layer | Config path | What it decides | Example |
|---|---|---|---|
| Tool Policy | tools.allow / tools.deny |
Which tools OpenClaw can use at all | "OpenClaw may use exec and write but NOT browser" |
| Sandbox | agents.defaults.sandbox.mode |
Where permitted tools run (host vs. container) | "When exec runs, do it inside Docker, not on the real Pi" |
| Elevated | tools.elevated |
Escape hatch β specific commands that must run on the host even when sandboxing is on | Rare; used sparingly for commands that genuinely need host access |
These layers are evaluated in order: tool policy first (is it allowed?), then sandbox (where does it run?), then elevated (does it get a host-access exception?).
Sandbox modes
| Mode | Behavior | Recommended for |
|---|---|---|
"off" |
Everything runs directly on the Pi | Not recommended |
"non-main" |
Your primary chat session runs on the host; all other sessions (sub-agents) run sandboxed | Recommended for this setup β the main session needs host access for browser/Chromium, while sub-agents remain fully isolated |
"all" |
Everything runs in containers | Maximum isolation β use if you don't need the browser tool |
Honest caveat from the docs
"This is not a perfect security boundary, but it substantially limits filesystem and process access when the model does something dumb."
Sandboxing is one strong layer. Combined with network isolation, a firewall, tool policy, and exec approvals, it forms part of a defense-in-depth strategy where no single layer needs to be perfect.
9. Architecture Overview
This is what the completed setup will look like:
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β YOUR HOME NETWORK (main) β
β β
β π» Laptop π± Phone πΊ Smart TV π NAS β
β β
β β NO connection to Pi β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ISOLATED NETWORK (Pi's "office") β
β β
β π Raspberry Pi 5 (16 GB RAM, 1 TB NVMe) β
β βββ OpenClaw (gateway bound to localhost only) β
β βββ Chromium (headless browser for web tasks) β
β βββ Ollama (local AI model for light tasks) β
β βββ Docker (sandbox for sub-agent execution) β
β βββ UFW Firewall (deny all incoming) β
β βββ Signal / Telegram (channel to you) β
β β
β π Internet access (outbound only): β
β β Anthropic API (Claude, with billing cap) β
β β Whitelisted research websites β
β β Dedicated GitHub account β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
You interact via messaging app (Signal/Telegram).
You administer via SSH or Tailscale.
Phase 0 Checklist
Before proceeding to Phase 1 (Network Preparation), confirm you understand these concepts:
- OpenClaw is an AI agent that executes real commands, browses the web, and writes code β it needs guardrails
- The Raspberry Pi is a dedicated, isolated host β keeping OpenClaw separate from personal devices
- Network isolation means the Pi cannot communicate with your laptop, phone, NAS, or other home devices
- Your router protects against internet threats but does not isolate devices from each other by default
- "Assume breach" = design so that even a fully compromised Pi cannot damage your personal devices or accounts
- Prompt injection (hidden instructions in content the AI reads) is the highest-rated threat in OpenClaw's threat model
- Smaller local models are for lightweight, tool-free tasks only; Claude handles all tool-enabled work
- Sandboxing runs tool commands in disposable Docker containers instead of directly on the Pi
- Defense in depth: network isolation + localhost binding + firewall + sandbox + tool policy + exec approvals + DM allowlist + dedicated accounts + API billing limits
Phase 1 β Network Preparation
What this phase accomplishes: Creates a network-isolated subnet for the Raspberry Pi so that even if OpenClaw is compromised, the blast radius cannot reach personal devices (laptops, phones, smart home, NAS) on the main network.
Prerequisites
- Completed Phase 0 (Concepts & Context) β you understand why network isolation matters.
- Access to your main router's admin panel.
- A working internet connection.
Hardware Required
| Item | Purpose | Approx. Cost (UK) |
|---|---|---|
| GL.iNet GL-MT300N-V2 "Mango" travel router | Creates an isolated subnet for the Pi. 2.4 GHz Wi-Fi only β adequate for this use case (the Pi connects via Ethernet; you only use the Wi-Fi for occasional laptop management before Tailscale is set up). | ~Β£20β25 |
| 2Γ short Ethernet cables (Cat 5e or Cat 6, 0.5mβ1m) | Physical connections between routers and Pi | ~Β£3β5 each |
Why the GL.iNet Mango? It runs OpenWrt (a well-regarded open-source router OS) out of the box, has a solid default firewall, tiny footprint, and low power draw. Its default firewall blocks all incoming connections from the WAN side.
Steps
1. Physical Network Layout
Connect the hardware in this topology:
[Wall socket] β [Main Router]
|
LAN port (any of ports 1β4)
|
Ethernet Cable A
|
WAN port on GL.iNet Mango
|
LAN port on GL.iNet Mango
|
Ethernet Cable B
|
[Raspberry Pi 5]
How this isolates the Pi: The secondary router performs NAT (Network Address Translation), a one-way boundary. The main router assigns addresses on 192.168.1.x; the GL.iNet assigns addresses on 192.168.8.x. Devices on 192.168.1.x cannot initiate connections to devices on 192.168.8.x. The Pi can reach the internet (outbound), but nothing on the main network can reach inward to the Pi.
Note: Do not power on or connect the Pi yet. Leave Cable B disconnected from the Pi until Phase 2.
2. Power On and Boot the GL.iNet Mango
- Plug the GL.iNet's USB power cable into a USB power source (a phone charger works).
- Plug Cable A from any LAN port on the main router into the WAN port on the GL.iNet.
- Wait approximately 60 seconds for the GL.iNet to boot.
3. Access the GL.iNet Admin Panel
- On your laptop or phone, connect to the GL.iNet's WiFi network. It broadcasts as
GL-MT300N-V2-xxx. The default password isgoodlife(printed on the bottom of the device). - Open a browser and navigate to
http://192.168.8.1. - Set a strong, unique admin password when prompted. Record it securely. This password controls all router settings.
4. Verify Internet Connectivity
In the GL.iNet admin panel dashboard, confirm the router has internet access. It should show a connected status β the GL.iNet automatically obtains an IP from the main router via DHCP.
5. Harden the GL.iNet
While in the admin panel:
5a. Set a Strong WiFi Password
Navigate to Wireless settings. Change the WiFi password to a strong, unique passphrase.
Critical: This password must be different from your main WiFi password. If they're the same, anyone who knows your main WiFi password can also access the Pi's isolated network, defeating the purpose of isolation.
Note: For maximum security, consider disabling the GL.iNet's WiFi entirely after Phase 2 is complete, since the Pi connects via Ethernet and SSH management can be done via Tailscale (Phase 8+).
5b. UPnP β No Action Required
UPnP is not installed by default on the GL.iNet Mango. The luci-app-upnp package would need to be deliberately installed before it could run. You are already in the safe state.
Why this matters: UPnP (Universal Plug and Play) lets devices automatically open ports on the router, the opposite of what we want for isolation.
5c. Leave the Default Firewall Intact
The GL.iNet's default firewall blocks all incoming connections from the WAN side. Do not modify it.
6. Configure Your Laptop's Network Settings
Your laptop will connect to two WiFi networks at different times: the main router (for normal internet) and the GL.iNet (for SSH access to the Pi). Each network requires different TCP/IP settings.
Why this matters: If both networks are configured with the same static IP settings, your laptop will try to use the GL.iNet's subnet addresses (e.g.
192.168.8.x) on the main router's network (192.168.1.x), causing the main WiFi connection to fail entirely.
6a. Main Router WiFi β Use Automatic (DHCP) Settings
These instructions are for macOS. Adapt for your OS.
- Open System Settings β Wi-Fi.
- Find your main network and click Detailsβ¦
- Click TCP/IP in the sidebar.
- Set Configure IPv4 to Using DHCP.
- Click DNS in the sidebar. Remove any manually entered DNS servers (the router will provide them automatically).
- Set Configure IPv6 to Automatically.
- Under Private Wi-Fi Address, set to Rotating or Fixed (not Off).
- Click OK.
6b. GL.iNet WiFi β Use Static IP Settings
- Connect to the GL.iNet WiFi network.
- Open System Settings β Wi-Fi.
- Click Details⦠next to the GL.iNet network.
- Click TCP/IP in the sidebar.
- Set Configure IPv4 to Manually.
- Enter:
- IP Address:
<YOUR_LAPTOP_IP>(e.g.192.168.8.170β must be in the192.168.8.2β254range) - Subnet Mask:
255.255.255.0 - Router:
192.168.8.1
- IP Address:
- Click DNS in the sidebar. Set the DNS server to
192.168.8.1. - Click OK.
Why a static IP? In Phase 2, the Pi's firewall (UFW) will be configured to allow SSH only from a specific IP address. A static IP ensures your laptop always gets the same address on the GL.iNet network, so the firewall rule works reliably.
Important: The Pi's static IP (assigned in Phase 2) must be different from the laptop's. For example, if the laptop is
192.168.8.170, the Pi could be192.168.8.160. Neither can be192.168.8.1(that's the router).
7. Disconnect from GL.iNet, Reconnect to Main WiFi
After configuring both network profiles:
- Disconnect from the GL.iNet WiFi.
- Connect to your main WiFi.
- Open a browser and confirm a website loads (e.g.
google.co.uk). - Verify: go to System Settings β Wi-Fi β Detailsβ¦ β TCP/IP and confirm the IP address starts with
192.168.1.(assigned by your main router via DHCP).
Verification
| Test | Expected Result |
|---|---|
GL.iNet admin panel (http://192.168.8.1) accessible when on GL.iNet WiFi |
Dashboard loads, shows internet connected |
| Main WiFi connects and has internet on laptop | Websites load; IP starts with 192.168.1. |
| Switching between networks works cleanly | Each network uses its own IP settings (DHCP on your main router, static on GL.iNet) |
Network isolation ping test will be performed in Phase 2 once the Pi is running: from the laptop on the main network, a
pingto the Pi's192.168.8.xaddress should fail (time out / no response). This confirms the isolation boundary.
Troubleshooting
Laptop cannot connect to main WiFi after configuring the GL.iNet network
Probable Cause: The same static IP/subnet/gateway values from the GL.iNet were accidentally applied to the main WiFi profile. The laptop tells the main router "my address is 192.168.8.x" which is on the wrong subnet.
Fix: Open System Settings β Wi-Fi β Detailsβ¦ for the main network β TCP/IP β change Configure IPv4 to Using DHCP. Remove any manual DNS entries. If it still won't connect, click Forget This Network, then rejoin fresh.
Intermittent WiFi drops on the GL.iNet network
Likely causes and fixes:
WiFi channel interference: The GL.iNet Mango only supports 2.4 GHz, which is crowded. In the GL.iNet admin panel (Wireless settings), manually set the channel to 1 or 11 β whichever is farthest from the channel your main router's 2.4 GHz band uses. The best non-overlapping 2.4 GHz channels are 1, 6, and 11.
macOS auto-switching: macOS may try to switch to a "better" network. In System Settings β Wi-Fi β Detailsβ¦ for the GL.iNet network, ensure Auto Join is ON, and turn off Limit IP Address Tracking and Low Data Mode.
Mango hardware limitation: The GL.iNet Mango is a tiny travel router with a small antenna. Minor drops are inherent to the hardware and Tailscale will be configured in a later phase to allow SSH from the main network without connecting to the GL.iNet WiFi at all.
Phase 1 Checklist
- GL.iNet Mango (or equivalent secondary router) purchased
- Two short Ethernet cables ready
- Cable A connected: main router LAN port β GL.iNet WAN port
- GL.iNet powered on and has internet (confirmed via admin panel)
- GL.iNet admin password set to a strong, unique value
- GL.iNet WiFi password set to a strong, unique value (different from main WiFi)
- GL.iNet default firewall left intact (no modifications)
- Laptop: main WiFi profile set to DHCP (automatic)
- Laptop: GL.iNet WiFi profile set to static IP (unique, in
192.168.8.xrange) - Laptop connects to main WiFi with internet access
- Cable B ready (not yet connected to Pi β that happens in Phase 2)
Phase 2a β Raspberry Pi OS Installation & Hardening
Install Raspberry Pi OS on a microSD card, migrate to an NVMe SSD for performance and reliability, then harden the OS to minimise the attack surface before installing any application software.
Prerequisites
Before starting this phase, you should have completed:
- Phase 0 β Concepts & Context: You understand what OpenClaw is, why network isolation matters, and the "assume breach" mindset.
- Phase 1 β Network Preparation: An isolated network segment (VLAN, guest network, or separate router) is operational. The Pi will connect to this segment only.
- Hardware assembled: The Raspberry Pi 5 is seated in the Argon NEO 5 NVMe case, the 1 TB NVMe SSD is installed in the M.2 slot on the bottom of the case, and the thermal pad makes contact between the Pi's processor and the aluminium lid. (Search YouTube for "Argon NEO 5 NVMe assembly" for visual walkthroughs.)
Part A β Flash the OS & First Boot
Step 1: Install Raspberry Pi Imager
Download and install Raspberry Pi Imager from raspberrypi.com/software on your laptop/desktop (Windows, macOS, or Linux).
Step 2: Generate an SSH key pair
SSH (Secure Shell) lets you control the Pi remotely. SSH keys are a cryptographic lock-and-key pair that replace passwords β they are essentially impossible to brute-force.
On your laptop/desktop terminal (Terminal on macOS/Linux; PowerShell on Windows 10/11):
ssh-keygen -t ed25519 -C "openclaw-pi"
| Flag | Meaning |
|---|---|
-t ed25519 |
Use the Ed25519 algorithm (modern, fast, very secure) |
-C "openclaw-pi" |
A label so you remember what this key is for |
When prompted:
- File location: Press Enter to accept the default (
~/.ssh/id_ed25519). - Passphrase: Set one β it protects the key file on your laptop.
This creates two files:
| File | Purpose | Share it? |
|---|---|---|
~/.ssh/id_ed25519 |
Private key | NEVER |
~/.ssh/id_ed25519.pub |
Public key | Goes on the Pi |
Copy the public key to your clipboard:
cat ~/.ssh/id_ed25519.pub
Copy the entire output (starts with ssh-ed25519 ...).
Step 3: Flash Raspberry Pi OS Lite (64-bit)
Insert the spare microSD card into your laptop.
Open Raspberry Pi Imager.
Configure:
- Device: Raspberry Pi 5
- Operating System: Raspberry Pi OS Lite (64-bit) β under Raspberry Pi OS (other)
Why Lite? No desktop = less software = smaller attack surface. This Pi is a headless server controlled via SSH.
Why 64-bit? OpenClaw and Node.js require it.
Click the settings/gear icon and configure:
Setting Value Notes Hostname <YOUR_HOSTNAME>(e.g.openclaw-pi)The Pi's network name Enable SSH Yes β Allow public-key authentication only Paste your public key from Step 2 Username <ADMIN_USER>(e.g.clawadmin)Avoid the default piβ attackers try it firstPassword <YOUR_PASSWORD>Strong password; needed for sudocommandsWi-Fi Only if connecting wirelessly to your isolated network If using Ethernet, skip Locale/Timezone <YOUR_TIMEZONE>(e.g.Europe/London)Important for log timestamps Raspberry Pi Connect Off See security note below β οΈ Security Note β Raspberry Pi Connect: Leave this off. It creates an outbound tunnel through Raspberry Pi's cloud servers, bypassing your network isolation and adding a third-party trust dependency. Secure remote access will be configured later via Tailscale.
Naming tip: Choose different values for hostname and admin username. A dedicated
openclawuser (unprivileged) will be created during hardening β if your admin username were alsoopenclaw, you'd have a naming collision.Click Write and wait for flashing to complete.
Step 4: First boot
- Insert the microSD card into the Pi (slot on the underside).
- Connect the Pi to your isolated network via Ethernet (or rely on Wi-Fi from Step 3).
- Plug in the 27 W USB-C power supply. The Pi boots automatically.
- Wait 60β90 seconds for first boot to complete.
Step 5: Connect via SSH
Important: Your laptop must be on the same network as the Pi. If the Pi is on your isolated/guest network and your laptop is on the main network, temporarily connect your laptop to the isolated network.
ssh <ADMIN_USER>@<YOUR_HOSTNAME>.local
Expected: A host authenticity message β type yes. Then a prompt like:
clawadmin@openclaw-pi:~ $
If the hostname doesn't resolve, find the Pi's IP from your router's admin page and connect directly:
ssh <ADMIN_USER>@<RASPBERRY_PI_IP>
Step 6: Verify the OS
uname -m
Expected: aarch64
hostname
Expected: your chosen hostname.
Step 7: Update all packages
sudo apt update && sudo apt full-upgrade -y
| Component | Meaning |
|---|---|
sudo |
Run with administrator privileges |
apt update |
Refresh the list of available updates |
apt full-upgrade -y |
Install all updates without prompting |
Note:
sudomay not prompt for a password β Raspberry Pi OS configures passwordless sudo for the initial user by default. On this single-user, network-isolated Pi where only you have SSH access, this is acceptable. Theopenclawuser created later has no sudo access at all β that is the primary security boundary.
Takes 5β10 minutes. Then reboot:
sudo reboot
Wait ~30 seconds, reconnect:
ssh <ADMIN_USER>@<YOUR_HOSTNAME>.local
Part B β Migrate to the NVMe SSD
Step 8: Verify the Pi sees the NVMe drive
lsblk
You should see mmcblk0 (microSD) and nvme0n1 (NVMe SSD). If nvme0n1 is missing, power off, reseat the SSD, try again.
Step 9: Copy the OS to the NVMe
sudo dd if=/dev/mmcblk0 of=/dev/nvme0n1 bs=4M status=progress
Do not interrupt this or close the terminal. Duration depends on microSD size (a 128 GB card at ~96 MB/s β 22 minutes).
Completion indicator: Your command prompt returns after a records in/out summary.
To verify it's still running (without interrupting): open a second terminal, SSH in, run:
ps aux | grep dd
If SSH drops during copy: Reconnect, run lsblk. If nvme0n1 shows partitions matching the microSD layout, the copy completed. If not, re-run dd β the source was read-only, nothing is lost.
Step 10: Set boot order to NVMe
sudo raspi-config
Navigate to: Advanced Options β Boot Order β NVMe/USB Boot
Step 11: Expand the NVMe partition
Still in raspi-config: Advanced Options β Expand Filesystem, then exit (do not reboot yet).
Now expand manually (required β raspi-config alone may not fully expand the NVMe):
sudo parted /dev/nvme0n1 resizepart 2 100%
Type Yes if prompted.
sudo resize2fs /dev/nvme0n1p2
Note: In some cases,
raspi-config"Expand Filesystem" alone does not expand the NVMe partition. The explicitparted+resize2fscommands above are included for reliability.
Step 12: Reboot and verify
sudo reboot
Reconnect, then verify:
findmnt /
Expected: SOURCE shows /dev/nvme0n1p2.
df -h /
Expected: Size β 938β940 GB.
Step 13: Remove the microSD card
sudo poweroff- Wait for the green LED to stop flickering (steady red = shut down).
- Unplug the power cable.
- Remove the microSD card.
- Plug the power back in.
- Wait ~60 seconds, reconnect via SSH to confirm NVMe-only boot works.
Keep the microSD card β label it (e.g. "OpenClaw Pi Recovery β <DATE>") and store safely. It's your emergency recovery device.
Part C β OS Hardening
OS hardening is covered in Phase 2b. Proceed directly to Phase 2b now.
Verification Checklist
| # | Check | Command | Expected |
|---|---|---|---|
| 1 | Running from NVMe | findmnt / |
Source: /dev/nvme0n1p2 |
| 2 | Full disk available | df -h / |
Size β 939 GB |
| 3 | 64-bit OS | uname -m |
aarch64 |
| 4 | System up to date | sudo apt update && sudo apt full-upgrade |
0 upgraded |
| 5 | MicroSD removed | Physical check | Card labelled and stored |
Troubleshooting
SSH hangs or "cannot resolve host"
Cause: Laptop and Pi are on different networks. mDNS (.local) only works within the same segment.
Fix: Temporarily connect your laptop to the Pi's isolated network. Tailscale (later phase) eliminates this.
SSH drops during dd copy
Cause: SSH idle timeout during long copy, especially on Wi-Fi with packet loss.
Fix: Reconnect, run lsblk. Matching partitions on nvme0n1 = copy completed. Otherwise re-run dd.
NVMe shows original microSD size after migration
Cause: raspi-config Expand Filesystem may not apply to NVMe when booted from microSD.
Fix: sudo parted /dev/nvme0n1 resizepart 2 100% then sudo resize2fs /dev/nvme0n1p2.
Pi won't boot after removing microSD
Cause: Boot order not saved.
Fix: Re-insert microSD, boot, re-run sudo raspi-config β Advanced Options β Boot Order β NVMe/USB Boot.
Phase 2b β Raspberry Pi OS Hardening
Locks down a fresh Raspberry Pi OS Lite (64-bit, Bookworm) installation with eight security hardening steps, preparing it as an isolated host for OpenClaw.
Prerequisites
Before starting this phase, you should have already completed:
- Phase 0 β Concepts & Context: You understand what OpenClaw is, what a Raspberry Pi is, and why network isolation and "assume breach" matter.
- Phase 1 β Network Preparation: The Pi is connected to a network-isolated segment (VLAN, guest network with AP isolation, or dedicated router). It cannot communicate with personal devices on your main network.
- Phase 2 (earlier steps):
- Raspberry Pi OS Lite (64-bit, Bookworm) flashed to a microSD card via Raspberry Pi Imager.
- Headless setup configured during flashing: hostname, SSH enabled with public key authentication, network connection, locale.
- Argon NEO 5 NVMe case assembled with the 1 TB NVMe SSD mounted.
- OS successfully migrated from the microSD to the NVMe SSD.
- Pi boots from the NVMe and responds to ping. The microSD card has been removed.
- You can SSH into the Pi from your laptop.
Step 1 β Update All Packages
Ensures you start from the most secure baseline by installing all available patches.
sudo apt update && sudo apt full-upgrade -y
sudoβ runs the command with administrator privileges.apt updateβ downloads the latest list of available software and security patches (does not install anything).&&β runs the next command only if the first succeeds.apt full-upgrade -yβ installs all available updates.-yauto-confirms.
Expected output: Scrolling text as packages are downloaded and installed. Takes a few minutes. You return to the normal command prompt when complete.
If the system asks you to reboot:
sudo reboot
Wait ~30 seconds, then SSH back in.
Step 2 β Lock Down SSH (Key-Only Access)
Disables password-based login so that only your specific SSH key can authenticate. Prevents brute-force password guessing attacks.
2.1 β Verify your SSH key is in place
cat ~/.ssh/authorized_keys
Expected output: A long string starting with ssh-ed25519 or ssh-rsa followed by a block of characters. This is your public key.
β οΈ Stop if the file is empty or you get "No such file." Your key is not installed. Fix this before proceeding or you will be locked out.
2.2 β Back up the SSH configuration
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
Creates a safety copy of the original config in case something goes wrong.
2.3 β Edit the SSH configuration
sudo nano /etc/ssh/sshd_config
nano is a simple text editor in the terminal. Use Ctrl+W to search for each setting below. Remove the leading # (if present) and set the value as shown:
| Setting | Set to | Why |
|---|---|---|
PasswordAuthentication |
PasswordAuthentication no |
Disables password login. Only SSH keys accepted. |
KbdInteractiveAuthentication |
KbdInteractiveAuthentication no |
Disables a secondary password-based authentication method. |
PermitRootLogin |
PermitRootLogin no |
Blocks direct login as root (the unlimited superuser account). |
Save and exit: Ctrl+X β Y β Enter.
2.4 β Restart the SSH service
sudo systemctl restart ssh
Applies the new configuration. Your current session stays connected.
2.5 β Test from a second terminal
β οΈ Critical: Do NOT close your current SSH session yet. It is your safety net.
Open a new terminal window on your laptop and SSH in:
ssh <ADMIN_USER>@<RASPBERRY_PI_IP>
- If you get in without being asked for a password (you may be prompted for your SSH key passphrase β that's fine and expected; it protects the key file on your laptop) β success.
- If you are asked for a password or see "Permission denied" β something went wrong. Use your still-open first session to troubleshoot.
Step 3 β Install and Configure the Firewall (UFW)
Blocks all incoming network connections except SSH from your specific laptop. Even on the isolated network, defense in depth adds layers.
3.1 β Install UFW
sudo apt install ufw -y
UFW = Uncomplicated Firewall. May already be installed β that's fine.
3.2 β Set default rules
sudo ufw default deny incoming
Blocks all incoming connections by default.
sudo ufw default allow outgoing
Allows the Pi to reach the internet (for updates, API calls, etc.). Safe β controls only connections the Pi initiates.
3.3 β Allow SSH from your laptop only
First, find your laptop's IP address on the isolated network.
macOS:
ipconfig getifaddr en0
(If that returns nothing, try ifconfig | grep "inet " and look for the address on your active network interface.)
Windows (PowerShell):
(Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.InterfaceAlias -notlike '*Loopback*' }).IPAddress
Then, on the Pi:
sudo ufw allow from <YOUR_LAPTOP_IP> to any port 22 proto tcp comment "SSH from my laptop"
allow from <YOUR_LAPTOP_IP>β only this one IP is permitted.to any port 22β SSH's port.proto tcpβ the protocol SSH uses.comment "..."β a human-readable note for future reference.
3.4 β Enable the firewall
sudo ufw enable
It will warn about disrupting SSH connections. Type y and press Enter (your SSH rule is already in place).
3.5 β Verify
sudo ufw status verbose
Expected output:
Status: active
Default: deny (incoming), allow (outgoing), disabled (routed)
To Action From
-- ------ ----
22/tcp ALLOW IN <YOUR_LAPTOP_IP> # SSH from my laptop
3.6 β Safety check
Open a new terminal and SSH in again. Confirm access still works before closing any existing sessions.
Step 4 β Install fail2ban
Monitors login attempts and automatically bans IP addresses that fail too many times. An additional layer on top of the firewall.
4.1 β Install
sudo apt install fail2ban -y
4.2 β Create a local configuration
Never edit the default jail.conf β updates can overwrite it. Create a local override:
sudo nano /etc/fail2ban/jail.local
Paste the following:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
[sshd]
enabled = true
port = ssh
backend = systemd
| Setting | Meaning |
|---|---|
bantime |
Block offending IPs for 1 hour. |
findtime |
Look at the last 10 minutes of login attempts. |
maxretry |
3 failures within findtime triggers a ban. |
backend |
Tells fail2ban where to find logs on modern Pi OS. |
Save and exit: Ctrl+X β Y β Enter.
4.3 β Start fail2ban and enable on boot
sudo systemctl enable fail2ban --now
enable = start on every boot. --now = also start immediately.
4.4 β Verify
sudo fail2ban-client status sshd
Expected output:
Status for the jail: sshd
|- Filter
| |- Currently failed: 0
| `- Total failed: 0
`- Actions
|- Currently banned: 0
`- Total banned: 0
All zeros is expected β nothing bad has happened yet.
Step 5 β Create a Dedicated OpenClaw User
OpenClaw will run as an unprivileged user with no sudo access. This confines any damage (from a bug, prompt injection, or misconfiguration) to OpenClaw's own directory β it cannot modify system settings, read your personal files, or change security configurations.
5.1 β Create the user
sudo adduser --disabled-password openclaw
--disabled-password means no one can log into this account remotely via password. You switch into it from your admin account when needed.
It will prompt for "Full Name" and other optional fields. Press Enter through all of them.
Restrict the home directory permissions:
sudo chmod 750 /home/openclaw
The default home directory permission on Debian is 755 (world-readable). Since this directory will contain .git-credentials and other sensitive files, restricting it to 750 (owner + group only) reduces exposure. With no other users in the openclaw group, this effectively makes it owner-only while still allowing group-based access if needed later.
5.2 β Create the OpenClaw configuration directory
sudo mkdir -p /home/openclaw/.openclaw
5.3 β Set ownership and permissions
sudo chown -R openclaw:openclaw /home/openclaw/.openclaw
sudo chmod 700 /home/openclaw/.openclaw
chown -R openclaw:openclawβ makes theopenclawuser the owner of its own config folder.chmod 700β only the owner can read, write, or access this directory. All other users are blocked.
5.4 β Verify
id openclaw
Expected output (example):
uid=1001(openclaw) gid=1001(openclaw) groups=1001(openclaw)
Confirm there is no sudo or admin in the groups list.
Test switching into the account:
sudo su - openclaw
pwd
Should print /home/openclaw. Type exit to return to your admin account.
Step 6 β Disable Unnecessary Services
Every running service is a potential attack surface. Turn off anything the Pi doesn't need.
6.1 β Disable Bluetooth
sudo systemctl disable bluetooth --now
Not needed for OpenClaw. Removes a wireless attack vector.
6.2 β Disable Avahi (network discovery / mDNS)
sudo systemctl disable avahi-daemon --now
Avahi broadcasts your Pi's presence on the network. Since you know the Pi's IP address, it doesn't need to advertise itself.
β οΈ Important consequence: Disabling Avahi means the
.localhostname (e.g.,openclaw-pi.local) will stop working for SSH. You must use the Pi's IP address directly from this point forward. Setting a static IP on your router (see Step 9 below) is strongly recommended.
6.3 β Disable Triggerhappy
sudo systemctl disable triggerhappy --now
A hotkey/button daemon. Not needed in a headless (no keyboard/monitor) setup.
6.4 β (If using Ethernet) Disable Wi-Fi
If the Pi is connected to the isolated network via an Ethernet cable, disable the Wi-Fi radio entirely:
sudo nmcli radio wifi off
This removes the entire wireless attack surface. Skip this if you are using Wi-Fi to connect to your isolated network.
6.5 β Verify
sudo systemctl is-active bluetooth avahi-daemon triggerhappy
Expected output:
inactive
inactive
inactive
Step 7 β Enable Automatic Security Updates
Configures the Pi to automatically download and install critical security patches daily, so it stays protected without manual intervention.
7.1 β Install unattended-upgrades
sudo apt install unattended-upgrades -y
7.2 β Enable automatic updates
sudo dpkg-reconfigure -plow unattended-upgrades
Select Yes when prompted.
7.3 β Customize the configuration
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
Find and set these three values (use Ctrl+W to search). Remove the leading // if present and set the value as shown:
| Setting | Value | Why |
|---|---|---|
Unattended-Upgrade::Automatic-Reboot |
"true" |
Some patches (e.g., kernel) require a reboot to take effect. |
Unattended-Upgrade::Automatic-Reboot-Time |
"04:00" |
Reboots at 4:00 AM to avoid disrupting daytime use. |
Unattended-Upgrade::Remove-Unused-Dependencies |
"true" |
Cleans up leftover packages after updates. Reduces attack surface. |
Save and exit: Ctrl+X β Y β Enter.
7.4 β Test with a dry run
sudo unattended-upgrades --dry-run --debug
This simulates an update run without installing anything. Expect lots of output. If all packages are already up to date (from Step 1), you will see:
No packages found that can be upgraded unattended and no pending auto-removals
...
upgrade result: True
This is normal. The key indicator is upgrade result: True β the process completed successfully.
7.5 β Verify it's enabled
sudo systemctl is-enabled unattended-upgrades
Expected output: enabled
Step 8 β Verify Swap (zram)
Raspberry Pi OS Bookworm comes with zram swap pre-configured. zram creates compressed swap space in RAM itself, providing a memory overflow safety net without wearing out your SSD.
β οΈ Do NOT install the
zram-toolspackage. It conflicts with the built-in zram configuration on Raspberry Pi OS Bookworm and causes service failures ("Device or resource busy" errors). The built-in configuration is sufficient.
8.1 β Verify zram is active
free -h
Expected output (look at the Swap row):
total used free shared buff/cache available
Mem: 15Gi 565Mi 14Gi 13Mi 467Mi 15Gi
Swap: 2.0Gi 0B 2.0Gi
zramctl
Expected output:
NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
/dev/zram0 zstd 2G 16K 69B 48K 4 [SWAP]
Confirm you see a zram device using zstd compression, mounted as [SWAP]. 2 GB of compressed zram on top of 16 GB of RAM is sufficient for running Ollama and OpenClaw.
Step 9 β (Recommended) Set a Static IP on Your Router
Since Avahi is now disabled (Step 6), you must use the Pi's IP address to SSH in. By default, the router assigns IPs dynamically (via DHCP), so the address could change. A static reservation ensures the Pi always gets the same IP.
9.1 β Get the Pi's MAC address
On the Pi, run:
If connected via Ethernet:
ip link show eth0 | grep ether
If connected via Wi-Fi:
ip link show wlan0 | grep ether
Note the value after link/ether (format: aa:bb:cc:dd:ee:ff). This is the Pi's MAC address β a unique hardware identifier.
9.2 β Create the reservation on your router
The exact steps depend on your router model. The general process is:
- Log into your router's admin panel (typically
http://192.168.8.1for GL.iNet routers β or the gateway IP for your specific router model). - Navigate to LAN settings (on GL.iNet: NETWORK β LAN).
- Find the Address Reservation (or DHCP Reservation) section.
- Add a new entry:
- MAC Address:
<YOUR_MAC_ADDRESS>(from step 9.1) - IP Address:
<RASPBERRY_PI_IP>(the current working IP)
- MAC Address:
- Save/Apply.
9.3 β Apply the reservation
sudo reboot
Wait ~30 seconds, then SSH in using the reserved IP:
ssh <ADMIN_USER>@<RASPBERRY_PI_IP>
Verification Checklist
After completing all steps, confirm:
- All OS packages are up to date (
sudo apt updateshows no upgradable packages) - SSH rejects password login (only key + passphrase accepted)
- Root login via SSH is disabled
- UFW is active, default deny incoming, SSH allowed only from your laptop's IP (
sudo ufw status verbose) - fail2ban is running and monitoring SSH (
sudo fail2ban-client status sshd) -
openclawuser exists with no sudo/admin group membership (id openclaw) -
~/.openclawdirectory is owned byopenclawwith700permissions - Bluetooth, Avahi, and Triggerhappy are inactive (
sudo systemctl is-active bluetooth avahi-daemon triggerhappy) - Automatic security updates are enabled (
sudo systemctl is-enabled unattended-upgrades) - zram swap is active (~2 GB, zstd compression) (
free -handzramctl) - You can SSH in using the Pi's IP address from a fresh terminal session
Troubleshooting
SSH hangs when using .local hostname after reboot
Cause: Avahi (mDNS) was disabled in Step 6. The .local hostname no longer resolves.
Fix: Use the Pi's IP address directly instead of the .local hostname:
ssh <ADMIN_USER>@<RASPBERRY_PI_IP>
Set a static IP reservation on your router (Step 9) so the address never changes.
zram service fails with "Device or resource busy"
Cause: The zram-tools package was installed and conflicts with Pi OS Bookworm's built-in zram configuration.
Fix: Remove zram-tools and rely on the built-in setup:
sudo apt remove zram-tools -y
sudo reboot
After reboot, verify with free -h and zramctl. You should see ~2 GB of swap on a zstd-compressed zram device.
SSH works by IP but not by hostname (even before disabling Avahi)
If mDNS resolution is slow or unreliable after a reboot, the SSH client may time out waiting for the hostname to resolve while ping (which has a longer timeout) eventually succeeds.
Fix: Always use the IP address. Set a static IP reservation so you have a reliable, memorable address.
Locked out of SSH after changing the config
If you still have a session open, use it to restore the backup:
sudo cp /etc/ssh/sshd_config.backup /etc/ssh/sshd_config
sudo systemctl restart ssh
If all sessions are closed, connect a USB keyboard and HDMI monitor (micro-HDMI on the Pi 5) directly to the Pi. You will get a text-based login prompt (no GUI needed). Log in with your username and password, then restore the SSH config as above.
Phase 3 β Ollama Installation & Local Model Setup
Install Ollama on the Raspberry Pi 5 and download small, efficient local models to serve as free, private fallbacks for lightweight tasks. Claude (via the Anthropic API) remains the primary model for all heavy reasoning and tool-enabled work.
Prerequisites
Before starting this phase, confirm that:
- Phase 1 (Network Isolation) is complete β the Pi is on an isolated network segment.
- Phase 2 (OS Installation & Hardening) is complete β Raspberry Pi OS Lite (64-bit) is running on the NVMe SSD, SSH is key-only, UFW is active, and
fail2banis installed. - You can SSH into the Pi from your personal computer.
Steps
1. SSH into the Raspberry Pi
From your personal computer's terminal, connect to the Pi:
ssh <ADMIN_USER>@<RASPBERRY_PI_IP>
Replace <ADMIN_USER> with the admin username you chose during OS flashing (e.g., clawadmin), and <RASPBERRY_PI_IP> with the static IP you reserved in Phase 2b, Step 9.
2. Install Ollama
Run the official Ollama installation script:
curl -fsSL https://ollama.com/install.sh | sh
What this does: Downloads and runs Ollama's installer, which installs the ollama binary to /usr/local/bin, creates a dedicated ollama system user, and sets up a systemd service so Ollama starts automatically on boot.
Expected output: The script prints progress lines ending with:
>>> Enabling and starting ollama service...
>>> Installation complete.
3. Verify the installation
Check that the Ollama binary is available:
ollama --version
Expected output: A version string such as ollama version 0.x.x.
Then confirm the background service is running:
sudo systemctl status ollama
Expected output: You should see active (running) highlighted in green. Press q to exit the status view.
4. Increase the default context window
Ollama defaults to a 4,096-token context window for all models, which is quite small. Increasing it to 8,192 gives models more "working memory" per conversation, which matters for OpenClaw interactions.
Open a systemd override file:
sudo systemctl edit ollama
This opens a text editor. Between the comment markers, add these two lines:
[Service]
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Save and exit the editor (in nano: Ctrl+O, Enter, Ctrl+X).
Then reload the systemd configuration and restart Ollama:
sudo systemctl daemon-reload
sudo systemctl restart ollama
Verify Ollama is still running after the restart:
sudo systemctl status ollama
β οΈ Performance Note: A larger context window uses more RAM per loaded model. On a 16 GB Pi, 8,192 tokens is a safe increase. Going much higher (e.g., 32,768) with an 8B model may cause memory pressure when OpenClaw and Docker are also running.
5. Download local models
Download three models, each serving a different purpose. Run each command and wait for it to complete before starting the next.
Small and fast (recommended starting model):
ollama pull qwen3:1.7b
Download size: ~1β2 GB. This 1.7-billion-parameter model is the best balance of speed and quality for the Pi. Expect ~5β10 tokens/second. Good for quick tasks and fast fallback.
Smarter but slower (for when quality matters more than speed):
ollama pull qwen3:8b
Download size: ~5 GB. Uses ~5β6 GB of RAM when loaded. Expect ~1β3 tokens/second on the Pi 5 β noticeably slower, but higher quality output. Qwen3 8B is recommended at this size class because it has strong tool-calling capability, which is how OpenClaw communicates with models.
Minimal and fastest (for the simplest tasks):
ollama pull gemma3:1b
Download size: <1 GB. Google's Gemma 3 at 1 billion parameters β the smallest practical option with the highest token throughput on the Pi 5.
How Ollama manages memory: Only one model is loaded into RAM at a time. When OpenClaw (or you) request a different model, Ollama unloads the current one and loads the new one. You don't need to worry about all three consuming RAM simultaneously.
6. Verify installed models
ollama list
Expected output: A table listing all three models with their names, sizes, and modification dates:
NAME ID SIZE MODIFIED
gemma3:1b ... ... ...
qwen3:1.7b ... ... ...
qwen3:8b ... ... ...
7. Test a model interactively
Send a simple question to verify a model works end-to-end:
ollama run qwen3:1.7b "What is a Raspberry Pi?"
What to expect:
- A pause of ~5β15 seconds before text begins appearing.
- The response streams word by word.
- Some models display a thinking process first (between
<think>tags), then the actual answer. This is normal. - Quality will be noticeably lower than Claude β this is expected for a model this size.
Press Ctrl+D or type /bye to exit if it drops into interactive mode.
Optionally, test the 8B model to compare speed and quality:
ollama run qwen3:8b "Explain what an API key is in one paragraph."
This will be slower (30β60+ seconds for a full response) but the output quality should be better.
8. Verify the Ollama API endpoint
OpenClaw communicates with Ollama through its local HTTP API. Confirm it responds:
curl http://localhost:11434/api/tags
Expected output: A JSON response listing your installed models. If you see your model names in the output, the API is working correctly.
Security Considerations
These are critical and will be enforced in Phase 4 (OpenClaw configuration):
Small models are more vulnerable to prompt injection. The OpenClaw security documentation explicitly warns that less capable models are more easily tricked by malicious content (e.g., hidden instructions on a web page). Larger, more powerful models like Claude are significantly better at recognizing and resisting these attacks.
Local models must NOT be used for tool-enabled work. Tools β running shell commands, reading/writing files, browsing the web β should only be handled by Claude. In Phase 4, we will configure OpenClaw so that when it falls back to a local model, dangerous tool access is restricted or disabled entirely.
Claude remains the primary model. Local models serve as free, private fallbacks for simple tasks (quick questions, summarization, light orchestration) or as a safety net if the Anthropic API is unavailable. The heavy reasoning, coding, and research work goes to Claude.
How OpenClaw Will Use These Models (Preview)
This configuration happens in Phase 4. Here is a conceptual preview so you understand the architecture:
// Simplified preview β do NOT configure this yet
{
agents: {
defaults: {
model: {
primary: "anthropic/claude-sonnet-4-5", // Always tried first
fallbacks: ["ollama/qwen3:8b", "ollama/gemma3:1b"] // Backup list
}
}
}
}
primaryβ The model OpenClaw tries first for every request (Claude).fallbacksβ A ranked backup list, tried in order only if the primary fails (API outage, billing limit reached, network issue).
OpenClaw does not automatically choose between local and cloud based on task difficulty. It always tries the primary model first. You can manually switch models mid-conversation using the /model slash command (e.g., /model ollama/qwen3:8b) to save on API costs for simple tasks.
Additionally, OpenClaw auto-discovers Ollama models when OLLAMA_API_KEY is set (any value works β Ollama doesn't require a real key). It queries the local API, detects models that report tool-calling support, and makes them available. This will be configured in Phase 4.
Verification Checklist
Before proceeding to Phase 4, confirm each item:
- Ollama is installed (
ollama --versionreturns a version number) - The Ollama service is running (
sudo systemctl status ollamashowsactive (running)) - Context window is increased to 8192 (
sudo systemctl cat ollamashows the override) - Three models are downloaded (
ollama listshowsqwen3:1.7b,qwen3:8b, andgemma3:1b) - At least one model produces a response (
ollama run qwen3:1.7b "test"returns text) - The Ollama API responds (
curl http://localhost:11434/api/tagsreturns JSON with model names)
Troubleshooting
Ollama service fails to start
sudo journalctl -u ollama --no-pager -n 50
Check the last 50 log lines for error messages. Common causes: port 11434 already in use, or insufficient permissions for the ollama user.
Model download stalls or fails
Re-run the ollama pull command β it resumes where it left off. If your internet connection is slow, large models (like the 5 GB qwen3:8b) may take a while. This is normal.
"Out of memory" or Pi becomes unresponsive when running 8B model
The 8B model is close to comfortable limits when OpenClaw and Docker are also running. If this happens:
- Stop the model: press
Ctrl+Cin the terminal. - Consider using
qwen3:1.7bas your primary local fallback instead. - The 8B model will be more practical once OpenClaw is idle (not running Docker sandboxes simultaneously).
curl to the API returns "Connection refused"
Ensure Ollama is running:
sudo systemctl start ollama
Then re-test with curl http://localhost:11434/api/tags.
Context: Local Models vs. Claude (Quick Reference)
| Attribute | Local (Ollama on Pi) | Cloud (Claude via API) |
|---|---|---|
| Cost | Free (runs on your hardware) | Pay-per-use (billing limits set in Anthropic Console) |
| Privacy | Nothing leaves the Pi | Requests sent to Anthropic's servers |
| Speed on Pi 5 | 1β10 tokens/sec depending on model size | Near-instant (limited by network latency) |
| Quality | Basic β suitable for simple tasks | High β complex reasoning, coding, research |
| Context window | ||
| Prompt injection resistance | Low β small models are easily fooled | High β large models resist manipulation better |
| Tool use (shell, files, browser) | Should NOT be enabled | Primary model for all tool-enabled work |
Phase 4 β OpenClaw Installation & Security Hardening
What this phase accomplishes: Install OpenClaw and all its dependencies on the Raspberry Pi, run the onboarding wizard, build the Docker sandbox, and harden the configuration to a security-first baseline. This is the most security-critical phase of the entire project.
Prerequisites
Before starting Phase 4, you should have completed:
- Phase 1 β Network Preparation: The Raspberry Pi is connected to an isolated network segment (e.g., a GL.iNet router creating a separate subnet). Your laptop can SSH into the Pi on this isolated network.
- Phase 2 β OS Installation & Hardening: Raspberry Pi OS Lite (64-bit, Bookworm) is installed and booting from the NVMe SSD. The OS is hardened with UFW, fail2ban, SSH key-only access, and unattended-upgrades. A dedicated non-root user named
openclawexists for running the OpenClaw service. - Phase 3 β Ollama Installed: Ollama is installed and operational (used later for lightweight local model tasks).
- Anthropic API key: You have created an Anthropic API key with a spending limit configured in the Anthropic Console. Store the key in a password manager β do not paste it into any chat or unencrypted file.
- Static IP / DHCP reservation: Your laptop has a stable IP on the isolated network (set via your router's admin panel or manually on the laptop), and your Pi's UFW rules allow SSH from that IP.
Part A β Install System-Wide Dependencies
These steps run as your admin user (the account with sudo privileges β not the openclaw user).
Step 1 β Install Node.js 22
OpenClaw requires Node.js β₯ 22.12.0. Older versions have known security vulnerabilities (CVE-2025-59466, CVE-2026-21636).
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
This downloads a setup script from NodeSource and configures the Pi's package list to include Node.js 22.
sudo apt install -y nodejs
Installs Node.js and npm (the Node Package Manager).
Verify:
node --version
Expected: v22.x.x (where x β₯ 12 for the minor version).
npm --version
Expected: a version number (e.g., 10.x.x or 11.x.x).
Step 2 β Install Git
OpenClaw's npm installation process requires Git.
sudo apt install -y git
Verify:
git --version
Expected: git version 2.x.x.
Step 3 β Install Docker
Docker is required for sandboxing β running OpenClaw's tool execution inside isolated containers.
sudo apt install -y docker.io
Step 4 β Create Docker group and add the openclaw user
The Docker group may not exist automatically on some Debian installations.
sudo groupadd docker
sudo usermod -aG docker openclaw
The first command creates the docker group (harmless if it already exists). The second adds the openclaw user to that group so it can run Docker commands without root.
Note: Group changes do not take effect until the user logs out and back in. We will verify this later when we SSH in as
openclaw.
Step 5 β Enable lingering for the openclaw user
Lingering allows the openclaw user's systemd services to run even when nobody is logged in as that user β essential for an always-on assistant.
sudo loginctl enable-linger openclaw
Verify:
loginctl show-user openclaw --property=Linger
Expected: Linger=yes.
Step 6 β Set up SSH access for the openclaw user
Copy your SSH public key to the openclaw account so you can log in directly via SSH (not via sudo su -, which doesn't provide a full systemd user session).
sudo mkdir -p /home/openclaw/.ssh
sudo cp /home/<ADMIN_USER>/.ssh/authorized_keys /home/openclaw/.ssh/authorized_keys
sudo chown -R openclaw:openclaw /home/openclaw/.ssh
sudo chmod 700 /home/openclaw/.ssh
sudo chmod 600 /home/openclaw/.ssh/authorized_keys
Replace <ADMIN_USER> with your admin account's username.
Part B β Install OpenClaw (as the openclaw user)
From your laptop, open a new SSH session and log in directly as the openclaw user:
ssh openclaw@<RASPBERRY_PI_IP>
β οΈ Important: Always SSH in directly as
openclawrather than usingsudo su - openclaw. Thesudo suapproach does not start a full systemd user session, which causes the gateway daemon installation to fail.
Step 7 β Verify Docker access
groups
Expected: you should see docker in the list. Then:
docker run hello-world
Expected: a message saying "Hello from Docker!" β confirms Docker is working for this user.
Step 8 β Configure npm global path
By default, npm global installs require sudo. This redirects them to a user-local directory instead, which is a security best practice (never run npm as root).
mkdir -p ~/.npm-global
npm config set prefix '~/.npm-global'
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
Step 9 β Install OpenClaw
npm install -g openclaw@latest
This downloads and installs the latest stable OpenClaw release. It may take several minutes on the Pi.
Verify:
openclaw --version
Expected: a version string like 2026.2.x.
Part C β Run the Onboarding Wizard
Step 10 β Launch the wizard
openclaw onboard --install-daemon
The --install-daemon flag tells the wizard to also set up a systemd user service that keeps OpenClaw running in the background.
Step 11 β Wizard prompts
Select the following options at each prompt:
| Prompt | Selection | Why |
|---|---|---|
| Understand this is powerful and risky? | Yes | Acknowledge the risks of running an autonomous AI agent. |
| Onboarding mode | Manual | Gives full control over every setting. |
| What do you want to set up? | Local gateway (this machine) | The gateway runs on this Pi. |
| Workspace directory | Accept the default: /home/openclaw/.openclaw/workspace |
Keeps everything under the dedicated user's home. |
| Model/auth provider | Anthropic | We're using Claude as the primary model. |
| Anthropic auth method | Anthropic API key | Simpler and more reliable on a headless Pi than OAuth. |
| Enter Anthropic API key | Paste your key directly into the terminal | Never paste API keys into chats or unencrypted notes. |
| Default model | Keep current (default: anthropic/claude-opus-4-6) |
The strongest model is most resistant to prompt injection. |
| Gateway port | 18789 (default) | Standard port; safe since we're binding to localhost. |
| Gateway bind | Loopback (127.0.0.1) | Only programs on the Pi itself can reach the gateway. Nothing on the network can connect. |
| Gateway auth | Token | Requires a secret token to connect β extra protection layer. |
| Tailscale exposure | Off | No external exposure. We start locked down. |
| Gateway token | Leave blank, press Enter | Auto-generates a strong random token. |
| Channels | Skip | We'll set this up carefully in Phase 5. |
| Skills | Skip | We'll vet skills individually later (supply chain risk). |
| Hooks | Skip for now | If the wizard requires at least one selection, choose session-memory (a safe, built-in feature that saves conversation context). |
| Bash completion | Yes | Convenience only β lets you Tab-complete OpenClaw commands. |
| Daemon/systemd | Yes | Installs the background service. |
β οΈ API Key Safety: Never paste your API key into a chat interface, email, or shared document. If you accidentally expose a key, revoke it immediately in the Anthropic Console and generate a new one.
Note: The wizard may prompt for additional options in newer versions. If an unrecognized prompt appears, skip it or choose the most restrictive option and verify against https://docs.openclaw.ai/start/wizard
Step 12 β Install the gateway daemon
If the wizard successfully installed the systemd service, skip to Step 13. If it reported that systemd user services were unavailable (this can happen), run:
openclaw gateway install
Expected output:
Installed systemd service: /home/openclaw/.config/systemd/user/openclaw-gateway.service
Verify the generated unit file uses the full absolute path to the openclaw binary:
cat ~/.config/systemd/user/openclaw-gateway.service | grep ExecStart
Expected output should show the full path, e.g.:
ExecStart=/home/openclaw/.npm-global/bin/openclaw gateway --port 18789
If ExecStart shows just openclaw (no path) or a wrong path:
# Find the correct path
which openclaw
# Edit the unit file
nano ~/.config/systemd/user/openclaw-gateway.service
Replace the ExecStart value with the full path from which openclaw, then reload:
systemctl --user daemon-reload
Step 13 β Enable and start the gateway
systemctl --user enable --now openclaw-gateway
enable = start on boot. --now = also start it right now.
Verify:
systemctl --user status openclaw-gateway
Expected: active (running) shown in green.
Part D β Build the Sandbox Docker Image
The sandbox is a Docker container where all of OpenClaw's tool execution runs in isolation. Without it, tools would run directly on the host Pi.
Step 14 β Run the doctor check
openclaw doctor
When prompted to create the OAuth directory (~/.openclaw/credentials), select Yes.
When prompted to build the sandbox image, select Yes. If the build fails with an error about a missing Dockerfile.sandbox or scripts/sandbox-setup.sh, proceed to Step 15.
Why this fails: When OpenClaw is installed via
npm(rather than cloned from Git), thescripts/directory is not included in the package. This is a known gap.
Step 15 β Build the sandbox image manually
Capture the UID of the openclaw user (the container user must match the host user for workspace bind-mount writes to succeed):
# Get the actual UID of the openclaw user
OPENCLAW_UID=$(id -u)
echo "Your openclaw user UID is: $OPENCLAW_UID"
Important β remember this number. Every Docker configuration in this guide uses your
openclawuser's UID to ensure the container user and host user match. Wherever you see<OPENCLAW_UID>in a config file, substitute this number (e.g.,1001). If the UIDs don't match, you will hit "permission denied" errors on bind-mounted files (rclone credentials, workspace files, etc.).
Create the Dockerfile:
cat > /tmp/Dockerfile.sandbox << EOF
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends \
bash coreutils curl git jq ca-certificates gnupg && \
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
useradd -m -s /bin/bash -u ${OPENCLAW_UID} claw
WORKDIR /workspace
USER claw
EOF
Note: This intentionally uses
EOFwithout quotes (not'EOF') so the shell substitutes${OPENCLAW_UID}.
This creates a Debian container with essential tools and Node.js 22 (required for web app prototyping β the primary use case), running as a non-root user (claw) whose UID matches the host openclaw user.
Build the image:
docker build -t openclaw-sandbox:bookworm-slim -f /tmp/Dockerfile.sandbox /tmp
This takes several minutes on the Pi.
Verify:
docker images | grep openclaw-sandbox
Expected: a line showing openclaw-sandbox with the tag bookworm-slim.
Step 16 β Re-run doctor to confirm
openclaw doctor
The sandbox warning should be gone. Expected sections:
- Security: "No channel security warnings detected."
- Skills status: Shows eligible and missing counts (normal).
- Plugins: Shows loaded/disabled/errors (0 errors is good).
Part E β Harden the Configuration
This is the most important part of the entire setup. We'll replace the default configuration with a security-hardened version.
Step 17 β Back up the current config
cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.backup
Step 18 β Retrieve your gateway token
You'll need the auto-generated token from the wizard. In a second SSH session (or note it down before editing):
grep token ~/.openclaw/openclaw.json.backup
Copy the token value β you'll paste it into the hardened config below.
Step 19 β Replace the config with the hardened version
First, note your openclaw user's UID (you'll need it for the config below):
id -u
This will output a number (e.g., 1001). Use this value wherever you see <OPENCLAW_UID> below.
Open the config file in nano:
nano ~/.openclaw/openclaw.json
Delete all existing content and replace with the following. You must replace <YOUR_GATEWAY_TOKEN> with your actual token from Step 18:
{
"messages": {
"ackReactionScope": "group-mentions"
},
"logging": {
"level": "info",
"redactSensitive": "tools"
},
"commands": {
"native": "auto",
"nativeSkills": "auto",
"restart": true,
"ownerDisplay": "raw"
},
"contextPruning": {
"mode": "cache-ttl",
"ttl": "1h"
},
"memorySearch": {
"enabled": false
},
"agents": {
"defaults": {
"maxConcurrent": 4,
"subagents": {
"maxConcurrent": 8
},
"models": {
"aliases": {
"opus": "anthropic/claude-opus-4-6"
},
"cacheRetention": "long"
},
"compaction": {
"mode": "safeguard"
},
"workspace": "/home/openclaw/.openclaw/workspace",
"model": {
"primary": "anthropic/claude-opus-4-6"
},
"heartbeat": {
"every": "0m"
},
"sandbox": {
"mode": "non-main",
"scope": "session",
"workspaceAccess": "rw",
"docker": {
"image": "openclaw-sandbox:bookworm-slim",
"network": "bridge",
"readOnlyRoot": false,
"user": "<OPENCLAW_UID>:<OPENCLAW_UID>",
"setupCommand": "git config --global credential.helper '!f() { echo username=x-access-token; echo password=$GITHUB_TOKEN; }; f'",
"env": {
"GIT_CONFIG_NOSYSTEM": "1",
"GIT_CONFIG_GLOBAL": "/tmp/.gitconfig"
}
},
"browser": {
"enabled": true,
"image": "openclaw-sandbox-browser:bookworm-slim",
"headless": true,
"allowHostControl": true
}
}
}
},
"tools": {
"allow": [
"exec",
"process",
"read",
"write",
"edit",
"apply_patch",
"group:sessions",
"group:memory",
"web_fetch",
"browser"
],
"deny": [
"canvas",
"nodes",
"cron",
"gateway",
"web_search"
],
"sandbox": {
"tools": {
"allow": [
"image",
"subagents",
"browser"
]
}
},
"elevated": {
"enabled": false
}
},
"gateway": {
"mode": "local",
"auth": {
"mode": "token",
"token": "<YOUR_GATEWAY_TOKEN>"
},
"port": 18789,
"bind": "loopback",
"tailscale": {
"mode": "off",
"resetOnExit": false
},
"nodes": {
"denyCommands": [
"camera.snap",
"camera.clip",
"screen.record",
"calendar.add",
"contacts.add",
"reminders.add"
]
}
},
"auth": {
"profiles": {
"anthropic:default": {
"provider": "anthropic",
"mode": "api_key"
}
}
},
"wizard": {
"lastRunAt": "<AUTOGENERATED_TIMESTAMP>",
"lastRunVersion": "<AUTOGENERATED_VERSION>",
"lastRunCommand": "onboard",
"lastRunMode": "local"
},
"meta": {
"lastTouchedVersion": "<AUTOGENERATED_VERSION>",
"lastTouchedAt": "<AUTOGENERATED_TIMESTAMP>"
}
}
Note: The
wizardandmetablocks are auto-generated by OpenClaw to record when the onboarding wizard last ran. Do not modify them β copy them exactly as shown (including the placeholder text). They will be overwritten automatically with real values the next time you run any wizard or configuration command.
Save and exit nano: Ctrl+O, Enter, Ctrl+X.
What each hardening change does
| Setting | Value | Purpose |
|---|---|---|
commands.native |
"auto" |
Enables native slash commands (like /restart, /status) that OpenClaw recognises automatically. |
commands.restart |
true |
Allows the /restart command via the messaging channel. |
contextPruning.mode |
"cache-ttl" |
Prunes cached context after the configured TTL, reducing memory and token usage. |
contextPruning.ttl |
"1h" |
Context older than 1 hour is pruned from the cache. |
memorySearch.enabled |
false |
Disables automatic memory search β memory files are loaded explicitly via workspace context instead. |
logging.redactSensitive |
"tools" |
Strips API keys and sensitive data from tool execution logs. |
agents.defaults.models.aliases |
{"opus": "..."} |
Creates a short alias so you can switch models with /model opus instead of typing the full identifier. |
agents.defaults.models.cacheRetention |
"long" |
Keeps model response caches longer, reducing redundant API calls. |
agents.defaults.model.primary |
"anthropic/claude-opus-4-6" |
Explicitly sets the strongest model. Stronger models are significantly harder to manipulate via prompt injection. |
agents.defaults.heartbeat.every |
"0m" |
Disables periodic heartbeat check-ins. Saves API tokens when idle; OpenClaw only responds when you message it. |
agents.defaults.sandbox.mode |
"non-main" |
Sub-agent and spawned sessions run inside Docker containers. The main session runs on the host, giving it direct access to Chromium and the browser tool. This is a reasonable relaxation β the main session is controlled exclusively by you (via the DM allowlist), while sub-agents (which may process untrusted content) remain fully sandboxed. |
agents.defaults.sandbox.scope |
"session" |
Each conversation gets its own container. One compromised session cannot affect another. |
agents.defaults.sandbox.workspaceAccess |
"rw" |
The sandbox can read/write the workspace folder (required for coding tasks). |
agents.defaults.sandbox.docker.network |
"bridge" |
Allows internet access inside the sandbox (needed for npm install, pip install, etc.). Uses Docker's default isolated network. |
agents.defaults.sandbox.docker.image |
"openclaw-sandbox:bookworm-slim" |
Uses the custom sandbox image built in Step 15, which includes Node.js 22 and runs as a non-root user matching the host openclaw UID. |
agents.defaults.sandbox.docker.setupCommand |
(git credential helper) | Configures a dynamic Git credential helper inside sandbox containers so they can push to GitHub using $GITHUB_TOKEN without storing plaintext credentials in .git-credentials. More robust than the raw token approach. |
agents.defaults.sandbox.docker.env.GIT_CONFIG_NOSYSTEM |
"1" |
Tells Git to skip reading the system-wide /etc/gitconfig. Avoids errors when the system config doesn't exist or isn't readable in the sandbox. |
agents.defaults.sandbox.docker.env.GIT_CONFIG_GLOBAL |
"/tmp/.gitconfig" |
Redirects Git's global config file to a writable path. In sandbox containers, $HOME resolves to / (the filesystem root), which isn't writable β so git config --global (used by the setupCommand) fails with "Permission denied." This redirects it to /tmp/.gitconfig, which is always writable. |
agents.defaults.sandbox.browser.enabled |
true |
Enables the sandbox browser for sub-agent sessions. Uses a dedicated browser image (openclaw-sandbox-browser:bookworm-slim) that runs Chromium inside the container. |
agents.defaults.sandbox.browser.allowHostControl |
true |
Allows sandbox sessions to request browser actions on the host when the sandbox browser is unavailable. |
tools.allow |
(explicit list) | Allowlist of permitted tools: shell commands, file I/O, sessions, memory, web fetch, and browser. Only these tools are available β everything else is blocked. Note: web_search is not included (requires Brave API key β see optional section in Phase 6). |
tools.deny |
(explicit list) | Blocklist as a second layer: canvas, node commands, cron, gateway control, and web search are hard-blocked. |
tools.sandbox.tools |
(allow list) | Separate tool allowlist for sandbox sessions β includes image, subagents, and browser. |
tools.elevated.enabled |
false |
Disables "elevated mode" β a feature that lets tool execution escape the sandbox. We never want this. |
Step 20 β Disable Bonjour / mDNS discovery
mDNS broadcasts the Pi's presence on the network. We don't need this.
echo 'OPENCLAW_DISABLE_BONJOUR=1' >> ~/.openclaw/.env
Creates an environment file that OpenClaw reads on startup. This stops the gateway from advertising itself via mDNS.
chmod 600 ~/.openclaw/.env
This file will later hold API keys (Brave Search, GitHub). Setting
600ensures only theopenclawuser can read it β matching the protection onopenclaw.json.
Step 21 β Install Chromium on the host
The browser tool requires Chromium to be installed on the host. Switch to your admin user session:
ssh <ADMIN_USER>@<RASPBERRY_PI_IP>
Install Chromium:
sudo apt install -y chromium
Verify installation:
chromium --version
Expected: Something like Chromium 146.x.x.x.
Return to your openclaw user session for the remaining steps.
Step 22 β Enable and configure the browser tool
The browser tool gives OpenClaw full Chromium browser control for research
tasks β visiting websites, reading JavaScript-rendered pages, and extracting
content that web_fetch (plain HTTP) cannot handle.
Enable the browser and configure it for headless operation on the Pi:
openclaw config set browser.enabled true --json
openclaw config set browser.headless true --json
openclaw config set browser.noSandbox true --json
Note:
noSandbox: truerefers to Chromium's own internal sandboxing, not OpenClaw's Docker sandbox. On a headless Raspberry Pi without a display server, Chromium's sandbox requires additional kernel features that are not available. Disabling it is standard practice for headless Chromium on servers and embedded devices. OpenClaw's Docker sandbox (which isolates sub-agent sessions) is a separate, independent security layer.
β οΈ Security Note β The browser runs on the host, not in the sandbox. Unlike
exec,read, andwrite(which run inside Docker containers innon-mainmode), the browser tool launches a Chromium process directly on the Pi. This is whysandbox.modeis set to"non-main"β the main session needs host access to control the browser.This makes the browser the highest-risk tool in your setup. It is the primary vector for the Indirect Injection via Fetched Content attack chain (rated High in OpenClaw's threat model): a website OpenClaw visits for research could contain hidden instructions β OpenClaw follows them β data is exfiltrated.
Mitigations already in place:
- Network isolation (the Pi is on a separate subnet β even if data is exfiltrated, it cannot reach your personal devices)
- DM allowlist (only you can instruct OpenClaw to browse)
- Claude as the primary model (strongest prompt injection resistance)
- Sub-agents remain fully sandboxed in Docker containers
- Sandbox browser (
openclaw-sandbox-browser:bookworm-slim) runs Chromium inside the container for sandboxed sessionsIf you later decide you don't need JavaScript rendering for research: Run
openclaw config set browser.enabled false --jsonand remove"browser"fromtools.allow. OpenClaw will still haveweb_fetch(plain HTTP + readability extraction). If you also have Brave Search configured (optional),web_searchwill remain available.
How OpenClaw's three web tools differ
| Tool | What it does | API key needed? | JavaScript? | Runs where | Risk level |
|---|---|---|---|---|---|
web_fetch |
HTTP GET on a URL. Extracts readable text (HTML β markdown). | No β free, built-in | No | Gateway process | Medium β reads page content, but no JS execution |
browser |
Full Chromium automation. Can click, scroll, fill forms, execute JS, take screenshots. | No β uses local Chromium | Yes | Host (main session) or sandbox (sub-agents) | Higher β interacts with live web pages that may contain adversarial content |
web_search |
Searches the web via Brave Search API. Returns titles, URLs, snippets. | Yes β Brave API key | No | Gateway process | Low β only returns search metadata |
For most research tasks (literature reviews, reading articles, checking
competitor pages), browser + web_fetch cover all web access needs
without any API keys. The browser tool handles JavaScript-heavy sites
(single-page apps, dynamic dashboards, pages behind cookie consent walls),
while web_fetch is faster for simple page reads.
web_search (Brave Search API) adds structured search-engine-style results
but requires an API key and is optional. See Phase 6 Part 2 for setup
instructions if you want it.
Step 23 β Lock down file permissions
chmod 700 ~/.openclaw
chmod 700 ~/.openclaw/credentials
chmod 600 ~/.openclaw/openclaw.json
| Permission | Meaning |
|---|---|
700 on directories |
Only the openclaw user can read, write, or list contents. No other user on the Pi can access them. |
600 on openclaw.json |
Only the openclaw user can read or write the config file. This file contains your API key and gateway token. |
Verify:
ls -la ~/.openclaw/openclaw.json
Expected: -rw------- at the start of the line.
Step 24 β Restart the gateway to apply all changes
systemctl --user restart openclaw-gateway
Part F β Verify the Hardened Setup
Step 25 β Run the security audit
openclaw security audit --deep
Expected output:
Summary: 0 critical Β· 1 warn Β· 1 info
The remaining items should be:
- WARN β
gateway.trusted_proxies_missing: Safe to ignore. This only applies if you put a reverse proxy in front of OpenClaw. We are not doing that. - INFO β
summary.attack_surface: Should show:groups: open=0, allowlist=0tools.elevated: disabledhooks: disabledbrowser control: disabled
Step 26 β Run the doctor
openclaw doctor
Expected: clean output with no critical or sandbox warnings.
Step 27 β Check gateway status
openclaw status
Confirm:
- Gateway: running, bound to loopback
- Auth: token set
- Systemd: installed, enabled, running
Verification Checklist
Before moving to Phase 5, confirm every item:
-
node --versionshows v22.12.0 or higher -
git --versionreturns a version -
openclaw --versionreturns a version -
docker run hello-worldworks (as theopenclawuser) - Logged in via direct SSH as the
openclawuser (not root, not admin) - Onboarding wizard completed with manual mode
- Systemd daemon installed, enabled, and running (
active (running)) - Sandbox Docker image built (
docker images | grep openclaw-sandbox) - Gateway bound to loopback (127.0.0.1)
- Gateway auth token set (auto-generated)
- Tailscale exposure is off
- Sandbox mode is
"non-main"β sub-agent tools run in Docker; main session runs on host for browser access - Tools restricted to an explicit allowlist
- Browser control disabled
- Elevated mode disabled
- Sensitive data redacted in logs
- Bonjour/mDNS broadcasting disabled
- File permissions:
~/.openclaw/at 700,openclaw.jsonat 600,credentials/at 700 - Heartbeat disabled (0m) to prevent idle API usage
- Security audit: 0 critical issues
-
openclaw doctor: clean
Troubleshooting
These are issues likely to affect others following this guide.
npm install -g openclaw@latest fails with ENOENT ... spawn git
Cause: Git is not installed. OpenClaw's install process requires it.
Fix: sudo apt install -y git (as your admin user), then retry the npm install.
npm install -g openclaw@latest fails with EACCES
Cause: npm's default global install directory requires root.
Fix: Configure a user-local global directory (see Step 8 above), then retry.
docker run hello-world gives "permission denied"
Cause: The openclaw user is not in the docker group, or the group doesn't exist yet.
Fix (as admin user):
sudo groupadd docker # Create the group if it doesn't exist
sudo usermod -aG docker openclaw
Then log out and SSH back in as openclaw. Verify with groups β docker should be listed.
openclaw gateway install fails with "systemctl --user unavailable" / DBUS error
Cause: You switched to the openclaw user via sudo su - instead of logging in directly via SSH. The sudo su method doesn't start a full systemd user session.
Fix: Exit back to your admin user, then SSH in directly as openclaw:
ssh openclaw@<RASPBERRY_PI_IP>
Then retry openclaw gateway install.
Sandbox image build fails with "Dockerfile.sandbox: no such file or directory"
Cause: The scripts/sandbox-setup.sh script expects files that are only present in a git clone of the OpenClaw repository. When installed via npm, these files are not included.
Fix: Build the image manually using the Dockerfile provided in Step 15 above.
SSH to the Pi times out after network changes
Cause: Your laptop's IP on the isolated network changed, and the Pi's UFW firewall only allows SSH from the previously configured IP.
Fix: Connect a keyboard and monitor to the Pi, log in, and update the UFW rule:
sudo ufw status numbered # Find the old SSH rule number
sudo ufw delete <RULE_NUMBER> # Remove the old rule
sudo ufw allow from <YOUR_NEW_IP> to any port 22 proto tcp comment "Laptop on isolated network"
Prevention: Set a static IP for your laptop (either via the router's DHCP reservation or manually in your laptop's network settings for this specific network).
Security Notes
What the sandbox does and does not protect
The Docker sandbox provides meaningful isolation for tool execution (shell commands, file reads/writes, code execution). If a prompt injection tricks OpenClaw into running something malicious, the damage is contained within the disposable container.
However, per the OpenClaw Sandboxing docs: "This is not a perfect security boundary, but when the model does something dumb, it materially limits filesystem and process access."
The sandbox does not isolate:
- The gateway process itself (runs on the host)
- Tools explicitly configured to run on the host (we disabled
tools.elevatedto prevent this) - Network traffic from the container (set to
bridgefor package downloads β the container can reach the internet)
Why sandbox.docker.network is set to "bridge" and not "none"
The default sandbox network is "none" (no network access), which is more secure. However, coding tasks frequently require downloading dependencies (npm install, pip install, etc.). Setting "bridge" allows outbound internet access from within the container.
β οΈ Security Note: This means a compromised sandbox could make outbound network requests. For maximum isolation, change this to
"none"and pre-install dependencies in a custom Docker image. For a coding assistant use case,"bridge"is a practical trade-off.
Why elevated mode is disabled
The OpenClaw docs describe tools.elevated as "an explicit escape hatch that runs exec on the host." With elevated mode enabled, tool execution can bypass the sandbox entirely. We disable it because our entire security model depends on the sandbox containing any potentially harmful actions.
Guide version: Phase 4, based on OpenClaw 2026.2.12. Always verify against the official OpenClaw docs β the project is under active development and configuration options may change.
Phase 5 β Telegram Channel Setup & Local Model Configuration
This phase connects OpenClaw to Telegram so you can interact with your agent from your phone, and configures local Ollama models as fallbacks alongside the Anthropic Claude API. By the end, you will have a fully working, security-hardened messaging channel with strict allowlisting, plus local models registered for lightweight tasks.
Prerequisites
Before starting this phase, confirm the following are complete:
- Phase 1β2: Raspberry Pi 5 running Raspberry Pi OS Lite (64-bit) on NVMe, hardened (UFW, fail2ban, SSH key-only, unattended-upgrades), on an isolated network segment.
- Phase 3: Ollama installed and running as a system service with models pulled (
ollama listshows your models). - Phase 4: OpenClaw installed under a dedicated
openclawuser (no sudo privileges), Docker working, onboarding wizard completed, gateway bound to loopback, sandbox mode"non-main", tool allowlist configured, Chromium installed, security audit clean. - Systemd service: The OpenClaw gateway is running as a user-level systemd service named
openclaw-gatewayunder theopenclawuser.
Key service commands used throughout this guide
# Stop the gateway
systemctl --user stop openclaw-gateway
# Start the gateway
systemctl --user start openclaw-gateway
# Restart the gateway
systemctl --user restart openclaw-gateway
# Check status
systemctl --user status openclaw-gateway
β οΈ Important: Do NOT use
sudowith these commands. Theopenclawuser does not have sudo privileges by design (least-privilege principle). The--userflag manages user-level services, which requires no elevated permissions.
β οΈ Important: The service is named
openclaw-gateway, notopenclaw. If you getUnit openclaw.service could not be found, verify the correct name with:systemctl --user list-units --type=service --all | grep -i claw
Part A β Install Telegram & Create Your Account
Step 1: Install the Telegram app
Download the official Telegram app on your phone:
- iPhone: App Store β search "Telegram" β publisher must be Telegram FZ-LLC
- Android: Google Play Store β search "Telegram" β publisher must be Telegram FZ-LLC
Verify the publisher name to avoid counterfeit apps.
Step 2: Create your account
Open Telegram and follow the signup process. It will ask for your phone number and send an SMS verification code.
Step 3: Enable two-factor authentication (2FA)
Navigate to Settings β Privacy and Security β Two-Step Verification and set a strong password.
This protects your Telegram account from being hijacked β which matters because your Telegram account will be the only one authorized to control OpenClaw.
Part B β Create a Telegram Bot via BotFather
Step 4: Open BotFather
In Telegram, tap the search bar and search for @BotFather. Look for the one with a blue verified checkmark β. The username must be exactly @BotFather β be careful of imposters.
Tap Start (or type /start).
Step 5: Create the bot
Type:
/newbot
BotFather will ask for:
- Display name β the name users see in chat. Example:
OpenClaw Assistant - Username β must end in
botand be unique across Telegram. Example:myopenclaw_bot
If the username is taken, try adding numbers (e.g., myopenclaw2026_bot).
Step 6: Save the bot token
BotFather will reply with your bot token. It looks like:
7123456789:AAF1xR-some-long-string-here
Copy this token immediately and save it in a password manager. This token is a master key to your bot β anyone who has it can impersonate the bot and read its messages. Never share it, never commit it to version control.
Step 7: Disable group joining
Type:
/setjoingroups
Select your bot, then choose Disable. This prevents anyone from adding your bot to group chats, reducing your attack surface.
Part C β Configure OpenClaw for Telegram
Step 8: Stop the gateway
SSH into your Pi as the openclaw user and stop the gateway so it doesn't pick up a half-written config:
systemctl --user stop openclaw-gateway
Step 9: Edit the OpenClaw configuration
nano ~/.openclaw/openclaw.json
You need to add two things: a channels section and enable the Telegram plugin.
9a: Add the channels.telegram block
Add a channels section at the top level of the JSON (e.g., after the gateway block). Place it at any top-level position with correct comma separation:
"channels": {
"telegram": {
"enabled": true,
"botToken": "<YOUR_BOT_TOKEN>",
"dmPolicy": "allowlist",
"allowFrom": [],
"groupPolicy": "disabled",
"streaming": "partial"
}
}
Replace <YOUR_BOT_TOKEN> with the actual token from BotFather.
Configuration explained:
| Key | Value | Purpose |
|---|---|---|
enabled |
true |
Turns on the Telegram channel. |
botToken |
"<YOUR_BOT_TOKEN>" |
Authenticates OpenClaw as your bot. |
dmPolicy |
"allowlist" |
Only user IDs listed in allowFrom can message the bot. Everyone else is silently ignored. This is stricter than the default "pairing" mode. |
allowFrom |
[] |
Empty for now β we add your user ID in Step 13. |
groupPolicy |
"disabled" |
Completely disables group chat. Defense-in-depth alongside the BotFather group-join disable. |
streaming |
"partial" |
Enables partial message streaming β OpenClaw sends incremental updates as it generates a response, so you see progress in real time instead of waiting for the full reply. |
9b: Enable the Telegram plugin
Telegram is delivered as a bundled plugin in OpenClaw. If you skipped channel setup during the onboarding wizard, the plugin is disabled by default. Look for or add a plugins section at the top level:
"plugins": {
"entries": {
"telegram": {
"enabled": true
}
}
}
If a plugins.entries.telegram block already exists with "enabled": false, change it to true.
β οΈ Critical: If you skip this step, the gateway will start but Telegram will never connect. The channels config alone is not enough β the plugin that powers the Telegram connection must also be enabled.
Step 10: Validate the JSON and restart
Save the file (Ctrl+O, Enter, Ctrl+X in nano), then validate:
cat ~/.openclaw/openclaw.json | python3 -m json.tool > /dev/null
- No output = valid JSON
- Error with line number = fix the syntax error (usually a missing comma or mismatched brace)
Start the gateway:
systemctl --user start openclaw-gateway
Step 11: Verify Telegram is connected
openclaw channels status
Expected output should include a line like:
- Telegram default: enabled, configured, running, mode:polling, token:config
If Telegram does not appear at all, check:
- The
plugins.entries.telegram.enabledistrue(see Step 9b) - The JSON is valid (re-run the validation command)
- The bot token is correctly pasted (no extra spaces or missing characters)
Step 12: Retrieve your Telegram user ID
Your user ID is a numeric identifier (e.g., 987654321) that uniquely identifies your Telegram account. You need it for the allowlist.
The gateway is running in polling mode β it continuously fetches new messages from Telegram and consumes them. This means if you send a message while the gateway is running, the gateway grabs it before any other tool can see it. To retrieve your user ID, you must stop the gateway first.
Stop the gateway:
systemctl --user stop openclaw-gateway
Send any message to your bot on Telegram (e.g., "test"). The message will show a single checkmark (delivered to Telegram's servers but not yet read by the bot).
On the Pi, query Telegram's API directly:
curl -s "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates" | python3 -m json.tool | grep -A2 '"from"'
Replace <YOUR_BOT_TOKEN> with your actual token (keep the word bot prefix β e.g., bot7123456789:AAF1xR-...).
Expected output:
"from": {
"id": 987654321,
"is_bot": false,
The id value is your Telegram user ID. Write it down.
Immediately clear your terminal history (the command above contains your bot token):
history -c
Step 13: Add your user ID to the allowlist
Edit the config:
nano ~/.openclaw/openclaw.json
Find the allowFrom line and add your numeric user ID as a string:
"allowFrom": ["<YOUR_TELEGRAM_USER_ID>"]
For example: "allowFrom": ["987654321"]
Save and validate:
cat ~/.openclaw/openclaw.json | python3 -m json.tool > /dev/null
Step 14: Start the gateway and test
systemctl --user start openclaw-gateway
Send a message to your bot on Telegram. OpenClaw should now process and reply to your message.
If this is the first time OpenClaw has received a message, it will run its bootstrap ritual β an introductory conversation where it asks your name and how you'd like to use it. This is normal.
Step 15: Run security checks
openclaw security audit --deep
Expected: 0 critical issues. You may see one warning:
WARN gateway.trusted_proxies_missing Reverse proxy headers are not trusted
This warning is safe to ignore β it applies only if you were using a reverse proxy in front of the gateway, which you are not.
openclaw doctor
Expected output should include: Telegram: ok (@<your_bot_username>)
Part D β Configure Ollama Local Models (Explicit Provider)
This section registers your local Ollama models in OpenClaw using explicit configuration. On the Raspberry Pi 5, Ollama's auto-discovery feature may time out during the /api/show introspection calls, so explicit configuration is more reliable and gives you precise control over which models are available.
Step 16: Verify Ollama is running
First, confirm the Ollama system service is active:
systemctl status ollama --no-pager
Expected: active (running) shown in the output. If it shows inactive or failed, switch to your admin user session to restart it:
# From a separate terminal, SSH in as your admin user: ssh <ADMIN_USER>@<RASPBERRY_PI_IP> sudo systemctl restart ollamaThen return to your
openclawuser session for the remaining steps.
Then verify the API responds:
curl -s http://127.0.0.1:11434/api/tags | python3 -m json.tool | head -20
Step 17: Stop the gateway
systemctl --user stop openclaw-gateway
Step 18: Add the explicit model provider configuration
nano ~/.openclaw/openclaw.json
Change 1: Add fallbacks to the model config. Find the model section inside agents.defaults:
"model": {
"primary": "anthropic/claude-opus-4-6"
}
Replace it with:
"model": {
"primary": "anthropic/claude-opus-4-6",
"fallbacks": []
}
This keeps Claude as the primary model for all real work. The fallbacks array is empty β you can optionally add local models later (e.g., "ollama/qwen3:8b") if you want automatic fallback when the Claude API is unavailable. Note that smaller local models are much more vulnerable to prompt injection and produce lower-quality code, so use fallbacks with caution.
Change 2: Add a top-level models section. Add this at the top level of the JSON (with correct comma separation):
"models": {
"mode": "merge",
"providers": {
"ollama": {
"baseUrl": "http://<DOCKER_BRIDGE_IP>:11434/v1",
"apiKey": "ollama-local",
"api": "openai-completions",
"models": [
{
"id": "qwen3:8b",
"name": "Qwen3 8B",
"reasoning": false,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 32768,
"maxTokens": 8192
},
{
"id": "gemma3:4b",
"name": "Gemma3 4B",
"reasoning": false,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 8192,
"maxTokens": 8192
},
{
"id": "gemma3:1b",
"name": "Gemma3 1B",
"reasoning": false,
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 8192,
"maxTokens": 8192
},
{
"id": "qwen3-vl:2b",
"name": "Qwen3 VL 2B",
"reasoning": false,
"input": ["text", "image"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 8192,
"maxTokens": 8192
}
]
}
}
}
Note: The models
gemma3:4bandqwen3-vl:2bare registered here but not yet downloaded. To use them, pull them via Ollama when ready:ollama pull gemma3:4b ollama pull qwen3-vl:2bRegistering models before downloading them is harmless β OpenClaw will report them as unavailable until they are pulled.
Configuration explained:
| Key | Purpose |
|---|---|
"mode": "merge" |
Keeps all built-in providers (Anthropic) and adds Ollama alongside them. Without this, Ollama would replace everything. |
"baseUrl" |
Points to Ollama's OpenAI-compatible API endpoint. Uses the Docker bridge gateway IP (<DOCKER_BRIDGE_IP>) instead of 127.0.0.1 so that sandboxed containers can also reach Ollama on the host. Find your Docker bridge IP with: ip addr show docker0 | grep inet. The /v1 suffix is required. |
"apiKey": "ollama-local" |
Ollama doesn't require a real key, but OpenClaw needs a non-empty value for its availability check. |
"api": "openai-completions" |
Tells OpenClaw to use the OpenAI-compatible completions protocol. |
"cost": all zeros |
These models run locally β no per-token charges. |
"input": ["text", "image"] on qwen3-vl |
Marks this as a vision model that can process images. |
"contextWindow" |
Maximum tokens the model can consider. qwen3:8b supports 32k; the smaller models are limited to 8k. |
Step 19: Ensure no OLLAMA_API_KEY in the .env file
If OLLAMA_API_KEY is present in ~/.openclaw/.env, OpenClaw will attempt auto-discovery alongside your explicit config. On the Pi 5, auto-discovery times out and produces a spurious error at startup. Since the API key is already defined in the explicit provider block, the env var is unnecessary.
Check the file:
cat ~/.openclaw/.env
If you see a line like OLLAMA_API_KEY=ollama-local, remove it:
nano ~/.openclaw/.env
Delete the OLLAMA_API_KEY=... line and save. The file should contain only:
OPENCLAW_DISABLE_BONJOUR=1
(Plus any other non-Ollama variables you may have.)
Step 20: Validate, restart, and verify
cat ~/.openclaw/openclaw.json | python3 -m json.tool > /dev/null
systemctl --user start openclaw-gateway
openclaw models list
Expected output:
Model Input Ctx Local Auth Tags
anthropic/claude-opus-4-6 text+image 195k no yes default,configured,alias:opus
Only Claude appears as the active model. The Ollama models (qwen3:8b, gemma3:4b, gemma3:1b, qwen3-vl:2b) are registered and available β you can switch to them manually in Telegram using the /model command, or add them to the fallbacks array if you want automatic failover.
Verification Checklist
- Telegram installed on your phone with 2FA enabled
- Bot created via
@BotFatherwith token saved in a password manager - Group joining disabled in BotFather (
/setjoingroupsβ Disable) -
channels.telegramadded toopenclaw.jsonwith:-
dmPolicyset to"allowlist" -
allowFromcontains only your numeric Telegram user ID -
groupPolicyset to"disabled"
-
- Telegram plugin enabled (
plugins.entries.telegram.enabled: true) - Gateway running β bot responds to your messages in Telegram
- Bot does NOT respond to messages from other users
- Terminal history cleared (
history -c) -
modelssection added with explicit Ollama provider (5 models defined) -
agents.defaults.model.fallbacksincludesollama/qwen3:8b - No
OLLAMA_API_KEYin~/.openclaw/.env -
openclaw models listshows both Claude and the Ollama fallback -
openclaw security audit --deepβ 0 critical issues -
openclaw doctorβ clean, showsTelegram: ok
Troubleshooting
Unit openclaw.service could not be found
The service is named openclaw-gateway, not openclaw. All systemctl commands must use:
systemctl --user <action> openclaw-gateway
To discover the correct service name:
systemctl --user list-units --type=service --all | grep -i claw
Telegram does not appear in logs or openclaw channels status
The Telegram plugin is a bundled plugin that is disabled by default if you skipped channel setup during onboarding. Ensure plugins.entries.telegram.enabled is set to true in openclaw.json. The channels.telegram config block alone is not sufficient.
curl getUpdates returns empty results or {"ok":true,"result":[]}
The gateway was running in polling mode and already consumed your messages. Polling fetches and marks messages as delivered β once consumed, they're gone from Telegram's queue. Always stop the gateway first (systemctl --user stop openclaw-gateway), then send a new message, then run the curl command.
Failed to discover Ollama models: TimeoutError
Ollama auto-discovery calls /api/show for each model to check capabilities. On the Raspberry Pi 5 this can time out. The solution is explicit provider configuration (Step 18) and removing OLLAMA_API_KEY from ~/.openclaw/.env (Step 19) so OpenClaw doesn't attempt auto-discovery at all. Your explicitly defined models will work regardless of this timeout.
Bot shows single checkmark but never double-checks / no reply
This means your message reached Telegram's servers but the bot hasn't read it. Check:
openclaw channels statusβ does it show Telegram asrunning?- Is the Telegram plugin enabled? (see above)
- Is your user ID in
allowFrom? If not, the gateway silently drops your message. - Check logs:
openclaw logs --followβ look for errors related to Telegram or your message.
JSON validation error after editing config
Common causes:
- Missing comma between sibling blocks (e.g., after
}when another key follows) - Trailing comma after the last item in an object or array
- Mismatched braces/brackets
The validation command (python3 -m json.tool) will report the line number of the error. Use nano +<LINE_NUMBER> ~/.openclaw/openclaw.json to jump directly to it.
Security audit shows gateway.trusted_proxies_missing warning
Safe to ignore. This warning applies only if you expose the Control UI through a reverse proxy. With your gateway bound to loopback and Telegram as your access channel, no reverse proxy is involved.
Phase 6 β Setting Up OpenClaw for Coding & Research
OpenClaw is configured and running on your Raspberry Pi 5. This phase creates a dedicated GitHub account for OpenClaw's coding work, backs up the agent workspace, enables the sandbox to push code to GitHub, sets up web search for research tasks, and verifies the full coding workflow end-to-end.
Prerequisites
Before starting this phase, you should have completed:
- Phase 2: Raspberry Pi OS installed and hardened;
openclawservice user created (no sudo privileges); your admin user (e.g.,<ADMIN_USER>) with sudo - Phase 3: Ollama installed; Git already installed on the Pi
- Phase 4: OpenClaw installed and configured (
~/.openclaw/openclaw.jsonat600,~/.openclaw/at700); Docker sandboxing enabled withnetwork: "bridge" - Phase 5: Telegram (or other messaging channel) connected and tested
- You are SSH'd into the Pi as the
openclawuser
Part 1 β Create a Dedicated GitHub Account
Step 1: Create a new email address
Create a free email account (Gmail, Outlook, or ProtonMail) that is not your personal email. This email will only be used for OpenClaw's GitHub account.
Example: openclaw-<YOUR_NAME>@gmail.com
Step 2: Create a new GitHub account
- Go to github.com in your browser (on your personal computer β not on the Pi).
- Click Sign up.
- Use the new email address from Step 1.
- Choose a username that identifies this as your bot's account (e.g.,
<YOUR_NAME>-openclaw-bot). - Complete the sign-up process.
Step 3: Generate a Fine-Grained Personal Access Token (PAT)
A Personal Access Token is a scoped credential that lets OpenClaw interact with GitHub on behalf of this account. Unlike a password, you can limit exactly what it can do and revoke it instantly.
Log into the new GitHub account.
Click your profile picture (top right) β Settings.
Scroll down the left sidebar β Developer settings β Personal access tokens β Fine-grained tokens.
Click Generate new token.
Token name:
openclaw-pi-accessExpiration: 90 days. (When it expires, generate a new one. This limits damage if the token leaks.)
Repository access: "All repositories" (this account has nothing personal on it).
Permissions β grant Read and Write access to the following:
Permission Access Why Actions Read/Write Run and manage GitHub Actions workflows Administration Read/Write Create/manage repositories programmatically Commit statuses Read/Write Report build/test status on commits Contents Read/Write Create and push code Deployments Read/Write Manage GitHub Pages deployments Issues Read/Write Create and manage issues Metadata Read-only Required by GitHub Pages Read/Write Enable and configure GitHub Pages Pull requests Read/Write Create and manage PRs Secrets Read/Write Manage repository secrets for CI/CD Workflows Read/Write Trigger and manage workflow runs Click Generate token.
Copy the token immediately and save it in a password manager. You will not be able to see it again.
β οΈ Security Note: The
Secretspermission grants read/write access to repository secrets (used to store API keys for CI/CD pipelines). If the token were compromised via prompt injection, an attacker could read or inject secrets. This is acceptable now because the dedicated account has no sensitive secrets stored, but reconsider this permission if you later store real credentials in GitHub Actions. You can always generate a new token with fewer permissions and swap it in.
Step 4: Configure Git on the Raspberry Pi
SSH into the Pi as the openclaw user. Git should already be installed from a previous phase.
git config --global user.name "<YOUR_OPENCLAW_GITHUB_USERNAME>"
git config --global user.email "<YOUR_OPENCLAW_EMAIL>"
These commands tell Git to label all commits as coming from the dedicated OpenClaw account. Replace the values with whatever you used when creating the GitHub account.
Step 5: Configure the Git credential helper
git config --global credential.helper store
This tells Git to remember your credentials after you type them once. The next time a git push occurs, Git will prompt for a username and password β use the GitHub username and paste the PAT as the password. After that, it remembers.
Note:
credential.helper storesaves the token in plaintext at~/.git-credentials. This is acceptable because the Pi is an isolated, single-purpose device that only you can SSH into. On a shared machine, you would use a more secure credential store.
Step 6: Back up the workspace to a private GitHub repository
The official OpenClaw documentation recommends backing up the agent workspace (~/.openclaw/workspace) to a private Git repository. The workspace contains personality files (SOUL.md, IDENTITY.md, USER.md, etc.) and memory logs β not credentials.
cd ~/.openclaw/workspace
git init
git add .
git commit -m "Initial workspace"
git initβ initializes version tracking in this folder.git add .β stages all files for the first commit.git commit -m "Initial workspace"β creates a save point with all current files.
Expected output (file list will vary):
[master (root-commit) xxxxxxx] Initial workspace
8 files changed, 311 insertions(+)
create mode 100644 AGENTS.md
create mode 100644 BOOTSTRAP.md
create mode 100644 HEARTBEAT.md
create mode 100644 IDENTITY.md
create mode 100644 SOUL.md
create mode 100644 TOOLS.md
create mode 100644 USER.md
create mode 100644 memory/.gitkeep
Now create a private repository on GitHub:
- Go to github.com (logged into the OpenClaw account).
- Click the + button β New repository.
- Name it
openclaw-workspace. - Select Private.
- Do NOT check "Initialize with README."
- Click Create repository.
- Copy the HTTPS URL (e.g.,
https://github.com/<YOUR_OPENCLAW_GITHUB_USERNAME>/openclaw-workspace.git).
Back on the Pi:
git remote add origin https://github.com/<YOUR_OPENCLAW_GITHUB_USERNAME>/openclaw-workspace.git
git branch -M main
git push -u origin main
When prompted for credentials, enter the GitHub username and paste the PAT as the password.
git remote add origin <url>β links the local repo to the GitHub repository.git branch -M mainβ renames the default branch tomain.git push -u origin mainβ uploads the code. The-uflag sets this as the default push target.
Secure the stored credentials:
chmod 600 ~/.git-credentials
git credential.helper storesaves the token in plaintext. Unlike files inside~/.openclaw/(which is protected by its700directory permissions),~/.git-credentialssits in/home/openclaw/which is typically world-readable (755). Setting600prevents other users on the Pi from reading the token.
Part 1 Checklist
- New email address created (not your personal one)
- New GitHub account created with that email
- Fine-grained PAT generated with the permissions listed above, 90-day expiry
- Token saved in your password manager
- Git configured on the Pi with the OpenClaw account's name and email
- Credential helper configured (
store) - Workspace initialized as a Git repo and pushed to a private GitHub repository
-
git pushcompleted without errors
Part 2 β (Optional) Web Search via Brave API
This section is optional. OpenClaw already has full web access via the
browsertool (Chromium) andweb_fetch(lightweight HTTP). These two tools cover all web access use cases without any API keys or costs.Brave Search adds structured search-engine-style results (like an API version of Google) β useful for quickly finding URLs, but not essential. Consider the tradeoffs before adding it:
Factor Without Brave With Brave Web access β browser+web_fetchβ All three tools API key needed No Yes (additional secret to manage) Cost Free Free tier: 2,000 queries/month; paid beyond Attack surface Smaller Slightly larger (one more API key to protect and rotate)
If you want Brave Search, follow the steps below. Otherwise, skip to Part 3.
Step 1: Obtain a Brave Search API key
- Go to brave.com/search/api/ on your personal computer.
- Create an account and choose the free tier ("Data for Search" plan).
- Generate an API key.
Step 2: Add the API key to OpenClaw's environment
nano ~/.openclaw/.env
Add this line:
BRAVE_API_KEY=<YOUR_BRAVE_SEARCH_API_KEY>
Save and exit (Ctrl+O, Enter, Ctrl+X). OpenClaw's web_search tool will automatically use Brave Search when this key is present β no config change needed.
Step 3: Enable the web_search tool
If you set up Brave Search, you need to move web_search from the deny list to the allow list in openclaw.json:
nano ~/.openclaw/openclaw.json
In tools.allow, add "web_search". In tools.deny, remove "web_search". Then restart:
systemctl --user restart openclaw-gateway
Verify permissions are still restrictive after editing:
ls -la ~/.openclaw/.env
Expected: -rw-------
If it shows -rw-r--r-- (can happen if a new file was created instead of appended):
chmod 600 ~/.openclaw/.env
Part 2 Checklist
- Brave Search API key obtained
- Key added to
~/.openclaw/.env -
web_searchmoved fromtools.denytotools.allowinopenclaw.json
Part 3 β Enable the Sandbox to Push Code to GitHub
If your sandbox is configured with network: "bridge" (set in Phase 4), it already has outbound internet access. This part focuses on adding GitHub credentials to the sandbox environment so OpenClaw can git push from inside the container. If your sandbox still uses network: "none", update it to "bridge" as shown in Step 2 below.
β οΈ Security Note: Changing the sandbox network from
"none"to"bridge"allows the sandbox to make outbound internet connections. If a prompt injection attack tricked OpenClaw into running a malicious command, it could potentially send data outbound. Your network isolation (the Pi on a separate VLAN/guest network) is the safety net β even if data leaves the Pi, it cannot reach your personal devices. You can revert to"none"at any time.
Step 1: Add the GitHub token to the environment file
nano ~/.openclaw/.env
Add this line (below the Brave key if already present):
GITHUB_TOKEN=<YOUR_GITHUB_PAT>
Save and exit.
Verify permissions are still restrictive:
ls -la ~/.openclaw/.env
Expected: -rw-------
If it shows -rw-r--r--:
chmod 600 ~/.openclaw/.env
Step 2: Update the sandbox configuration
nano ~/.openclaw/openclaw.json
Locate the agents.defaults.sandbox.docker section. Update network from "none" to "bridge" and add the env block with Git credentials. The relevant section should look like this:
{
"agents": {
"defaults": {
"sandbox": {
"docker": {
"network": "bridge",
"user": "<OPENCLAW_UID>:<OPENCLAW_UID>",
"setupCommand": "git config --global credential.helper '!f() { echo username=x-access-token; echo password=$GITHUB_TOKEN; }; f'",
"env": {
"GIT_CONFIG_NOSYSTEM": "1",
"GIT_CONFIG_GLOBAL": "/tmp/.gitconfig",
"GIT_AUTHOR_NAME": "<YOUR_OPENCLAW_GITHUB_USERNAME>",
"GIT_AUTHOR_EMAIL": "<YOUR_OPENCLAW_EMAIL>",
"GIT_COMMITTER_NAME": "<YOUR_OPENCLAW_GITHUB_USERNAME>",
"GIT_COMMITTER_EMAIL": "<YOUR_OPENCLAW_EMAIL>",
"GITHUB_TOKEN": "<YOUR_GITHUB_PAT>"
}
}
}
}
}
}
Note: Only the fields being added or changed are shown. Keep all your existing settings (
mode,scope,workspaceAccess,image,readOnlyRoot) intact.
Note: The
setupCommandconfigures a dynamic Git credential helper inside each sandbox container. When Git needs to authenticate (e.g., duringgit push), it calls this helper function, which returns theGITHUB_TOKENfrom the environment. This is more robust than storing plaintext credentials in.git-credentialsβ the token is only exposed when Git actively needs it.
Note: The sandbox does not inherit the host's
process.env. Environment variables must be set explicitly inagents.defaults.sandbox.docker.env. The OpenClaw docs confirm this: sandbox containers are isolated from the host environment. This means the token must be placed directly in this config block. The file is already permission-locked to600(only theopenclawuser can read it), limiting exposure.
Note:
GIT_CONFIG_NOSYSTEMandGIT_CONFIG_GLOBALfix a sandbox-specific issue: the container's$HOMEresolves to/(the filesystem root), which isn't writable. Without these variables, thesetupCommandfails immediately witherror: could not lock config file //.gitconfig: Permission denied, killing every sub-agent before it starts β even if the task doesn't involve Git at all.GIT_CONFIG_GLOBAL=/tmp/.gitconfigredirects Git's global config to a writable path so the credential helper setup succeeds. These must be present in theenvblock from the start, not just when you add Git credentials in Phase 6.
Save and exit.
Step 3: Validate the JSON
Before restarting, check that your config file is valid:
cat ~/.openclaw/openclaw.json | python3 -m json.tool > /dev/null
If this command produces no output, the JSON is valid. If it prints an error, re-edit the file and fix the syntax.
Step 4: Restart the OpenClaw gateway
systemctl --user restart openclaw-gateway
Note: The systemd service is named
openclaw-gateway, notopenclaw. The commandopenclaw restartis not a valid CLI command β/restartis a slash command sent through the messaging channel. To restart from the terminal, always usesystemctl.
Verify it's running:
systemctl --user status openclaw-gateway
You should see active (running) in the output.
Part 3 Checklist
-
GITHUB_TOKENadded to~/.openclaw/.env - Sandbox config updated:
network: "bridge",envblock with Git credentials - JSON validated without errors
- File permissions still correct (
openclaw.jsonis600) - Gateway restarted with
systemctl --user restart openclaw-gateway - Gateway status confirmed as
active (running)
Part 4 β Test the Full Coding Workflow
Step 1: Send a test project request
Through your messaging channel (Telegram), send OpenClaw a message like:
Create a simple HTML page that says "Hello from OpenClaw" with some nice styling. Initialize a git repo, commit the code, and push it to a new public repository called "hello-test" on GitHub.
Step 2: Verify on GitHub
Check the dedicated GitHub account in your browser. The new repository should appear with the committed code.
Step 3: View the rendered webpage with GitHub Pages
GitHub Pages hosts static websites for free directly from a repository.
- Go to the repo on GitHub (logged into the OpenClaw account).
- Settings β Pages (in the left sidebar).
- Under Source, select Deploy from a branch.
- Pick the
mainbranch,/ (root)folder. - Click Save.
Within a minute or two, the page will be live at:
https://<YOUR_OPENCLAW_GITHUB_USERNAME>.github.io/hello-test/
Note: GitHub Pages on the free plan requires public repositories. This is fine β these are throwaway prototypes on a dedicated bot account with no personal data.
Part 4 Checklist
- Sent a test coding + push request through the messaging channel
- OpenClaw created files, committed, and pushed successfully
- Repository appeared on GitHub with the correct code
- (Optional) GitHub Pages enabled and webpage accessible
Part 5 β Safe Review Habits
These are not configuration steps β they are practices to follow as you use OpenClaw for real projects.
Always review before you trust. OpenClaw can write working code, but it can also introduce bugs, security flaws, or unwanted dependencies. Before using any prototype, read through the code. Open GitHub Pages links and test the behavior.
Check what it committed. On the GitHub repo page, click into the Commits tab. Verify it only committed what you asked for β no extra files, no credentials, no unexpected changes.
Use /status regularly. This slash command (sent in Telegram) shows the current model, token usage, and cost for the session. Build a habit of checking it during long sessions.
Use /new between projects. This slash command resets the session (clears conversation history). Each new message after /new only sends your request plus the system prompt β cheaper and faster. Use it when switching between unrelated tasks.
Use /compact during long sessions. This summarizes older conversation history into a shorter form, freeing up tokens while preserving important context. Use it when a single task requires a long back-and-forth.
If something feels off, stop and check logs:
cat ~/.openclaw/agents/*/sessions/*.jsonl | tail -50
Or send /stop in the chat to immediately halt whatever OpenClaw is doing.
Verification β Phase 6 Complete
- Dedicated GitHub account created (separate from your personal account)
- Workspace backed up to a private GitHub repository
- (Optional) Brave Search API key configured
- Sandbox configured with network access, Git credential helper, and GitHub credentials
- Full coding workflow tested: describe β OpenClaw builds β code pushed to GitHub
- (Optional) GitHub Pages enabled for viewing prototypes
- Review habits understood:
/new,/compact,/status,/stop
Troubleshooting
openclaw restart returns "unknown command"
restart is not a CLI command. Use systemctl --user restart openclaw-gateway from the terminal, or send /restart as a slash command through the messaging channel.
Service name not found (Unit openclaw.service not found)
The systemd service is named openclaw-gateway, not openclaw. Find the exact name with:
systemctl --user list-units --type=service | grep -i openclaw
Sub-agents crash with could not lock config file //.gitconfig: Permission denied
Every sub-agent dies immediately with zero tokens processed. The full error is:
error: could not lock config file //.gitconfig: Permission denied
Cause: The sandbox container's $HOME is / (the filesystem root), which isn't writable. The setupCommand runs git config --global ... during bootstrap, which tries to write to $HOME/.gitconfig β i.e., /.gitconfig. The write fails, and the entire agent startup crashes before it can process any task.
Fix: Add two environment variables to agents.defaults.sandbox.docker.env in openclaw.json:
"env": {
"GIT_CONFIG_NOSYSTEM": "1",
"GIT_CONFIG_GLOBAL": "/tmp/.gitconfig",
...other env entries...
}
GIT_CONFIG_NOSYSTEM=1 skips the system-wide Git config. GIT_CONFIG_GLOBAL=/tmp/.gitconfig redirects the global config to a writable path. The setupCommand credential helper still works β it just writes to /tmp/.gitconfig instead. Restart the gateway after applying.
Why not
/dev/null? Using/dev/nullwould silently discard the credential helper written bysetupCommand, breaking Git authentication for any sub-agent that needs to push code.
OpenClaw cannot push to GitHub from the sandbox
This happens when the sandbox still has network: "none" or is missing the GITHUB_TOKEN in its env block. The sandbox does not inherit host environment variables β credentials must be explicitly set in agents.defaults.sandbox.docker.env within openclaw.json.
sudo commands fail as the openclaw user
By design, the openclaw user has no sudo privileges. Any packages that need installing should be installed by your admin user (e.g., <ADMIN_USER>). Git should already be installed from a previous phase.
JSON validation fails after editing openclaw.json
OpenClaw accepts JSON5 format (which supports comments // and trailing commas), but all configuration blocks in this guide use strict JSON for maximum compatibility. The validation command python3 -m json.tool validates strict JSON only.
If you add JSON5-specific syntax (comments, trailing commas), python3 -m json.tool will report errors. In that case, skip the validation step and rely on openclaw doctor after restarting the gateway to catch config issues.
Phase 6.5: Google Drive Access for Document Sharing
What this phase accomplishes: Configure the Pi so that OpenClaw can upload reports and documents to a dedicated Google Drive account, allowing you to access files from any device without needing to SSH into the Pi.
Prerequisites: Phases 1β6 complete. You have a dummy Gmail account created specifically for OpenClaw (the same one used for the GitHub account, or a separate one β your choice).
Why this phase exists: OpenClaw runs inside a Docker sandbox. Files it creates are only accessible via the shared workspace directory (~/.openclaw/workspace/). While you can SSH in and retrieve files, it's much more convenient to have OpenClaw upload them directly to a Google Drive you can access from your phone or laptop.
Security note: The Google Drive account must be a dedicated, throwaway account β never your personal one. If OpenClaw's credentials are compromised, the attacker only gets access to an empty Google Drive, not your personal files.
Critical: How to SSH In Correctly
Throughout this phase you will need two types of SSH sessions β one as your admin user (for installing system packages) and one as the openclaw user (for everything else). Getting this wrong causes subtle, confusing failures.
β οΈ The #1 gotcha in this phase: You must SSH in directly as the target user. Never use
su openclaworsudo su - openclawto switch users after logging in. Switching users this way does not start a proper systemd user session, which causessystemctl --usercommands to fail with:Failed to connect to user scope bus via local transport: Operation not permitted
If connecting via Tailscale SSH:
By default, Tailscale SSH may log you in as root. You must specify the user explicitly:
# β
CORRECT β specify the user before the hostname
ssh openclaw@openclaw-pi
# β
CORRECT β admin user
ssh <ADMIN_USER>@openclaw-pi
# β WRONG β lands as root, then switching user breaks systemd
ssh openclaw-pi
su openclaw
If connecting via the isolated network directly:
# β
CORRECT
ssh openclaw@<RASPBERRY_PI_IP>
# β
CORRECT β admin user
ssh <ADMIN_USER>@<RASPBERRY_PI_IP>
How to verify you're in the right session:
whoami && pwd
| User | Expected output |
|---|---|
| Admin | <ADMIN_USER> and /home/<ADMIN_USER> |
| OpenClaw | openclaw and /home/openclaw |
If you see /root as your working directory, you're logged in as root β exit and reconnect as the correct user.
Part A β Install rclone on the Pi
rclone is a command-line tool that syncs files to cloud storage (Google Drive, Dropbox, etc.). Think of it as a bridge between the Pi's filesystem and cloud storage.
1. Install rclone (as admin user)
SSH in as your admin user (not openclaw β the openclaw user cannot install system packages):
ssh <ADMIN_USER>@<RASPBERRY_PI_IP>
Or via Tailscale:
ssh <ADMIN_USER>@openclaw-pi
Then install:
sudo apt install -y rclone
Verify:
rclone --version
Expected: A version string like rclone v1.x.x.
2. Install rclone on your laptop too
You will need rclone on your laptop to complete the Google authentication (the Pi has no web browser).
- macOS:
brew install rclone - Windows: Download from rclone.org/downloads
- Linux:
sudo apt install rclone
Verify on your laptop:
rclone --version
3. Exit and reconnect as the openclaw user
exit
ssh openclaw@<RASPBERRY_PI_IP>
Or via Tailscale:
ssh openclaw@openclaw-pi
Verify: whoami returns openclaw and pwd returns /home/openclaw.
Part B β Authenticate rclone with the Dummy Google Account
This step connects rclone to your dummy Google Drive. Because the Pi is headless (no web browser), we authorise on your laptop and transfer the token to the Pi.
4. Start rclone configuration on the Pi
rclone config
Follow these prompts:
| Prompt | Selection | Why |
|---|---|---|
n/s/q> |
n (New remote) |
Create a new cloud storage connection |
name> |
openclaw-gdrive |
A label for this connection |
Storage> |
drive (or type the number next to "Google Drive") |
We're connecting to Google Drive |
client_id> |
Press Enter (leave blank) | Uses rclone's built-in client ID |
client_secret> |
Press Enter (leave blank) | Uses rclone's built-in client secret |
scope> |
1 (Full access) |
OpenClaw needs to create and upload files |
service_account_file> |
Press Enter (leave blank) | Not using a service account |
Edit advanced config? |
n (No) |
Defaults are fine |
Use auto config? |
n (No) |
The Pi is headless β no browser available |
Rclone will now display a command like this:
For this to work, you will need rclone available on a machine that has
a web browser available.
Execute the following on the machine with the web browser (same rclone
version recommended):
rclone authorize "drive" "eyJjbGll...long-encoded-string..."
Then paste the result.
config_token>
Leave this terminal open and waiting. Do not press anything. We now switch to your laptop.
5. Authorise on your laptop
Open a new, separate terminal window on your laptop (keep the Pi's SSH session open).
Copy-paste the exact command shown on the Pi (including the long encoded string in quotes):
rclone authorize "drive" "eyJjbGll...the-exact-string-from-your-Pi..."
β οΈ Important: Copy the complete command from the Pi's terminal. The long encoded string after
"drive"contains your remote's configuration. Don't just typerclone authorize "drive"without it.
What happens next:
- Your laptop's default browser opens to a Google sign-in page.
- Sign in with your dedicated dummy Gmail account β NOT your personal Google account.
- Google may show a warning: "This app isn't verified." This is normal for rclone.
- Click Advanced β Go to rclone (unsafe) β Allow.
- The browser shows "Success!" and your laptop's terminal displays a token:
Paste the following into your remote machine --->
{"access_token":"ya29.EXAMPLE_TOKEN...","token_type":"Bearer","refresh_token":"1//EXAMPLE_REFRESH...","expiry":"2026-..."}
<---End paste
6. Paste the token back on the Pi
- Copy the entire JSON blob from your laptop's terminal β everything from
{to}inclusive. - Switch back to your Pi's SSH session (which is still waiting at
config_token>). - Paste the token and press Enter.
Continue with the remaining prompts:
| Prompt | Selection | Why |
|---|---|---|
Configure this as a Shared Drive (Team Drive)? |
n (No) |
Using a personal Drive |
Keep this "openclaw-gdrive" remote? |
y (Yes) |
Save the configuration |
q |
Quit config | Done |
7. Test the connection
rclone lsd openclaw-gdrive:
Expected: An empty listing (the Drive is new) or a list of any existing folders. If you see an error, re-run rclone config, select e (Edit existing remote), choose openclaw-gdrive, and repeat the authorisation flow from Step 4.
8. Create a folder structure on Google Drive
rclone mkdir openclaw-gdrive:OpenClaw/reports
rclone mkdir openclaw-gdrive:OpenClaw/projects
Verify:
rclone lsd openclaw-gdrive:OpenClaw
Expected: Two folders listed (reports and projects).
Part C β Make rclone Available Inside the Sandbox
The Docker sandbox has its own filesystem and does NOT have access to host tools by default. To let OpenClaw use rclone from inside the sandbox, we need to: (1) build a new sandbox image that includes rclone, (2) mount the rclone credentials into the container, and (3) tell OpenClaw to use the new image.
9. Build a custom sandbox image with rclone
cat > /tmp/Dockerfile.sandbox-gdrive << 'EOF'
FROM openclaw-sandbox:bookworm-slim
USER root
RUN apt-get update && \
apt-get install -y --no-install-recommends rclone && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
USER claw
EOF
docker build -t openclaw-sandbox:gdrive -f /tmp/Dockerfile.sandbox-gdrive /tmp
Verify:
docker images | grep openclaw-sandbox
Expected: Two images listed β bookworm-slim (your original) and gdrive (the new one).
10. Stop the gateway and update the OpenClaw config
systemctl --user stop openclaw-gateway
nano ~/.openclaw/openclaw.json
Find the agents.defaults.sandbox.docker section and make two changes:
Change 1: Update the image name:
"docker": {
"image": "openclaw-sandbox:gdrive",
Change 2: Add the rclone config to the bind mounts. Add a binds array inside the docker block (alongside network, readOnlyRoot, etc.):
"docker": {
"image": "openclaw-sandbox:gdrive",
"network": "bridge",
"readOnlyRoot": false,
"user": "<OPENCLAW_UID>:<OPENCLAW_UID>",
"binds": [
"/home/openclaw/.openclaw/workspace/.shared-config/rclone:/home/claw/.config/rclone:ro"
],
"env": {
...existing env entries (GIT_AUTHOR_NAME, etc.)...
}
}
Use the same UID you noted in Phase 4, Step 15 (e.g.,
"1001:1001"). This must match Phase 4's config β the container user and host user must have the same UID for bind-mounted files to be accessible.
What each change does:
| Key | Value | Purpose |
|---|---|---|
"image" |
"openclaw-sandbox:gdrive" |
Uses the new image with rclone pre-installed |
"binds" |
rclone config path with :ro |
Mounts the rclone credentials (read-only) into the container so rclone can authenticate with Google Drive |
β οΈ Security Note β Bind mounts: The
:roat the end means read-only. The sandbox can read the rclone credentials to authenticate with Google Drive, but cannot modify or delete them. This is the least-privilege approach.
Save and exit: Ctrl+O, Enter, Ctrl+X.
11. Verify rclone.conf permissions
If you followed Phase 4's dynamic UID approach (where the Dockerfile's
useradd UID matches your openclaw user's UID), bind-mounted files
are readable by the container without loosening permissions.
Verify:
id -u
This should match the UID used in your Dockerfile from Phase 4, Step 15,
and the "user" field in openclaw.json. If they match, no permission
changes are needed β rclone.conf can stay at its default 600.
If, for any reason, the UIDs don't match (e.g., you rebuilt the sandbox image without the dynamic UID), you have two options:
Recommended: Rebuild the sandbox image with the correct UID:
OPENCLAW_UID=$(id -u) # Re-run the Dockerfile build from Phase 4, Step 15, then: docker build -t openclaw-sandbox:gdrive -f /tmp/Dockerfile.sandbox-gdrive /tmp openclaw sandbox recreate --allQuick workaround: Loosen the file permissions:
chmod 644 ~/.config/rclone/rclone.confThis allows any local user to read the file. Acceptable on this single-purpose, isolated Pi, but less clean than fixing the UID.
12. Validate the JSON and restart the gateway
cat ~/.openclaw/openclaw.json | python3 -m json.tool > /dev/null
If no output appears, the JSON is valid. If you see an error, re-open the file and fix the syntax (usually a missing or extra comma).
Now restart:
systemctl --user restart openclaw-gateway
systemctl --user status openclaw-gateway
Expected: active (running).
β οΈ If you see
Failed to connect to user scope bus via local transport: Operation not permitted: You are not in a direct SSH session as theopenclawuser. This happens when you usedsu openclaworsudo su - openclawto switch users, or when Tailscale SSH logged you in as root. Fix: Exit completely (exit, possibly twice), then reconnect withssh openclaw@<RASPBERRY_PI_IP>orssh openclaw@openclaw-pi. See the "Critical: How to SSH In Correctly" section at the top of this phase.
13. Recreate sandbox containers
The existing sandbox containers still use the old image. Force them to rebuild:
openclaw sandbox recreate --all
OpenClaw will list the containers to be removed and show a confirmation prompt:
β This will stop and remove these containers. Continue?
β β Yes / β No
UI note: The filled circle
βindicates the currently selected option. By default, No is selected. Use the left arrow key β to move the selection to Yes, then press Enter to confirm.
The old containers will be removed. They are automatically recreated with the new openclaw-sandbox:gdrive image the next time OpenClaw needs them (i.e., when you send it a message via Telegram).
Part D β Test the Integration
14. Test from SSH first
Verify rclone works from the openclaw user on the host:
echo "Test file from Pi" > /tmp/test-upload.txt
rclone copy /tmp/test-upload.txt openclaw-gdrive:OpenClaw/reports/
rclone ls openclaw-gdrive:OpenClaw/reports/
Expected: test-upload.txt listed. Check Google Drive from your browser β the file should be there.
Clean up:
rclone delete openclaw-gdrive:OpenClaw/reports/test-upload.txt
rm /tmp/test-upload.txt
15. Test via Telegram
Send OpenClaw a message through Telegram:
Create a short text file called "hello.txt" containing "Hello from OpenClaw!" and save it to /workspace/reports/. Then upload it to Google Drive by running:
rclone copy /workspace/reports/hello.txt openclaw-gdrive:OpenClaw/reports/
Check Google Drive β hello.txt should appear in the OpenClaw/reports folder.
16. Teach OpenClaw the workflow
For best results, when you ask OpenClaw to create reports, include the upload step in your instructions:
Create a report about [topic]. Save the report to /workspace/reports/[filename]. After saving, upload it to Google Drive:
rclone copy /workspace/reports/[filename] openclaw-gdrive:OpenClaw/reports/
Pro tip: You can create a reusable instruction by adding a note to OpenClaw's system prompt (in the AGENTS.md file in the workspace). This way you don't have to repeat the upload command every time. Example line to add:
When creating reports or documents, always save them to /workspace/reports/ and then upload them to Google Drive using: rclone copy /workspace/reports/<filename> openclaw-gdrive:OpenClaw/reports/
To add this:
nano ~/.openclaw/workspace/AGENTS.md
Add the instruction, save (Ctrl+O, Enter, Ctrl+X), and restart:
systemctl --user restart openclaw-gateway
Part E β Run Security Checks
17. Audit
openclaw security audit --deep
openclaw doctor
Expected: 0 critical issues.
18. Verify file permissions
ls -la ~/.config/rclone/rclone.conf
Expected: -rw------- (600 β readable only by the openclaw user). If the
UID strategy from Phase 4 was followed correctly, 600 is sufficient because
the container user and host user share the same UID.
If permissions are wrong:
chmod 600 ~/.config/rclone/rclone.conf
Verification
- rclone installed on the Pi (
rclone --version) and on your laptop - Authenticated with the dedicated dummy Gmail account (not personal)
-
rclone lsd openclaw-gdrive:OpenClawshowsreportsandprojectsfolders - Custom sandbox image built (
docker images | grep gdrive) -
openclaw.jsonupdated with new image name and rclone bind mount (:ro) - rclone config permissions set to
600(sufficient when the dynamic UID strategy from Phase 4 is followed β see Step 11) - JSON validated (
python3 -m json.toolβ no errors) - Gateway restarted successfully (
systemctl --user status openclaw-gatewayshowsactive (running)) - Sandbox containers recreated (
openclaw sandbox recreate --all) - Test upload from SSH works β file appears in Google Drive
- Test upload from Telegram works β file appears in Google Drive
- Security audit: 0 critical; doctor: clean
Troubleshooting
Failed to connect to user scope bus via local transport: Operation not permitted: You are not in a direct SSH session. Exit completely and reconnect with ssh openclaw@<RASPBERRY_PI_IP> or ssh openclaw@openclaw-pi. Never use su openclaw or sudo su - openclaw. See the "Critical: How to SSH In Correctly" section at the top.
Tailscale SSH logs you in as root: Specify the user explicitly: ssh openclaw@openclaw-pi. If this is rejected, check your Tailscale SSH ACLs in the Tailscale admin console.
sandbox recreate confirmation prompt β can't select Yes: Use the left arrow key β to move the selection from No to Yes, then press Enter.
rclone: command not found inside sandbox: The sandbox is still using the old image. Run openclaw sandbox recreate --all (select Yes) and retry.
rclone copy fails with "permission denied" inside sandbox: The bind mount may not be working. Verify the binds path matches exactly: /home/openclaw/.openclaw/workspace/.shared-config/rclone:/home/claw/.config/rclone:ro. The left side is the host path; the right side is the container path.
OpenClaw says "rclone config exists but can't read it" or reports a UID/permission mismatch: The container user's UID does not match the host openclaw user's UID. If you followed Phase 4's dynamic UID strategy, rebuild the sandbox image with the correct UID (see Step 11). As a quick workaround, run chmod 644 ~/.config/rclone/rclone.conf on the Pi β but rebuilding the image is the cleaner fix. Do not run chown 1000:1000 as OpenClaw may suggest β that would change ownership away from the openclaw user on the host, which could cause different problems.
Failed to create file system for "openclaw-gdrive" inside sandbox: The rclone config isn't accessible. Check that the host path exists (ls ~/.openclaw/workspace/.shared-config/rclone/rclone.conf) and that the bind mount uses :ro.
Token paste step β browser shows "This app isn't verified": This is normal for rclone. Click Advanced β Go to rclone (unsafe) β Allow.
rclone authorize on laptop shows wrong version warning: The Pi and laptop should ideally have the same rclone version. Minor version differences usually work fine. If authorisation fails, update rclone on both machines and retry.
Files upload but Google Drive shows 0 bytes: Network issue mid-transfer. Retry the upload. If persistent, check docker.network is set to "bridge" (not "none").
Phase 6.5 added to support document sharing via Google Drive. Verify against official OpenClaw docs for any changes to sandbox bind mount syntax.
Phase 7 β Ongoing Maintenance & Monitoring
Detailed walkthroughs for each item below are planned for a future update. This checklist covers the essentials.
Maintenance Checklist
| Task | Frequency | How |
|---|---|---|
| Monitor API costs | Weekly | Log into console.anthropic.com β Usage tab. Verify spending is within your configured limit. |
| Check OpenClaw logs | As needed | openclaw logs --follow or journalctl --user -u openclaw-gateway -n 100 |
| Update OpenClaw | When notified | As the openclaw user: npm update -g openclaw@latest, then systemctl --user restart openclaw-gateway |
| Update Ollama models | Monthly | ollama pull qwen3:1.7b (re-pulls latest version). Repeat for each model. |
| Update the OS | Automatic | unattended-upgrades handles security patches. Verify: sudo unattended-upgrades --dry-run |
| Rotate Anthropic API key | Every 90 days | Generate a new key in the Anthropic Console. Update ~/.openclaw/openclaw.json. Revoke the old key. Restart the gateway. |
| Rotate gateway auth token | Every 90 days | Stop the gateway. Edit gateway.auth.token in openclaw.json. Restart. Update the admin user's CLI config (openclaw config set gateway.auth.token). |
| Rotate GitHub PAT | On expiry (90 days) | Generate a new fine-grained token. Update ~/.openclaw/.env (GITHUB_TOKEN) and sandbox.docker.env.GITHUB_TOKEN in openclaw.json. Update ~/.git-credentials. Restart. |
| Re-run security audit | Monthly | openclaw security audit --deep β should show 0 critical. |
| Backup OpenClaw workspace | Weekly or after changes | cd ~/.openclaw/workspace && git add -A && git commit -m "backup" && git push |
Incident Response
If something seems wrong β unexpected messages, unusual API charges, OpenClaw behaving erratically β follow the OpenClaw Security docs' incident response protocol:
- Stop the gateway immediately:
systemctl --user stop openclaw-gateway - Rotate all secrets: Anthropic API key, gateway token, GitHub PAT, Telegram bot token.
- Review logs:
openclaw logsandjournalctl --user -u openclaw-gateway - Re-run the security audit:
openclaw security audit --deep - Restart only after all secrets are rotated and the audit is clean.
Phase 8: Tailscale Remote Access & Control UI Setup
Set up Tailscale to securely access the Raspberry Pi (SSH and OpenClaw Control UI) from any network β without switching WiFi or breaking network isolation.
Prerequisites
Before starting this phase, you should have completed:
- Network isolation: The Raspberry Pi is on an isolated network (e.g., a GL.iNet travel router) separate from your main home WiFi.
- Raspberry Pi OS hardened: SSH (public-key only) configured, UFW firewall active with default-deny incoming, a dedicated admin user (
<ADMIN_USER>) withsudoprivileges, and a restricted user (openclaw) running the OpenClaw gateway β withoutsudo. - OpenClaw installed and running: The gateway is operational and accessible via a messaging channel (e.g., Telegram). The gateway is bound to loopback (
127.0.0.1) with token-based authentication. - Existing SSH access: You can SSH into the Pi from the isolated network (e.g.,
ssh <ADMIN_USER>@<RASPBERRY_PI_IP>when connected to the GL.iNET WiFi).
What you'll need
| Item | Purpose |
|---|---|
| A laptop (macOS) on your normal home WiFi | Installing Tailscale client, accessing the Control UI |
| SSH access to the Pi via the isolated network | Installing Tailscale on the Pi (one last time) |
| An email address or SSO account (Google, Microsoft, GitHub, Apple) | Creating your Tailscale account |
Key concepts
- Tailscale is a service that creates a private, encrypted overlay network (called a tailnet) connecting only your specific devices to each other β regardless of what physical network they're on. It uses WireGuard encryption and does not require opening any ports on your router.
- Tailscale Serve is a Tailscale feature that securely proxies a local service (like the OpenClaw gateway on
127.0.0.1) so that other devices on your tailnet can access it via HTTPS. - MagicDNS is Tailscale's built-in DNS feature that lets you reach devices by hostname (e.g.,
openclaw-pi) instead of IP address. <GATEWAY_PORT>refers to the OpenClaw gateway port configured in Phase 4. The default is 18789. If you used a different port, substitute it wherever you see<GATEWAY_PORT>in this phase.
Steps
1. Create a Tailscale account
Where: Your laptop, on your normal home WiFi. No need to touch the Pi yet.
- Open your browser and go to https://login.tailscale.com.
- Sign up using Google, Microsoft, GitHub, Apple, or email.
- After signing in, you'll land on the Tailscale admin console β a dashboard showing all devices on your tailnet. It will be empty initially.
- Note your tailnet name. It appears at the top of the admin console and looks like
tail1234.ts.netoryour-name.github. You'll need this later.
Why: Your Tailscale account defines your tailnet. Only devices logged into your account can see each other. This is what keeps it secure.
Cost: Tailscale's free "Personal" plan supports up to 100 devices and 3 users. This setup requires only 2 devices and 1 user.
2. Install Tailscale on the Raspberry Pi
Where: SSH into the Pi from the isolated network. This is the last time you'll need to switch WiFi for routine Pi management.
ssh <ADMIN_USER>@<RASPBERRY_PI_IP>
2a. Install Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
Downloads and runs Tailscale's official install script. It auto-detects Raspberry Pi OS (64-bit Debian) and installs the correct version.
Expected output: Package installation messages, finishing within 1β2 minutes.
Note β Kernel mismatch dialog: If you see a message like "Newer kernel available β The currently running kernel version is X which is not the expected kernel version Y", click OK to dismiss it. This means a system update installed a newer kernel that hasn't been loaded yet. We'll reboot after installation.
2b. Reboot the Pi
After installation completes, reboot to load any pending kernel updates:
sudo reboot
Wait 30β60 seconds, then SSH back in:
ssh <ADMIN_USER>@<RASPBERRY_PI_IP>
2c. Start Tailscale and authenticate
sudo tailscale up --ssh --hostname=openclaw-pi
| Flag | Purpose |
|---|---|
--ssh |
Enables Tailscale SSH β lets you SSH into the Pi through Tailscale without needing traditional SSH keys. Authentication is handled by your Tailscale account identity. |
--hostname=openclaw-pi |
Gives the Pi a readable name on your tailnet instead of a random string. |
How Tailscale SSH relates to Phase 2b hardening: Tailscale SSH uses your Tailscale account identity for authentication β not SSH keys or passwords. Connections via Tailscale bypass the traditional SSH daemon (and therefore bypass fail2ban and your sshd_config settings). This is secure because only devices authenticated on your personal Tailscale account can connect, but it is a different authentication path from the one hardened in Phase 2b. Both paths remain active: direct SSH on the isolated network uses your SSH keys + sshd_config; Tailscale SSH uses your Tailscale identity.
Expected output: A URL like:
To authenticate, visit:
https://login.tailscale.com/a/abc123xyz
Copy this URL, open it in your laptop's browser, and log in with the same Tailscale account you just created. This authorizes the Pi to join your tailnet.
2d. Verify Tailscale is connected
tailscale status
Expected output: Your Pi listed with a Tailscale IP address (format: 100.x.y.z).
3. Install Tailscale on your Mac
Where: Your laptop. Switch back to your normal home WiFi.
- Open the App Store on your Mac.
- Search for Tailscale.
- Download and install it (free).
- Open the Tailscale app. A small icon appears in the menu bar (top-right of screen).
- Click the icon β Log in. Sign in with the same Tailscale account.
4. Test Tailscale connectivity
Where: Your Mac's Terminal (Cmd+Space β type "Terminal"). Your Mac should be on your normal home WiFi; the Pi should be on the isolated network. They are on completely separate networks.
4a. Verify both devices are visible
tailscale status
Expected: Both your Mac and openclaw-pi listed with 100.x.y.z addresses.
4b. Test network connectivity
ping openclaw-pi -c 4
Sends 4 test messages to the Pi via the Tailscale network.
Expected:
64 bytes from 100.x.y.z: icmp_seq=0 time=5.2 ms
64 bytes from 100.x.y.z: icmp_seq=1 time=4.8 ms
...
If you see replies, Tailscale networking is working.
4c. Test SSH (this will likely fail β see next step)
ssh <ADMIN_USER>@openclaw-pi
Expected: This will probably time out or be refused. The Pi's UFW firewall only allows SSH from the specific IP address you configured during hardening. Tailscale connections arrive on a different interface (tailscale0) with a 100.x.y.z address, which the firewall blocks. This is fixed in the next step.
5. Allow SSH through the firewall for Tailscale
Where: SSH into the Pi from the isolated network (the old way, one last time).
ssh <ADMIN_USER>@<RASPBERRY_PI_IP>
Add a firewall rule that permits SSH only on the Tailscale interface:
sudo ufw allow in on tailscale0 to any port 22 proto tcp comment "SSH via Tailscale"
| Part | Meaning |
|---|---|
on tailscale0 |
Only applies to the Tailscale virtual network interface. Traffic from your physical network or any other source is unaffected. |
to any port 22 proto tcp |
Allow SSH connections (port 22, TCP). |
comment "SSH via Tailscale" |
Labels the rule for future reference. |
Why this is safe: The
tailscale0interface only carries traffic from devices authenticated on your Tailscale account. Nobody else can reach it.
Verify the rule was added:
sudo ufw status verbose
Expected: Your existing rules plus the new Tailscale SSH rule:
To Action From
-- ------ ----
22/tcp ALLOW IN <YOUR_LAPTOP_IP> # SSH from laptop (isolated network)
22/tcp on tailscale0 ALLOW IN Anywhere # SSH via Tailscale
22/tcp (v6) on tailscale0 ALLOW IN Anywhere (v6) # SSH via Tailscale
Now switch your Mac back to normal home WiFi and test:
ssh <ADMIN_USER>@openclaw-pi
Expected: Successful SSH login. You can now manage the Pi without switching WiFi networks.
6. Enable HTTPS and MagicDNS in the Tailscale admin console
Where: Your laptop's browser.
Tailscale Serve (which we'll set up next) requires both HTTPS certificates and MagicDNS to be enabled on your tailnet.
- Go to https://login.tailscale.com/admin/dns.
- Confirm MagicDNS is enabled. If not, toggle it on.
- Find HTTPS Certificates and enable it.
About the certificate transparency warning: When you enable HTTPS, Tailscale will show a notice that machine names (e.g.,
openclaw-pi.tail1234.ts.net) are recorded in a public certificate transparency log. This is standard for all HTTPS certificates across the internet. It reveals only that a device with that name exists β not your real name, IP address, location, or what the device does. No one can connect to your Pi using this information. It is safe to enable.
7. Set up Tailscale Serve (manual method)
Where: SSH into the Pi via Tailscale (from your normal home WiFi β no more switching!):
ssh <ADMIN_USER>@openclaw-pi
β οΈ Deviation from official docs: The OpenClaw documentation recommends setting
gateway.tailscale.mode: "serve"inopenclaw.jsonfor automatic Tailscale Serve management. On this setup, the automatic method does not work because theopenclawuser (which runs the gateway as a systemd user service) lacks the elevated permissions required to runtailscale serve. The manual approach below is used instead. This is functionally equivalent and avoids granting additional privileges to the restricted user.Note on config state: The Phase 4 hardened config sets
gateway.tailscale.mode: "off". After completing this phase, you do not need to change it β the manualtailscale serve --bgcommand handles everything independently. The config value remains"off"because the gateway is not managing Tailscale Serve itself.
First, confirm the gateway is running and responding locally:
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:<GATEWAY_PORT>/
Expected: 200 (or 401 β either means the gateway is alive).
If the gateway is not running, start it:
sudo -u openclaw XDG_RUNTIME_DIR=/run/user/$(id -u openclaw) systemctl --user restart openclaw-gateway
Now set up Tailscale Serve:
sudo tailscale serve --bg http://127.0.0.1:<GATEWAY_PORT>
| Part | Meaning |
|---|---|
sudo |
Tailscale Serve requires elevated permissions to configure. |
--bg |
Runs persistently in the background; survives reboots. |
http://127.0.0.1:<GATEWAY_PORT> |
Tells Tailscale to forward incoming HTTPS requests to the OpenClaw gateway on localhost. |
β οΈ Important β use
http://, nothttps+insecure://: The gateway serves plain HTTP on localhost. Usinghttps+insecure://causes a protocol mismatch that results in a502 Bad Gatewayerror. Thehttp://prefix only affects the local hop on the Pi itself (Tailscale process β OpenClaw process, entirely in memory). The connection from your Mac to the Pi is fully encrypted by Tailscale's WireGuard tunnel regardless of this setting.
Expected output:
Available within your tailnet:
https://openclaw-pi.<YOUR_TAILNET_NAME>.ts.net/
|-- proxy http://127.0.0.1:<GATEWAY_PORT>
Serve started and running in the background.
To disable the proxy, run: tailscale serve --https=443 off
Verify:
tailscale serve status
Expected: An active serve config showing the proxy target.
8. Find your tailnet name (if you don't have it)
If you didn't note your tailnet name in Step 1, retrieve it from your Mac's terminal:
tailscale status --json | grep MagicDNSSuffix
Expected output:
"MagicDNSSuffix": "<YOUR_TAILNET_NAME>.ts.net"
Alternatively, check https://login.tailscale.com/admin β the tailnet name is displayed at the top of the page.
9. Configure the admin user's CLI to connect to the gateway
Where: SSH session on the Pi (as the admin user).
The openclaw CLI installed under the admin user's home directory needs to know the gateway port and auth token to issue management commands (like approving device pairings).
9-pre. Install the OpenClaw CLI for the admin user
The openclaw binary was installed under the openclaw user's home directory and is not in the admin user's $PATH. Install it for your admin user too:
mkdir -p ~/.npm-global
npm config set prefix '~/.npm-global'
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
npm install -g openclaw@latest
Verify:
openclaw --version
If you prefer not to install it twice, you can use the full path instead: /home/openclaw/.npm-global/bin/openclaw. For example: /home/openclaw/.npm-global/bin/openclaw devices list.
9a. Find the gateway auth token
sudo grep '"token"' /home/openclaw/.openclaw/openclaw.json
Look for the line under the gateway.auth section (not the Telegram botToken or GITHUB_TOKEN). It will look like:
"token": "<YOUR_GATEWAY_TOKEN>"
9b. Configure the CLI
openclaw config set gateway.port <GATEWAY_PORT>
openclaw config set gateway.auth.token "<YOUR_GATEWAY_TOKEN>"
Why: The admin user has a separate config file (
/home/<ADMIN_USER>/.openclaw/openclaw.json) from the gateway's config (/home/openclaw/.openclaw/openclaw.json). The CLI needs the port and token to authenticate with the running gateway over its local WebSocket.
Verify the CLI can now connect:
openclaw devices list
Expected: A list of pending and/or paired devices (possibly empty if no devices have connected yet).
10. Connect the Control UI and approve the device
Where: Your Mac's browser (on normal home WiFi).
10a. Open the Control UI
Navigate to:
https://openclaw-pi.<YOUR_TAILNET_NAME>.ts.net/
10b. Authenticate
The UI will show a Gateway Token input field. Paste the gateway auth token (the same one from Step 9a) and click Connect.
The UI will likely display: disconnected (1008): pairing required. This is expected β the gateway requires one-time device pairing approval for new connections.
10c. Approve the device pairing
Back in your SSH session on the Pi:
openclaw devices list
You should see a pending pairing request from your browser. Note the <requestId>.
openclaw devices approve <requestId>
10d. Restart the gateway and reconnect
sudo -u openclaw XDG_RUNTIME_DIR=/run/user/$(id -u openclaw) systemctl --user restart openclaw-gateway
Refresh the Control UI page in your browser. It should now show as connected.
11. Create convenience aliases
The gateway management command is long. Create shortcuts by adding aliases to your shell configuration:
echo 'alias oc-restart="sudo -u openclaw XDG_RUNTIME_DIR=/run/user/\$(id -u openclaw) systemctl --user restart openclaw-gateway"' >> ~/.bashrc
echo 'alias oc-status="sudo -u openclaw XDG_RUNTIME_DIR=/run/user/\$(id -u openclaw) systemctl --user status openclaw-gateway"' >> ~/.bashrc
echo 'alias oc-logs="sudo -u openclaw XDG_RUNTIME_DIR=/run/user/\$(id -u openclaw) journalctl --user -u openclaw-gateway -n 50 --no-pager"' >> ~/.bashrc
source ~/.bashrc
| Alias | Command |
|---|---|
oc-restart |
Restarts the OpenClaw gateway |
oc-status |
Checks if the gateway is running |
oc-logs |
Shows the last 50 lines of gateway logs |
Test:
oc-status
Expected: active (running) in the output.
Note on log permissions: The
oc-logsalias reads the openclaw user's systemd journal. If it returnsNo journal files were opened due to insufficient permissions, the admin user may not have access to the openclaw user's journal. As a workaround, use:sudo journalctl _UID=$(id -u openclaw) -n 50 --no-pager
Verification
Confirm the following from your Mac on your normal home WiFi (not the isolated network):
| Test | Command / Action | Expected Result |
|---|---|---|
| Tailscale status | tailscale status (Mac terminal) |
Both Mac and openclaw-pi listed |
| SSH via Tailscale | ssh <ADMIN_USER>@openclaw-pi |
Successful login |
| Tailscale Serve active | tailscale serve status (Pi terminal) |
Proxy config shown |
| Control UI accessible | Open https://openclaw-pi.<YOUR_TAILNET_NAME>.ts.net/ |
UI loads and shows "connected" |
| Gateway health | oc-status (Pi terminal) |
active (running) |
| Network isolation intact | From Mac on home WiFi, try ping <RASPBERRY_PI_IP> (the GL.iNet IP) |
Should fail β isolation is working |
Access summary
After completing this phase, your access model is:
| Location | Method | Works? |
|---|---|---|
| Home WiFi | ssh <ADMIN_USER>@openclaw-pi (Tailscale) |
β |
| Isolated network (GL.iNET) | ssh <ADMIN_USER>@<RASPBERRY_PI_IP> (direct) |
β |
| Coffee shop, hotel, anywhere with internet | ssh <ADMIN_USER>@openclaw-pi (Tailscale) |
β |
| Any network (browser) | https://openclaw-pi.<YOUR_TAILNET_NAME>.ts.net/ |
β |
| Telegram (phone) | Send messages to the OpenClaw bot | β (unchanged) |
Troubleshooting
SSH via Tailscale times out or is refused
Cause: The Pi's UFW firewall is blocking connections on the tailscale0 interface.
Fix: Connect via the isolated network and add the firewall rule:
sudo ufw allow in on tailscale0 to any port 22 proto tcp comment "SSH via Tailscale"
Control UI shows ERR_CONNECTION_REFUSED
Cause: Tailscale Serve is not running.
Fix: SSH into the Pi and start it manually:
sudo tailscale serve --bg http://127.0.0.1:<GATEWAY_PORT>
Also verify that MagicDNS and HTTPS Certificates are both enabled at https://login.tailscale.com/admin/dns.
Control UI shows HTTP ERROR 502
Cause: Protocol mismatch β Tailscale Serve is connecting to the gateway using the wrong protocol.
Fix: Reset and reconfigure with http:// (not https+insecure://):
sudo tailscale serve --https=443 off
sudo tailscale serve --bg http://127.0.0.1:<GATEWAY_PORT>
Control UI shows disconnected (1008): pairing required
Cause: The browser is a new device that hasn't been approved by the gateway yet.
Fix:
- Make sure you've entered the gateway token in the Control UI's token field.
- In your SSH session:
openclaw devices list
openclaw devices approve <requestId>
- Restart the gateway and refresh the browser:
oc-restart
openclaw devices list fails with unauthorized: gateway token missing
Cause: The admin user's CLI config is missing the gateway port and/or auth token.
Fix:
openclaw config set gateway.port <GATEWAY_PORT>
openclaw config set gateway.auth.token "<YOUR_GATEWAY_TOKEN>"
Find the token with:
sudo grep '"token"' /home/openclaw/.openclaw/openclaw.json
sudo -u openclaw openclaw ... fails with Permission denied
Cause: OpenClaw was installed under the admin user's home directory. The restricted openclaw user cannot access binaries in another user's home directory (by design).
Fix: Run openclaw CLI commands as the admin user, not as the openclaw user. The CLI connects to the gateway over the local WebSocket β it doesn't need to run as the same user that owns the gateway process.
Kernel mismatch warning during Tailscale installation
Cause: A system update installed a newer kernel that hasn't been loaded yet.
Fix: Click OK to dismiss, then reboot the Pi after Tailscale installation completes:
sudo reboot
Phase 9 β Cost Management & Usage Monitoring
What this phase covers: Understanding how API costs work, why an always-on AI agent is fundamentally different from ChatGPT-style usage, and how to configure OpenClaw to keep your spending predictable.
Prerequisites: A fully working OpenClaw setup (Phases 0β8 complete). No additional hardware or software needed.
Why this matters: The guide so far covers security, networking, and configuration β but says nothing about cost. For beginners running an always-on AI agent on Claude Opus, this is a critical blind spot. A $5 experiment can quietly become a $200 month if you don't understand the mechanics.
1. Understanding the Cost Model
Every time you send a message to Claude through OpenClaw, two things are billed:
- Input tokens β everything sent to the model (your message, the conversation history, system prompt, workspace files)
- Output tokens β everything the model generates back (its reply, tool calls, reasoning)
One "token" is roughly ΒΎ of a word. The sentence "Hello, how are you today?" is about 7 tokens.
Current pricing (March 2026)
| Model | Input | Output | Best for |
|---|---|---|---|
| Claude Opus 4.6 | $5 / MTok | $25 / MTok | Complex reasoning, coding, main sessions |
| Claude Sonnet 4.6 | $3 / MTok | $15 / MTok | Balanced speed and intelligence |
| Claude Haiku 4.5 | $1 / MTok | $5 / MTok | Fast, simple tasks, sub-agents |
MTok = 1 million tokens. Prices are per million.
β οΈ API pricing changes over time. Always check Anthropic's pricing page for current rates.
Why this is different from ChatGPT
With ChatGPT, you pay a flat monthly subscription ($20/month) regardless of how much you use it. With the Anthropic API (which OpenClaw uses), you pay per token β the more you use, the more you pay. This gives you access to the full power of the model with no rate limits, but it means costs scale with usage.
2. How Input Tokens Accumulate
This is the single most important thing to understand about API costs: every message you send includes the entire conversation history.
Claude doesn't "remember" your previous messages. Each time you send a new message, OpenClaw packages up the entire conversation so far β every message you've sent, every response Claude gave, plus the system prompt and workspace files β and sends it all as input tokens.
This means input costs grow quadratically, not linearly:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HOW INPUT TOKENS GROW PER MESSAGE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Message 1: [system prompt + workspace files + user message] β
β βββββββββββββββββ 8,000 tokens βββββββββββββββββββ€ β
β Input cost: 8K tokens β
β β
β Message 2: [system + workspace + msg 1 + response 1 + msg 2] β
β βββββββββββββββββββ 12,000 tokens βββββββββββββββββ€ β
β Input cost: 12K tokens (not 4K!) β
β β
β Message 3: [system + workspace + msg 1-2 + responses 1-2 + msg 3]β
β βββββββββββββββββββββ 18,000 tokens βββββββββββββββ€ β
β Input cost: 18K tokens β
β β
β Message 10: [entire conversation history] β
β βββββββββββββββββββββββ 80,000+ tokens βββββββββββ€ β
β Input cost: 80K+ tokens β
β β
β β οΈ You pay for the FULL context every single message. β
β A 20-message session doesn't cost 20Γ one message β β
β it costs roughly 20 + 19 + 18 + ... + 1 = 210Γ one message. β
β (Triangular growth, not linear!) β
β β
β π Rough formula: total_input β n(n+1)/2 Γ avg_message_size β
β + n Γ base_context (system prompt + workspace files) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The takeaway: Long conversations are disproportionately expensive. A 30-message coding session costs far more than three 10-message sessions covering the same work. Use /new or /reset to start fresh sessions when the topic changes.
3. Where Your Tokens Go
Every single message β even just "hello" β carries a base context that OpenClaw sends alongside it. Here's what that looks like:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BASE CONTEXT BREAKDOWN (per message) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β System prompt (OpenClaw internals) ~2,000 tokens ββ β
β AGENTS.md ~1,500 tokens ββ β
β SOUL.md ~300 tokens β β
β USER.md ~250 tokens β β
β TOOLS.md ~1,000 tokens β β
β MEMORY.md ~1,200 tokens β β
β HEARTBEAT.md ~100 tokens β β
β Skills metadata ~500 tokens β β
β βββββββββββββββββββββββββββββββββββββββββββββββββ β
β Base total: ~6,850 tokens β
β β
β β οΈ This is sent with EVERY message, even "hello". β
β Larger workspace files = higher base cost. β
β Keep MEMORY.md and TOOLS.md lean. β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Practical implication: If your MEMORY.md grows to 5,000 tokens because you never prune it, that's an extra 5,000 input tokens on every single message. Over 100 messages in a day, that's 500K extra tokens β $2.50 on Opus just from one bloated file.
4. What Prompt Caching Does (and Why You Want It)
Anthropic offers prompt caching β a feature that dramatically reduces the cost of the repeated prefix (system prompt, workspace files, conversation history) that gets re-sent with every message.
Here's the mechanism: the first time a block of tokens is sent, it's written to a cache. On subsequent messages, the cached portion is read back at a fraction of the cost instead of being re-processed at full price.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β WITHOUT CACHING vs WITH CACHING β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β WITHOUT CACHING (every message pays full price): β
β β
β Msg 1: [ββββββββββββββββββββ] 8K tokens β $0.04 β
β Msg 2: [ββββββββββββββββββββ] 12K tokens β $0.06 β
β Msg 3: [ββββββββββββββββββββ] 18K tokens β $0.09 β
β Msg 4: [ββββββββββββββββββββ] 24K tokens β $0.12 β
β ββββββ β
β Total: $0.31 β
β β
β WITH CACHING (repeated prefix served from cache): β
β β
β Msg 1: [ββββββββββββββββββββ] 8K tokens β $0.05 (cache write) β
β Msg 2: [ββββββββββββββββββββ] 12K tokens β $0.02 (8K cached) β
β Msg 3: [ββββββββββββββββββββ] 18K tokens β $0.02 (12K cached) β
β Msg 4: [ββββββββββββββββββββ] 24K tokens β $0.02 (18K cached) β
β ββββββ β
β Total: $0.11 (65% savings!) β
β β
β Legend: ββββ = full-price tokens ββββ = cache-hit tokens β
β ββββ = unused context window β
β β
β Cache hits: $0.50 / MTok (90% cheaper than $5 / MTok base) β
β Cache writes (1h): $10 / MTok on first use, then saves on re-read β
β Cache writes (5m): $6.25 / MTok (shorter TTL, lower write cost) β
β Net effect: massive savings on long conversations. β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Enabling prompt caching in OpenClaw
SSH into your Pi and edit the OpenClaw configuration:
sudo -u openclaw nano /home/openclaw/.openclaw/openclaw.json
Add cacheRetention inside the agents.defaults block:
{
"agents": {
"defaults": {
"cacheRetention": "long"
}
}
}
The "long" setting uses Anthropic's 1-hour cache TTL. This means cached tokens persist for 1 hour between messages β ideal for conversations where you're actively going back and forth. The write cost is higher ($10/MTok vs $6.25/MTok for the 5-minute TTL), but the cache stays warm much longer, so you get more cache hits overall.
Restart the gateway for the change to take effect:
systemctl --user restart openclaw-gateway
5. OpenClaw Configuration for Cost Control
Beyond prompt caching, OpenClaw provides several configuration options to manage costs. All of these go in /home/openclaw/.openclaw/openclaw.json.
Model tiering
Use a powerful model for your main session and cheaper models for sub-agents (automated tasks the agent spawns):
{
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-6",
"subagentModel": "anthropic/claude-sonnet-4-6"
}
}
}
This means your conversations use Opus (the best model), but when the agent spawns background tasks β file analysis, code generation, research β those use Sonnet at 40% lower cost.
Context pruning
Long conversations accumulate tokens. Context pruning automatically trims older messages from the conversation when they fall outside a time window:
{
"agents": {
"defaults": {
"contextPruning": {
"mode": "cache-ttl",
"ttl": "1h"
}
}
}
}
This keeps the conversation focused and prevents runaway input costs on long sessions.
Heartbeat frequency
Heartbeats are periodic check-ins where OpenClaw's agent wakes up to see if anything needs attention. Each heartbeat is a full API call with the complete base context.
The default interval is roughly 30 minutes, which means ~48 API calls per day just for heartbeats. If cost is a concern, consider increasing the interval or disabling heartbeats when you don't need proactive monitoring.
Local model offloading
If you installed Ollama in Phase 3, you can route low-stakes tasks to local models at zero API cost:
- Heartbeat checks (simple "anything new?" polls)
- Basic lookups and formatting
- Draft generation before sending to a cloud model for refinement
This is configured per-task through OpenClaw's model routing, not as a global default. Consult the OpenClaw docs for the latest routing configuration options.
6. The Silent Cost Killers
These are the things that burn tokens without you noticing:
| Cost killer | Why it's expensive | What to do |
|---|---|---|
| Bloated workspace files | A 5,000-token MEMORY.md is sent with every single message | Prune regularly. Keep workspace files lean and relevant. |
| Frequent heartbeats | ~48 API calls/day at default 30-min interval, each carrying the full base context | Increase interval, use local models for heartbeats, or disable when not needed. |
| Sub-agents spawning sub-agents | Each gets its own full context and session | Use model tiering (Sonnet/Haiku for sub-agents). Monitor with openclaw status. |
| Long coding sessions | 30+ messages deep with tool outputs can hit 100K+ input tokens per message | Use /new to reset between distinct tasks. Break work into focused sessions. |
| Group chat history | Every message in a group chat adds to the context window | Keep group chats focused. Use DMs for extended conversations. |
| Tool outputs | File contents, command outputs, and search results all count as tokens | Be specific about what you ask for. "Show lines 1-50" beats "show me the whole file". |
7. Practical Cost Estimates
Here are rough monthly estimates for Claude Opus 4.6 usage through OpenClaw, with prompt caching enabled. These assume a base context of ~7K tokens per message.
| Usage pattern | Messages/day | Estimated monthly cost |
|---|---|---|
| Light β a few questions per day, short conversations | 5β10 | $5β15 |
| Moderate β daily conversations, occasional coding, heartbeats enabled | 20β40 | $20β60 |
| Heavy β extended coding sessions, sub-agents, research tasks, frequent heartbeats | 60β100+ | $60β200+ |
Without caching, multiply these estimates by roughly 2β3Γ.
With model tiering (Sonnet for sub-agents, Haiku for heartbeats), heavy usage can drop significantly β potentially 30β50% savings depending on how much work is offloaded.
π‘ Tip: Your actual costs depend heavily on conversation length, tool usage, and how often you reset sessions. The single biggest cost saver is keeping conversations short and using
/newbetween topics.
8. Monitoring Your Spending
Anthropic Console
Your primary cost-monitoring tool is the Anthropic Console:
- Log in at console.anthropic.com
- Navigate to Settings β Plans & Billing to see your current spend
- Check the Usage tab for a breakdown by model, showing input vs output vs cache token usage
Setting a monthly spend limit
This is strongly recommended for beginners:
- In the Anthropic Console, go to Settings β Plans & Billing β Spend Limits
- Set a monthly limit you're comfortable with (e.g., $50 for moderate usage)
- When you hit the limit, API calls will start returning errors β OpenClaw will stop working until the next billing cycle or until you raise the limit
This is your safety net. Set it before you start using OpenClaw daily.
OpenClaw's built-in status
You can check your current session's token usage directly through OpenClaw:
/status
This shows the current session's token consumption, model in use, and cost estimate. Use it periodically to calibrate your sense of how much different activities cost.
Phase 9 Checklist
Before moving on, confirm:
- You understand the difference between input and output tokens
- You understand why long conversations cost more than short ones (quadratic growth)
- Prompt caching is enabled (
cacheRetention: "long"inopenclaw.json) - You've set a monthly spend limit in the Anthropic Console
- You've considered model tiering for sub-agents (optional but recommended)
- You know how to check usage via the Anthropic Console and
/status