Understanding openclaw.json: The Config File, Section by Section
Nearly everything OpenClaw does is decided by one file: ~/.openclaw/openclaw.json. Which models run, who’s allowed to message you, what the agent may execute, when it wakes up on its own, and how exposed the whole thing is to your network.
It’s also a file most people never read properly. They let onboarding write it, poke at a key when something breaks, and never form a picture of the whole. That’s a shame, because the file has a clean structure once you see it — and the structure is the security model.
This page walks it top to bottom, then hands you a hardened baseline to copy.
First, the Format
The file is JSON5, not strict JSON. That means comments and trailing commas are legal:
{ // This is a real comment and it will not break anything agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" }, // trailing comma, fine }, },}Use the comments. Six weeks from now you will not remember why a particular user ID is in an allowlist, and a five-word note saves a genuinely annoying investigation.
Unquoted keys are also permitted, which is why documentation examples often look like JavaScript object literals rather than JSON. Both forms work.
Three Ways to Edit It
Directly. Open it in an editor. The Gateway watches the file and hot-reloads. Most changes apply with no downtime; some restart just their own subsystem — the affected channel, cron, the heartbeat, the health monitor.
The Control UI. openclaw dashboard opens http://127.0.0.1:18789, where a Config tab renders a form with channel schemas built in. This is the best option when you’re unsure of a key’s exact name or legal values, because the form only lets you pick real ones.
The CLI. Best for scripting and for changing one value without opening an editor:
openclaw config get agents.defaults.workspaceopenclaw config set agents.defaults.heartbeat.every "4h"openclaw config validateGet into the habit of running openclaw config validate after hand-editing. A stray brace in a hot-reloaded config produces confusing downstream behaviour, and validation catches it in a second.
The Two-Bucket Structure
This is the organising idea, and once you see it the file stops feeling arbitrary.
openclaw.json│├── INFRASTRUCTURE ── how the system runs; applies across everything│ ├── gateway ── port, bind address, auth, TLS│ ├── channels ── messaging platforms and access policy│ ├── session ── conversation isolation rules│ ├── messages ── group mention gating, reply visibility│ ├── cron ── scheduled job engine│ ├── hooks ── inbound webhook endpoint│ ├── plugins ── third-party extensions│ └── env ── environment variables and secrets│└── AGENT BEHAVIOUR ── what the assistant is and may do └── agents ├── defaults ── baseline for every agent └── entries ── per-agent overridesRoot-level keys are infrastructure and cross-agent defaults. Anything under agents is per-agent behaviour, with defaults providing the baseline and entries overriding it.
When you’re hunting for a setting, ask: is this about how the system runs, or about how the assistant behaves? That narrows it to one bucket immediately.
gateway — Where It Listens and Who Gets In
The highest-stakes section in the file.
{ gateway: { mode: "local", bind: "loopback", port: 18789, auth: { mode: "token", token: "LONG_RANDOM_TOKEN_HERE" }, reload: { mode: "hybrid" } }}bind decides network reachability:
| Value | Reachable from | Auth needed |
|---|---|---|
loopback (default) | This host only | No |
lan | Local network | Yes |
tailnet | Your Tailscale network | Yes |
custom | Whatever you specify | Yes |
Leave this on loopback. The Control UI is an administrative surface — it can read your config, view session transcripts, and change agent behaviour. Treat reachability the way you’d treat a database admin console.
When you genuinely need phone access from outside the house, the right answer is a tailnet or an SSH tunnel, not a LAN bind and certainly not a forwarded port. Covered in Remote Access and Day-2 Operations.
auth.mode offers token (a shared bearer token — the recommended option), password (supplied via the OPENCLAW_GATEWAY_PASSWORD environment variable), and trusted-proxy (identity headers from a reverse proxy in front). Any bind other than loopback requires auth. Generate a real token, not something you invented:
openssl rand -hex 32channels — Who Can Talk to Your Assistant
Where you spend most of your configuration time. Each platform gets its own block, with shared fallbacks under channels.defaults.
{ channels: { defaults: { groupPolicy: "allowlist", implicitMentions: { replyToBot: true, quotedBot: true, threadParticipation: true } }, telegram: { botToken: "123456:ABC-...", dmPolicy: "pairing", allowFrom: ["tg:123456789"], groups: { "*": { requireMention: true } } }, whatsapp: { dmPolicy: "pairing", allowFrom: ["+15555550123"], groupPolicy: "allowlist", groupAllowFrom: ["+15551234567"] } }}dmPolicy is the front door:
| Value | Behaviour |
|---|---|
pairing (default) | Unknown sender gets a one-time code; you approve it explicitly |
allowlist | Only senders in allowFrom; no handshake offered to anyone else |
open | Everyone (requires an explicit allowFrom: ["*"] — deliberately awkward) |
disabled | Ignore DMs entirely |
pairing is a genuinely good default: an unknown sender can request access, but nothing happens until you run the approval command. Codes expire after an hour and pending requests are capped at three per account, so a stranger who discovers your bot can’t spam approval prompts at you.
Use allowlist when you know exactly who should have access and want no handshake at all. Avoid open unless you have thought carefully about what a stranger can make your agent do — which, if exec is available, is a great deal.
groupPolicy governs rooms and group chats: allowlist (default), open, or disabled. And requireMention keeps the bot quiet unless addressed — leave it on for any group with real traffic.
implicitMentions is a small setting with an outsized effect on how natural the bot feels. Replying to its message, quoting it, or being in an active thread with it all count as addressing it. Without this you have to type @bot on every line, which makes conversation stilted.
Peer identifiers differ by platform — Telegram and Discord use numeric user IDs, Slack uses user:U12345, WhatsApp uses a phone number or JID, Matrix uses the full @user:server form. Get these from the Control UI rather than guessing.
Full detail: Connecting WhatsApp and Telegram and Slack, Discord and Teams.
session — Who Shares Context With Whom
Small section, large consequences.
{ session: { dmScope: "per-channel-peer", threadBindings: { enabled: true } }}dmScope: "per-channel-peer" gives every sender on every channel an isolated conversation. Without it, DMs can funnel into one shared session — convenient when you’re the only user, a genuine privacy problem the moment you’re not.
The rule is simple: the instant a second person can DM your bot, this setting is mandatory. It appears in the project’s own hardened baseline for exactly that reason.
threadBindings ties a conversation to a platform thread, so a Slack thread keeps its own continuous context instead of bleeding into the channel’s.
agents — What Your Assistant Is
The behavioural half of the file.
{ agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" }, workspace: "~/openclaw-workspace", heartbeat: { every: "4h" }, sandbox: { mode: "all", scope: "agent", workspaceAccess: "rw" } }, entries: { research: { tools: { allow: ["read", "web_fetch", "web_search"], deny: ["exec", "write"] }, sandbox: { mode: "all", workspaceAccess: "ro" } }, ops: { tools: { allow: ["read", "exec"] } } } }}model.primary is the workhorse model. Model choice is a security decision as much as a quality one — the documentation is explicit that for tool-enabled agents, or agents that read untrusted content, prompt-injection risk with older or smaller models is often unacceptably high. Saving money on the model that holds your shell access is a false economy.
workspace is where the agent’s memory files and working documents live. Point it somewhere you’re happy to have written to, and consider putting it under version control — a git log of your assistant’s memory is a surprisingly useful audit trail.
heartbeat.every controls proactive wake-ups. Start at "4h". Every 30 minutes is ~1,400 model invocations a month, each carrying a system prompt and accumulated context, and it is the most common cause of bill shock.
sandbox decides whether tools run in a container. mode: "all" turns it on, scope: "agent" isolates per agent, and workspaceAccess takes "none", "ro", or "rw". See Sandboxing OpenClaw.
Notice what the research entry does: read and fetch, no exec, no write, read-only workspace. It’s the agent you point at untrusted content, and its capability floor means injected instructions have nowhere to go. That’s the single most useful pattern in this file.
tools — The Enforcement Layer
{ tools: { profile: "messaging", deny: ["group:automation", "group:runtime", "group:fs"], exec: { security: "deny", ask: "always" } }}profile selects a pre-built set — messaging is a deliberately narrow one suited to an assistant that chats and reads but doesn’t reshape your filesystem.
allow and deny work individually or by group (group:fs, group:runtime, group:automation). Deny is the safer instrument: denying a group covers tools added by future updates that you haven’t heard of yet.
exec.ask: "always" means every command execution pauses for your approval. Noisy, and worth living with while you learn what your agent actually tries to do. Most people are surprised at least once.
This section is the reason tool policy works at all: it is enforced by the Gateway, outside the model. A model that has been socially engineered into wanting to run something still cannot, if the tool isn’t permitted. Instructions in a system prompt are a preference; this is a wall.
cron, hooks, messages, plugins, env
{ cron: { enabled: true, sessionRetention: "24h" }, hooks: { enabled: true, token: "shared-secret", path: "/hooks" }, messages: { visibleReplies: "automatic", groupChat: { requireMention: true } }, env: { OPENROUTER_API_KEY: "sk-or-..." }}cron runs scheduled jobs. sessionRetention stops scheduled runs from accumulating sessions indefinitely.
hooks exposes an HTTP endpoint external systems can call. The token is a shared secret — treat it like a password, because anything that can call this endpoint can wake your agent.
messages holds cross-channel conversational defaults: mention gating and how visibly the agent replies.
plugins loads third-party extensions and custom providers. Third-party plugins run with your Gateway’s privileges — apply the scrutiny you’d apply to any dependency with filesystem access.
env holds environment variables and secrets. Which is a good moment for a reminder: this file contains plaintext credentials.
chmod 700 ~/.openclawchmod 600 ~/.openclaw/openclaw.jsonAnd if you version-control your config — a good idea — commit a redacted copy. Never the live one.
A Hardened Starting Configuration
Copy this, adjust the identifiers, and widen deliberately from here. It is deliberately restrictive: locked-down front door, no execution, isolated sessions.
{ // ── Infrastructure ────────────────────────────── gateway: { mode: "local", bind: "loopback", // do not widen without reading the threat model auth: { mode: "token", token: "REPLACE_WITH_openssl_rand_hex_32" } },
session: { dmScope: "per-channel-peer" // mandatory once anyone else can DM you },
// ── Capability floor ──────────────────────────── tools: { profile: "messaging", deny: ["group:automation", "group:runtime", "group:fs"], exec: { security: "deny", ask: "always" } },
// ── Who gets in ───────────────────────────────── channels: { defaults: { groupPolicy: "allowlist" }, telegram: { botToken: "REPLACE_ME", dmPolicy: "pairing", allowFrom: ["tg:YOUR_NUMERIC_ID"], groups: { "*": { requireMention: true } } } },
// ── Agent behaviour ───────────────────────────── agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" }, workspace: "~/openclaw-workspace", heartbeat: { every: "4h" } // cost control: start slow, tighten later } },
cron: { enabled: true, sessionRetention: "24h" }}Start here. Add exec when you have a specific job that needs it, and preferably to a dedicated agent rather than to defaults. Expanding from a narrow base is straightforward; retracting from a wide one after something has gone wrong is not.
Editing Habits Worth Having
Validate after every hand-edit.
openclaw config validateAudit before any change to network exposure.
openclaw security audit --deepBack up before upgrades. Config keys do change between releases on a project moving this fast.
tar czf openclaw-backup-$(date +%F).tar.gz ~/.openclawThat archive holds API keys and channel credentials — store it accordingly.
Change one thing at a time. Hot reload makes it tempting to rewrite three sections at once. When behaviour then changes in a way you didn’t expect, you’ve lost the ability to say which edit caused it.
Where to Go Next
You now have a map of the file and a defensible baseline running.
Next: Connecting WhatsApp and Telegram covers linking real messaging accounts, the pairing flow in practice, and the mistakes that lock you out of your own bot. If you’d rather understand the risk model before opening more doors, skip ahead to The OpenClaw Threat Model — it’s the page that makes every other setting on this one make sense.