openclaw policy CLI Reference: Enterprise Conformance Checks
Reference for the openclaw policy command, which provides enterprise conformance checks over OpenClaw settings. Ideal for teams needing durable assertions and drift detection.
Read this when
- You want to check OpenClaw settings against an authored policy.jsonc
- You want policy findings in doctor lint
- You need a policy attestation hash for audit evidence
openclaw policy
The bundled Policy plugin supplies openclaw policy. It acts as an enterprise conformance layer over existing OpenClaw settings, not as a separate configuration system. Requirements are authored in policy.jsonc; OpenClaw treats the active workspace as evidence; drift is reported through doctor --lint. Policy neither enforces tool calls nor alters runtime behavior at request time, nor does it attest per-agent credential stores such as openclaw-agent.sqlite.
Policy examines configured channels, MCP servers, model providers, network SSRF posture, ingress/channel access, Gateway exposure and node command posture, authored message-routing probes, agent workspace access, sandbox posture, data-handling posture, secret provider/auth profile posture, and governed tool metadata (the ## Tools section of AGENTS.md). Deploy it when a workspace requires a durable, checkable assertion like "Telegram must not be enabled" or "governed tools must declare risk and owner metadata." If only local behavior is needed without attestation or drift detection, plain config suffices.
Separately, openclaw agent exec applies an isolated implicit policy config per run: the agent sandbox is off, Gateway-host execution is fully allowed, and filesystem tools are limited to --cwd.
Quick start
openclaw plugins enable policy
The plugin remains active even when policy.jsonc is absent, allowing doctor to report the missing artifact rather than silently skipping checks.
Hand-author policy.jsonc; it is never generated from current settings. Each top-level section acts as a rule namespace: a check runs only when a concrete rule exists under it (unsupported sections or keys fail as policy/policy-jsonc-invalid instead of being silently ignored). Minimal example covering every supported section:
{
"channels": {
"denyRules": [
{
"id": "no-telegram",
"when": { "provider": "telegram" },
"reason": "Telegram is not approved for this workspace.",
},
],
},
"mcp": {
"servers": {
"allow": ["docs"],
"deny": ["untrusted"],
},
},
"models": {
"providers": {
"allow": ["openai", "anthropic"],
"deny": ["openrouter"],
},
},
"network": {
"privateNetwork": {
"allow": false,
},
},
"routing": {
"requireBindings": true,
"requireConfiguredChannels": true,
"probes": [
{
"id": "family-dm",
"route": {
"channel": "imessage",
"peer": { "kind": "direct", "id": "+15555550123" },
},
"expect": {
"agentId": "family",
"matchedBy": ["binding.peer"],
},
},
],
},
"ingress": {
"session": {
"requireDmScope": "per-channel-peer",
},
"channels": {
"allowDmPolicies": ["pairing", "allowlist", "disabled"],
"denyOpenGroups": true,
"requireMentionInGroups": true,
},
},
"gateway": {
"exposure": {
"allowNonLoopbackBind": false,
"allowTailscaleFunnel": false,
},
"auth": {
"requireAuth": true,
"requireExplicitRateLimit": true,
},
"controlUi": {
"allowInsecure": false,
},
"remote": {
"allow": false,
},
"http": {
"denyEndpoints": ["chatCompletions", "responses"],
"requireUrlAllowlists": true,
},
"nodes": {
"denyCommands": ["system.run"],
},
},
"agents": {
"workspace": {
"allowedAccess": ["none", "ro"],
"denyTools": ["exec", "process", "write", "edit", "apply_patch"],
},
},
"dataHandling": {
"sensitiveLogging": {
"requireRedaction": true,
},
"telemetry": {
"denyContentCapture": true,
},
"retention": {
"requireSessionMaintenance": true,
},
"memory": {
"denySessionTranscriptIndexing": true,
},
},
"secrets": {
"requireManagedProviders": true,
"denySources": ["exec"],
"allowInsecureProviders": false,
},
"auth": {
"profiles": {
"requireMetadata": ["provider", "mode"],
"allowModes": ["api_key", "token"],
},
},
"execApprovals": {
"requireFile": true,
"defaults": { "allowSecurity": ["deny"] },
"agents": {
"allowSecurity": ["deny", "allowlist"],
"allowAutoAllowSkills": false,
"allowlist": { "expected": ["deploy", "status"] },
},
},
"tools": {
"requireMetadata": ["risk", "sensitivity", "owner"],
"profiles": {
"allow": ["messaging", "minimal"],
},
"fs": {
"requireWorkspaceOnly": true,
},
"exec": {
"allowSecurity": ["deny", "allowlist"],
"requireAsk": ["always"],
"allowHosts": ["sandbox"],
},
"elevated": {
"allow": false,
},
"denyTools": ["group:runtime", "group:fs"],
},
}
Cross-cutting notes not obvious from the rule tables below:
- If
gateway.bindis omitted while denying non-loopback binds, you accept the runtime default; setgateway.bind: "loopback"for strict conformance. - For a read-only agent, set sandbox
modetoallornon-mainon the applicable defaults/agent andworkspaceAccesstononeorro. Missing oroffsandbox mode does not satisfy a read-only policy. agents.workspace.denyToolsacceptsexec,process,write,edit,apply_patch. The config tool-deny groupsgroup:fs(file mutation) andgroup:runtime(shell/process) satisfy the equivalent posture.- Exec-approvals checks read the live SQLite approvals document only when an
execApprovalsrule is present; a missing or invalid artifact is unobservable evidence, not a synthetic pass. - Secret and auth-profile evidence records provider/source posture and SecretRef metadata only, never raw values. Policy does not read or attest per-agent credential stores such as
openclaw-agent.sqlite. - Data-handling evidence is config-level posture (telemetry capture toggle, session maintenance mode, transcript-indexing setting) plus the always-on log redaction invariant. It does not inspect logs, telemetry exports, transcripts, or memory files, and a clean result does not prove that no personal data or secrets exist in them.
- Routing probes reuse OpenClaw's runtime binding resolver. Routing evidence records only the probe id, resolved agent, match kind, and redacted binding metadata. It never records peer, account, guild, team, or role identifiers. Adding a routing section intentionally changes the policy and attestation hashes; policies without routing keep their existing evidence shape.
Policy rule reference
Every rule below is optional; a check runs only when the rule is present. The observed state is existing OpenClaw config or workspace metadata.
Scoped overlays
Use scopes.<scopeName> when specific agents or channels need stricter policy than the top-level baseline. The scope name is just a label; matching uses the selector inside the scope. Overlays are additive: the global rule still runs, and the scoped rule can add its own finding against the same evidence.
| Selector | Supported sections | Use when |
|---|---|---|
agentIds | tools, agents.workspace, sandbox, dataHandling.memory, execApprovals | One or more runtime agents need stricter rules. |
channelIds | ingress.channels | One or more channels need stricter ingress rules. |
If an agentIds entry is not present in agents.entries.*, OpenClaw evaluates the scoped rule against inherited global/default posture for that runtime agent id instead of skipping it.
{
"tools": {
"exec": {
"allowHosts": ["sandbox", "node"],
},
},
"sandbox": {
"requireMode": ["all", "non-main"],
},
"scopes": {
"release-workspace": {
"agentIds": ["release-agent", "review-agent"],
"agents": {
"workspace": {
"allowedAccess": ["none", "ro"],
},
},
},
"release-lockdown": {
"agentIds": ["release-agent"],
"tools": {
"exec": {
"allowHosts": ["sandbox"],
"allowSecurity": ["deny", "allowlist"],
"requireAsk": ["always"],
},
"denyTools": ["exec", "process", "write", "edit", "apply_patch"],
},
"sandbox": {
"requireMode": ["all"],
"allowBackends": ["docker"],
},
"dataHandling": {
"memory": {
"denySessionTranscriptIndexing": true,
},
},
},
"shell-sandbox": {
"agentIds": ["shell-agent"],
"sandbox": {
"allowBackends": ["openshell"],
"containers": {
"requireReadOnlyMounts": false,
},
},
},
"telegram-ingress": {
"channelIds": ["telegram"],
"ingress": {
"channels": {
"allowDmPolicies": ["pairing"],
"denyOpenGroups": true,
"requireMentionInGroups": true,
},
},
},
},
}
The same agent can appear in multiple scopes if each scope governs a different field, as above. A repeated scoped field for the same agent must be equally or more restrictive; a weaker duplicate claim is rejected (allow-lists are subsets, deny-lists are supersets, required booleans are fixed).
Container posture rules (sandbox.containers.*) are checked only against evidence the matched agent's sandbox backend can expose. The Docker and Podman backends expose the same sandbox.docker.* container posture settings. If a backend cannot observe a rule you enabled for it, policy reports policy/sandbox-container-posture-unobservable instead of passing; scope container rules to the agent groups that use a backend which can expose them.
Backend authorization uses the configured identity. backend: "docker" requires allowBackends: ["docker"], while backend: "podman" requires allowBackends: ["podman"].
Top-level ingress.session.requireDmScope stays global; session.dmScope is not channel-attributable evidence, so it cannot be scoped by channelIds.
Every scope present in policy.jsonc must be valid and enforceable.
Channels
| Policy field | Observed state | Use when |
|---|---|---|
channels.denyRules[].when.provider | channels.* provider and enabled state | Block channels coming from a provider like telegram. |
channels.denyRules[].reason | Finding message and repair hint context | Clarify the reasoning behind the provider denial. |
MCP servers
| Policy field | Observed state | Use when |
|---|---|---|
mcp.servers.allow | mcp.servers.* ids | Force every MCP server that is configured to sit on an allowlist. |
mcp.servers.deny | mcp.servers.* ids | Reject particular MCP server ids that are configured. |
Model providers
| Policy field | Observed state | Use when |
|---|---|---|
models.providers.allow | models.providers.* ids and selected model refs | Ensure configured providers and chosen model refs stick to approved providers. |
models.providers.deny | models.providers.* ids and selected model refs | Refuse configured providers and chosen model refs according to provider id. |
Network
| Policy field | Observed state | Use when |
|---|---|---|
network.privateNetwork.allow | Private-network SSRF escape hatches | Assign false to keep private-network access turned off. |
Message routing
| Policy field | Observed state | Use when |
|---|---|---|
routing.requireBindings | Channel route bindings, excluding ACP bindings | Demand a minimum of one message-routing binding. |
routing.requireConfiguredChannels | Binding channel ids and configured channels.* ids | Spot binding channel ids that are outdated or mistyped. |
routing.probes[].route | The public OpenClaw route resolver | Show a typical inbound route without dispatching a message. |
routing.probes[].expect.agentId | Resolved agent id | Force the route to land on the agent under review. |
routing.probes[].expect.matchedBy | Resolver match kind | Demand peer, account, channel, or other reviewed binding specificity. |
Probe ids must not repeat. A route accommodates channel, optional accountId,
peer, parentPeer, guildId, teamId, and memberRoleIds. Peer kinds are
direct, group, and channel. matchedBy can hold one or more runtime
match kinds, such as binding.peer, binding.account, binding.channel,
or default.
Routing checks serve as conformance checks only. They leave startup, message delivery, binding precedence, and fallback behavior untouched. Findings call for operator review, since a binding changed automatically could redirect private messages.
Ingress and channel access
| Policy field | Observed state | Use when |
|---|---|---|
ingress.session.requireDmScope | session.dmScope | Demand a reviewed isolation scope for direct messages. |
ingress.channels.allowDmPolicies | channels.*.dmPolicy and legacy channel DM policy fields | Permit only reviewed direct-message channel policies. |
ingress.channels.denyOpenGroups | Channel, account, and group ingress policy | Block open group ingress for the configured channels and accounts. |
ingress.channels.requireMentionInGroups | Channel, account, group, guild, and nested mention gate config | Require mention gates whenever group ingress is open or gated by mentions. |
Gateway
| Policy field | Observed state | Use when |
|---|---|---|
gateway.exposure.allowNonLoopbackBind | gateway.bind | Assign false to enforce loopback Gateway binding. |
gateway.exposure.allowTailscaleFunnel | Tailscale serve/funnel Gateway posture | Assign false to block Tailscale Funnel exposure. |
gateway.auth.requireAuth | gateway.auth.mode | Assign true to refuse disabled Gateway auth. |
gateway.auth.requireExplicitRateLimit | gateway.auth.rateLimit | Assign true to mandate explicit auth rate-limit configuration. |
gateway.controlUi.allowInsecure | Device-identity invariant and origin fallback | Assign false to mandate device identity and forbid Host-header origin fallback. |
gateway.remote.allow | Remote Gateway mode/config | Assign false to forbid remote Gateway mode. |
gateway.http.denyEndpoints | Gateway HTTP API endpoints | Reject endpoint ids like chatCompletions or responses. |
gateway.http.requireUrlAllowlists | Gateway HTTP URL-fetch inputs | Assign true to mandate URL allowlists on URL-fetch inputs. |
gateway.nodes.denyCommands | gateway.nodes.commands.deny | Mandate exact node command ids such as system.run to be denied in OpenClaw config. |
gateway.nodes.denyCommands functions as an exact, case-sensitive policy deny-superset rule.
Deploy it when policy must demonstrate that privileged node commands are
explicitly denied via OpenClaw config. A deployment that deliberately permits
a privileged node command should revise policy.jsonc after review rather than
depending on gateway.nodes.commands.allow alone.
Agent workspace
| Policy field | Observed state | Use when |
|---|---|---|
agents.workspace.allowedAccess | agents.defaults.sandbox.workspaceAccess and agents.entries.*.sandbox.workspaceAccess | Permit only sandbox workspace access values like none or ro. |
agents.workspace.denyTools | Global and per-agent tool deny config | Require mutation tools (exec, process, write, edit, apply_patch) to be denied. |
Sandbox posture
| Policy field | Observed state | Use when |
|---|---|---|
sandbox.requireMode | agents.defaults.sandbox.mode and per-agent mode | Permit only reviewed sandbox modes such as all or non-main. |
sandbox.allowBackends | agents.defaults.sandbox.backend and per-agent backend | Permit only reviewed sandbox backends such as docker or podman. |
sandbox.containers.denyHostNetwork | Container-backed sandbox/browser network mode | Forbid host network mode. |
sandbox.containers.denyContainerNamespaceJoin | Container-backed sandbox/browser network mode | Forbid joining another container network namespace. |
sandbox.containers.requireReadOnlyMounts | Container-backed sandbox/browser mount mode | Require mounts to be read-only. |
sandbox.containers.denyContainerRuntimeSocketMounts | Container-backed sandbox/browser mount targets | Forbid container runtime socket mounts. |
sandbox.containers.denyUnconfinedProfiles | Container security profile posture | Forbid unconfined container security profiles. |
sandbox.browser.requireCdpSourceRange | Sandbox browser CDP source range | Require browser CDP exposure to declare a source range. |
Policy treats missing sandbox.mode as its implicit default off, so
sandbox.requireMode reports a fresh or unconfigured sandbox as outside an
allowlist such as ["all"].
Data Handling
| Policy field | Observed state | Use when |
|---|---|---|
dataHandling.sensitiveLogging.requireRedaction | Runtime invariant oc://openclaw.invariant/logging/redaction | Set to true to record the requirement; OpenClaw always satisfies it. |
dataHandling.telemetry.denyContentCapture | diagnostics.otel.captureContent | Set to true to reject telemetry content capture. |
dataHandling.retention.requireSessionMaintenance | session.maintenance.mode | Set to true to require effective session maintenance mode enforce. |
dataHandling.memory.denySessionTranscriptIndexing | memory.search.experimental.sessionMemory, memory.search.rememberAcrossConversations, and per-agent overrides | Set to true to reject session transcript indexing into memory. |
Secrets
| Policy field | Observed state | Use when |
|---|---|---|
secrets.requireManagedProviders | Config SecretRefs and secrets.providers.* declarations | Set to true to require SecretRefs to point at declared providers. |
secrets.denySources | Secret provider sources and SecretRef sources | Deny sources such as exec, file, or another configured source name. |
secrets.allowInsecureProviders | Insecure secret-provider posture flags | Set to false to reject providers that opt into insecure posture. |
Exec approvals
Exec-approvals checks read the runtime exec_approvals_config singleton row in
~/.openclaw/state/openclaw.sqlite by default, or the same database under
$OPENCLAW_STATE_DIR/state when OPENCLAW_STATE_DIR is set. Findings keep the
stable oc://exec-approvals.json/... URI scheme; it now denotes paths within
the authoritative JSON document stored in that row.
Posture rules under execApprovals.defaults.* or execApprovals.agents.*
require readable artifact evidence; a missing or invalid artifact reports as
unobservable evidence rather than a best-effort pass. Once readable, omitted
fields inherit runtime defaults: missing defaults.security is full, and
missing agent security inherits that default. Evidence includes defaults,
agents.*, agents.*.allowlist[].pattern, optional argPattern, effective
autoAllowSkills posture, and entry source, never socket path/token,
commandText, lastUsedCommand, resolved paths, or timestamps.
| Policy field | Observed state | Use when |
|---|---|---|
execApprovals.requireFile | Active runtime exec_approvals_config row | Set to true to require the approvals document to exist and parse. |
execApprovals.defaults.allowSecurity | defaults.security, defaulting to full | Allow only approved default approval security modes. |
execApprovals.agents.allowSecurity | agents.*.security, inheriting defaults | Allow only approved per-agent effective approval security modes. |
execApprovals.agents.allowAutoAllowSkills | defaults.autoAllowSkills and agents.*.autoAllowSkills, inheriting runtime defaults | Set to false to require strict manual allowlists without implicit skill CLI approval. |
execApprovals.agents.allowlist.expected | Aggregate agents.*.allowlist[] pattern and optional argPattern entries | Require the approvals allowlist to match the reviewed pattern set. |
Example: require the approvals artifact, deny permissive defaults, and allow only reviewed exec approval posture for selected agents.
{
"execApprovals": {
"requireFile": true,
"defaults": {
// Security modes: "deny", "allowlist", or "full".
// This default permits only the locked-down deny posture.
"allowSecurity": ["deny"],
},
},
"scopes": {
"restricted-shell": {
"agentIds": ["family-agent", "groups-agent"],
"execApprovals": {
"agents": {
// Selected agents may use reviewed allowlist posture, but not "full".
"allowSecurity": ["allowlist"],
// false means skill CLIs must appear in the reviewed allowlist instead of
// being implicitly approved by autoAllowSkills.
"allowAutoAllowSkills": false,
"allowlist": {
"expected": [
// Simple entry: exact reviewed executable pattern with no argPattern.
"travel-hub",
// Constrained entry: pattern plus reviewed argument regex.
{ "pattern": "calendar-cli", "argPattern": "^sync\\b" },
"/bin/date",
],
},
},
},
},
},
}
Auth profiles
| Policy field | Observed state | Use when |
|---|---|---|
auth.profiles.requireMetadata | auth.profiles.* provider and mode metadata | Demand metadata keys like provider and mode on config auth profiles. |
auth.profiles.allowModes | auth.profiles.*.mode | Restrict auth profile modes to supported ones such as api_key, aws-sdk, oauth, or token. |
Tool metadata
| Policy field | Observed state | Use when |
|---|---|---|
tools.requireMetadata | Governed AGENTS.md tool declarations | Make governed tools declare metadata keys such as risk, sensitivity, or owner. |
Tool posture
| Policy field | Observed state | Use when |
|---|---|---|
tools.profiles.allow | tools.profile and agents.entries.*.tools.profile | Permit only tool profile ids such as minimal, messaging, or coding. |
tools.fs.requireWorkspaceOnly | tools.fs.workspaceOnly and per-agent tools.fs overrides | Configure true to enforce a workspace-only filesystem tool posture. |
tools.exec.allowSecurity | tools.exec.security and per-agent exec security | Allow exec security modes only like deny or allowlist. |
tools.exec.requireAsk | tools.exec.ask and per-agent exec ask mode | Enforce an approval posture such as always. |
tools.exec.allowHosts | tools.exec.host and per-agent exec host routing | Limit exec host routing modes to those like sandbox. |
tools.elevated.allow | tools.elevated.enabled and per-agent elevated posture | Set false to keep elevated tool mode disabled. |
tools.alsoAllow.expected | tools.alsoAllow and per-agent tools.alsoAllow | Mandate precise alsoAllow entries, flagging missing or unexpected additive tool grants. |
tools.denyTools | tools.deny and agents.entries.*.tools.deny | Ensure configured tool deny lists cover tool ids or groups such as group:runtime and group:fs. |
Run checks
Execute policy-only checks while authoring:
openclaw policy check
openclaw policy check --agent ops
openclaw policy check --json
openclaw policy check --severity-min error
With policy check, only the policy check set runs, producing evidence, findings,
and attestation hashes. When the Policy plugin is enabled, those findings
surface in openclaw doctor --lint as well.
For a multi-agent fleet with explicit ownership, supply --agent <id> so the
command pulls governed declarations and policy.jsonc from that agent's
workspace. Without the flag, a sole-agent or retained legacy-owner setup
still resolves; OpenClaw never picks an arbitrary first agent.
Compare an operator policy file to an authored baseline:
openclaw policy compare --baseline official.policy.jsonc
openclaw policy compare --baseline official.policy.jsonc --agent ops
openclaw policy compare --baseline official.policy.jsonc --policy policy.jsonc --json
policy compare validates policy-file syntax against itself; runtime state, evidence, credentials, and secrets are not inspected. The same rule metadata governing scoped overlays applies here: allowlists must remain identical or tighter, denylists identical or wider, required booleans unchanged, ordered strings only shifting toward the stricter end of the configured sequence, and exact lists matching exactly. The baseline may be an organization-authored policy; the checked policy can introduce stricter values or additional rules. A top-level checked rule can fulfill a scoped baseline rule when it is equally or more restrictive. Scope names across files need not align; comparison keys on selector (agentIds/channelIds) and field. For routing probes, every baseline probe id must stay attached to the same route and expected agent. A checked policy may add probes or tighten matchedBy, yet removing a probe, altering its route or agent, or broadening its accepted match kinds counts as weaker.
When the checked policy path originates from the plugin configuration and is relative, --agent <id> picks the workspace for resolution. Absolute policy paths function independently of an agent workspace.
Clean compare (--json):
{
"ok": true,
"baselinePath": "official.policy.jsonc",
"policyPath": "policy.jsonc",
"rulesChecked": 3,
"findings": []
}
Clean policy check --json output provides stable hashes that an operator or supervisor can note down:
{
"ok": true,
"attestation": {
"policy": {
"path": "policy.jsonc",
"hash": "sha256:..."
},
"workspace": {
"scope": "policy",
"hash": "sha256:..."
},
"findingsHash": "sha256:...",
"attestationHash": "sha256:..."
},
"checksRun": 5,
"checksSkipped": 0,
"findings": []
}
Configure policy
Policy configuration sits under plugins.entries.policy.config.
{
"plugins": {
"entries": {
"policy": {
"enabled": true,
"config": {
"enabled": true,
"path": "policy.jsonc",
"workspaceRepairs": false,
"expectedHash": "sha256:...",
"expectedAttestationHash": "sha256:...",
},
},
},
},
}
| Setting | Purpose |
|---|---|
enabled | Activate policy checks even before policy.jsonc is present. |
workspaceRepairs | Permit doctor --fix to modify policy-managed workspace settings. |
expectedHash | Optional hash-lock for the approved policy artifact. |
expectedAttestationHash | Optional hash-lock for the last accepted clean policy check. |
path | Workspace-relative path to the policy artifact. |
Assign plugins.entries.policy.config.enabled the value false to turn off policy checks for a workspace while keeping the plugin installed.
Accept policy state
Example JSON output:
{
"ok": true,
"attestation": {
"checkedAt": "2026-05-10T20:00:00.000Z",
"policy": {
"path": "policy.jsonc",
"hash": "sha256:..."
},
"workspace": {
"scope": "policy",
"hash": "sha256:..."
},
"findingsHash": "sha256:...",
"attestationHash": "sha256:..."
},
"evidence": {
"channels": [
{
"id": "telegram",
"provider": "telegram",
"source": "oc://openclaw.config/channels/telegram",
"enabled": false
}
],
"mcpServers": [
{
"id": "docs",
"transport": "stdio",
"source": "oc://openclaw.config/mcp/servers/docs",
"command": "npx"
}
],
"modelProviders": [
{
"id": "openai",
"source": "oc://openclaw.config/models/providers/openai"
}
],
"modelRefs": [
{
"ref": "openai/gpt-5.6-sol",
"provider": "openai",
"model": "gpt-5.6-sol",
"source": "oc://openclaw.config/agents/defaults/model"
}
],
"network": [
{
"id": "browser-private-network",
"source": "oc://openclaw.config/browser/ssrfPolicy/dangerouslyAllowPrivateNetwork",
"value": false
}
],
"gatewayExposure": [
{
"id": "gateway-bind",
"kind": "bind",
"source": "oc://openclaw.config/gateway/bind",
"value": "loopback",
"nonLoopback": false,
"explicit": true
}
],
"agentWorkspace": [
{
"id": "agents-defaults-workspace-access",
"kind": "workspaceAccess",
"source": "oc://openclaw.config/agents/defaults/sandbox/workspaceAccess",
"scope": "defaults",
"value": "ro",
"sandboxMode": "all",
"sandboxModeSource": "oc://openclaw.config/agents/defaults/sandbox/mode",
"sandboxEnabled": true,
"explicit": true
},
{
"id": "agents-defaults-tool-exec",
"kind": "toolDeny",
"source": "oc://openclaw.config/tools/deny",
"scope": "defaults",
"tool": "exec",
"denied": true,
"explicit": true
}
],
"secrets": [
{
"id": "vault",
"kind": "provider",
"source": "oc://openclaw.config/secrets/providers/vault",
"providerSource": "env"
},
{
"id": "oc://openclaw.config/models/providers/openai/apiKey",
"kind": "input",
"source": "oc://openclaw.config/models/providers/openai/apiKey",
"provenance": "secretRef",
"refSource": "env",
"refProvider": "vault"
}
],
"authProfiles": [
{
"id": "github",
"source": "oc://openclaw.config/auth/profiles/github",
"validMetadata": true,
"provider": "github",
"mode": "token"
}
],
"tools": [
{
"id": "deploy",
"source": "oc://AGENTS.md/tools/deploy",
"line": 12,
"risk": "critical",
"sensitivity": "restricted",
"capabilities": ["IRREVERSIBLE_EXTERNAL"]
}
]
},
"checksRun": 30,
"checksSkipped": 0,
"findings": []
}
attestation.policy.hash points to the authored rule artifact. evidence captures the OpenClaw state observed during checks, and workspace.hash references that evidence payload. findingsHash identifies the exact finding set. checkedAt logs the check timestamp. attestationHash identifies the stable claim (policy hash, evidence hash, findings hash, and clean/dirty status) and intentionally omits checkedAt, so identical policy state always yields the same attestation hash. These four values together form the audit tuple for a single policy check.
When a gateway or supervisor uses policy to block, approve, or annotate a runtime action, it should log the attestation hash from the most recent clean check. checkedAt remains in JSON output for audit logs but falls outside the stable hash.
Lifecycle for accepting policy state:
- Author or review
policy.jsonc. - Execute
openclaw policy check --json. - If clean, save
attestation.policy.hashasexpectedHash. - Save
attestation.attestationHashasexpectedAttestationHash. - Re-run
openclaw doctor --lintin CI or release gates.
If policy rules change deliberately, refresh both accepted hashes from a clean check. If only workspace settings shift (policy unchanged), typically just expectedAttestationHash updates.
Enabling or upgrading agents.workspace rules injects agentWorkspace evidence into the workspace hash and attestation hash; review the new evidence and refresh accepted attestation hashes after enabling. Enabling or upgrading tool posture rules adds toolPosture evidence in the same manner.
openclaw policy watch re-executes the check and reports when current evidence no longer aligns with expectedAttestationHash:
openclaw policy watch --json
openclaw policy watch --agent ops --json
Use --once in CI or scripts needing a single drift evaluation. Without --once, it polls every two seconds by default; use --interval-ms to adjust the interval.
Findings
| Check id | Finding |
|---|---|
policy/policy-jsonc-missing | The policy is active, yet policy.jsonc has not been supplied. |
policy/policy-jsonc-invalid | Parsing of the policy failed, or its rule entries are malformed. |
policy/policy-hash-mismatch | The policy fails to align with the configured expectedHash. |
policy/attestation-hash-mismatch | The accepted attestation no longer corresponds to the current policy evidence. |
policy/policy-conformance-invalid | Comparison syntax in a baseline or checked policy file is invalid. |
policy/policy-conformance-missing | A rule mandated by the baseline policy file is absent from the checked file. |
policy/policy-conformance-weaker | A checked policy file specifies a value weaker than the baseline policy file. |
policy/channels-denied-provider | An active channel falls under a channel deny rule. |
policy/mcp-denied-server | Policy blocks a configured MCP server. |
policy/mcp-unapproved-server | A configured MCP server sits outside the allowlist. |
policy/models-denied-provider | A model provider or model ref in the config uses a provider that is denied. |
policy/models-unapproved-provider | A model provider or model ref in the config is not on the allowlist. |
policy/network-private-access-enabled | The private-network SSRF escape hatch is on, but policy forbids it. |
policy/routing-bindings-required | A channel route binding is demanded by policy, yet none is set up. |
policy/routing-binding-channel-unconfigured | A route binding points to a channel that is missing from channels.*. |
policy/routing-agent-mismatch | An authored route resolves to a different agent than intended. |
policy/routing-match-kind-mismatch | An authored route matches at a binding specificity that is unexpected. |
policy/ingress-dm-policy-unapproved | The channel DM policy falls outside the policy allowlist. |
policy/ingress-dm-scope-unapproved | session.dmScope fails to meet the DM isolation scope required by policy. |
policy/ingress-open-groups-denied | A channel group policy is set to open, while policy disallows open group ingress. |
policy/ingress-group-mention-required | Mention gates are disabled for a channel or group entry, though policy requires them. |
policy/gateway-non-loopback-bind | The gateway bind posture allows non-loopback exposure, which policy denies. |
policy/gateway-auth-disabled | Gateway authentication is off, but policy mandates it. |
policy/gateway-rate-limit-missing | The gateway auth rate-limit posture is not explicit, though policy demands it. |
policy/gateway-control-ui-insecure | Insecure exposure toggles for the Gateway Control UI are switched on. |
policy/gateway-tailscale-funnel | Gateway Tailscale Funnel exposure is active, but policy forbids it. |
policy/gateway-remote-enabled | Remote mode for the gateway is running, though policy denies it. |
policy/gateway-http-endpoint-enabled | A Gateway HTTP API endpoint is turned on, yet policy denies it. |
policy/gateway-http-url-fetch-unrestricted | URL-fetch input on the Gateway HTTP API lacks a required URL allowlist. |
policy/gateway-node-command-denied | A node command that policy denies is not denied by OpenClaw config. |
policy/agents-workspace-access-denied | Agent sandbox mode or workspace access is not within the policy allowlist. |
policy/agents-tool-not-denied | A tool that policy requires to be denied is not denied by an agent or default config. |
policy/tools-profile-unapproved | A global or per-agent tool profile in the config is outside the allowlist. |
policy/tools-fs-workspace-only-required | Filesystem tools lack the workspace-only path posture configuration. |
policy/tools-exec-security-unapproved | Exec security mode is not listed in the policy allowlist. |
policy/tools-exec-ask-unapproved | Exec ask mode is not listed in the policy allowlist. |
policy/tools-exec-host-unapproved | Exec host routing is not listed in the policy allowlist. |
policy/tools-elevated-enabled | Elevated tool mode is on, but policy denies it. |
policy/tools-also-allow-missing | A configured alsoAllow list omits an entry that policy requires. |
policy/tools-also-allow-unexpected | A configured alsoAllow list contains an entry that policy does not expect. |
policy/tools-required-deny-missing | A global or per-agent tool deny list misses a tool that must be denied. |
policy/sandbox-mode-unapproved | Sandbox mode is not within the policy allowlist. |
policy/sandbox-backend-unapproved | Sandbox backend is not within the policy allowlist. |
policy/sandbox-container-posture-unobservable | A container posture rule is active for a backend that cannot observe it. |
policy/sandbox-container-host-network-denied | A container-backed sandbox or browser operates in host network mode. |
policy/sandbox-container-namespace-join-denied | A container-backed sandbox or browser joins the namespace of another container. |
policy/sandbox-container-mount-mode-required | A mount in a container-backed sandbox or browser is not read-only. |
policy/sandbox-container-runtime-socket-mount | A mount in a container-backed sandbox or browser exposes the container runtime socket. |
policy/sandbox-container-unconfined-profile | The container sandbox profile is unconfined, which policy denies. |
policy/sandbox-browser-cdp-source-range-missing | The sandbox browser CDP source range is absent, though policy requires one. |
policy/data-handling-telemetry-content-capture | Telemetry content capture is enabled, but policy denies it. |
policy/data-handling-session-retention-not-enforced | Session retention maintenance is not enforced, though policy requires it. |
policy/data-handling-session-transcript-memory-enabled | Session transcript memory indexing is on, but policy denies it. |
policy/secrets-unmanaged-provider | A provider referenced by a config SecretRef is absent from the declarations under secrets.providers. |
policy/secrets-denied-provider-source | A source used by a config secret provider or SecretRef is forbidden by policy. |
policy/secrets-insecure-provider | Policy denies an insecure posture, yet a secret provider opts into it. |
policy/auth-profile-invalid-metadata | An auth profile in config lacks valid metadata for provider or mode. |
policy/auth-profile-unapproved-mode | The mode of a config auth profile falls outside the policy allowlist. |
policy/exec-approvals-missing | Policy demands the SQLite exec approvals document, but its row is absent. |
policy/exec-approvals-invalid | Parsing the configured SQLite exec approvals document fails. |
policy/exec-approvals-default-security-unapproved | A security mode in exec approval defaults sits outside the policy allowlist. |
policy/exec-approvals-agent-security-unapproved | The effective exec approval security mode for an agent lies outside the allowlist. |
policy/exec-approvals-auto-allow-skills-enabled | Policy denies implicit auto-allow of skill CLIs, yet an exec approval agent does it. |
policy/exec-approvals-allowlist-missing | A pattern mandated by policy is missing from the approvals allowlist. |
policy/exec-approvals-allowlist-unexpected | The approvals allowlist holds a pattern that policy does not anticipate. |
policy/tools-missing-risk-level | Risk metadata is absent from a governed tool declaration. |
policy/tools-unknown-risk-level | A governed tool declaration carries an unrecognized risk value. |
policy/tools-missing-sensitivity-token | Sensitivity metadata is missing from a governed tool declaration. |
policy/tools-missing-owner | Owner metadata is absent from a governed tool declaration. |
policy/tools-unknown-sensitivity-token | A governed tool declaration uses a sensitivity value that is unrecognized. |
A finding may carry two parts: target for the workspace object observed to
deviate, and requirement for the authored rule that triggered the finding.
Currently both are oc:// address strings, though the field names indicate
policy role rather than address format.
Example findings:
{
"checkId": "policy/channels-denied-provider",
"severity": "error",
"message": "Channel 'telegram' uses denied provider 'telegram'.",
"source": "policy",
"path": "openclaw config",
"ocPath": "oc://openclaw.config/channels/telegram",
"target": "oc://openclaw.config/channels/telegram",
"requirement": "oc://policy.jsonc/channels/denyRules/#0",
"fixHint": "Telegram is not approved for this workspace."
}
{
"checkId": "policy/tools-missing-risk-level",
"severity": "error",
"message": "AGENTS.md tool 'deploy' has no explicit risk classification.",
"source": "policy",
"path": "AGENTS.md",
"line": 12,
"ocPath": "oc://AGENTS.md/tools/deploy",
"target": "oc://AGENTS.md/tools/deploy",
"requirement": "oc://policy.jsonc/tools/requireMetadata"
}
{
"checkId": "policy/mcp-unapproved-server",
"severity": "error",
"message": "MCP server 'remote' is not in the policy allowlist.",
"source": "policy",
"path": "openclaw config",
"ocPath": "oc://openclaw.config/mcp/servers/remote",
"target": "oc://openclaw.config/mcp/servers/remote",
"requirement": "oc://policy.jsonc/mcp/servers/allow"
}
{
"checkId": "policy/models-unapproved-provider",
"severity": "error",
"message": "Model ref 'anthropic/claude-sonnet-4.7' uses unapproved provider 'anthropic'.",
"source": "policy",
"path": "openclaw config",
"ocPath": "oc://openclaw.config/agents/defaults/model/fallbacks/#0",
"target": "oc://openclaw.config/agents/defaults/model/fallbacks/#0",
"requirement": "oc://policy.jsonc/models/providers/allow"
}
{
"checkId": "policy/network-private-access-enabled",
"severity": "error",
"message": "Network setting 'browser-private-network' allows private-network access.",
"source": "policy",
"path": "openclaw config",
"ocPath": "oc://openclaw.config/browser/ssrfPolicy/dangerouslyAllowPrivateNetwork",
"target": "oc://openclaw.config/browser/ssrfPolicy/dangerouslyAllowPrivateNetwork",
"requirement": "oc://policy.jsonc/network/privateNetwork/allow"
}
{
"checkId": "policy/gateway-non-loopback-bind",
"severity": "error",
"message": "Gateway bind setting 'gateway-bind' permits non-loopback exposure.",
"source": "policy",
"path": "openclaw config",
"ocPath": "oc://openclaw.config/gateway/bind",
"target": "oc://openclaw.config/gateway/bind",
"requirement": "oc://policy.jsonc/gateway/exposure/allowNonLoopbackBind"
}
{
"checkId": "policy/gateway-node-command-denied",
"severity": "error",
"message": "Gateway node command 'system.run' is denied by policy but not denied by OpenClaw config.",
"source": "policy",
"path": "openclaw config",
"ocPath": "oc://openclaw.config/gateway/nodes/commands/deny",
"target": "oc://openclaw.config/gateway/nodes/commands/deny",
"requirement": "oc://policy.jsonc/gateway/nodes/denyCommands",
"fixHint": "Add 'system.run' to gateway.nodes.commands.deny or update policy after review."
}
{
"checkId": "policy/agents-workspace-access-denied",
"severity": "error",
"message": "agents.defaults sandbox workspaceAccess 'rw' is not allowed by policy.",
"source": "policy",
"path": "openclaw config",
"ocPath": "oc://openclaw.config/agents/defaults/sandbox/workspaceAccess",
"target": "oc://openclaw.config/agents/defaults/sandbox/workspaceAccess",
"requirement": "oc://policy.jsonc/agents/workspace/allowedAccess"
}
Repair
Access to doctor --lint and policy check is read-only.
When workspaceRepairs is not explicitly turned on, doctor --fix only touches
policy-managed workspace settings; otherwise, checks report what they would
fix and leave settings untouched.
In this release, repair can turn off channels that channels.denyRules denies and
carry out the automatic narrowing repairs listed below. Turn on workspaceRepairs
only after reviewing the policy file, since a valid rule can alter workspace
config:
- apply
tools.elevated.enabled=falsewhen a global policy blocks elevated tools - insert missing required-deny tool ids into
tools.denyoragents.entries.*.tools.denywhen policy requires those tools to be denied - flip insecure
gateway.controlUi.*toggles tofalse - apply
gateway.mode=localwhen policy blocks remote gateway mode - change reported
gateway.http.endpoints.*.enabledpaths tofalsewhen policy blocks Gateway HTTP API endpoints - change reported channel ingress
groupPolicypaths toallowlistwhen policy blocks open group ingress - change reported channel ingress
requireMentionpaths totruewhen policy requires group mentions - apply
diagnostics.otel.captureContent=false, ordiagnostics.otel.captureContent.enabled=falsefor object-form telemetry capture settings, when policy blocks telemetry content capture
Repairs scoped to elevated tools are detect-only. Scoped data-handling repairs are also skipped when the finding points to shared telemetry config, because altering the shared setting would impact more than the scoped policy target.
dataHandling.sensitiveLogging.requireRedaction offers neither check nor repair.
OpenClaw always redacts sensitive logs, so nothing can flag it as off.
The key remains a supported policy rule: openclaw policy checks
its shape, openclaw policy compare still requires a candidate policy to be at
least as strict as the baseline for it, and openclaw policy check logs the
runtime invariant oc://openclaw.invariant/logging/redaction in the
dataHandling evidence and attestation as proof the requirement is met.
Scoped required-deny repairs are skipped when the finding reports inherited
root tools.deny, because adding the required tool to root config would impact
more than the scoped policy target. Agent-local required-deny repairs can update
the reported agents.entries.*.tools.deny path.
Scoped channel ingress repairs are skipped when the finding reports inherited
channels.defaults.*, because changing the shared channel default would impact
more than the scoped policy target. Gateway HTTP URL-fetch allowlist findings
stay manual because automatic repair cannot pick the correct endpoint URL
allowlist values.
Gateway bind and node-command findings always require review. When policy/gateway-non-loopback-bind or policy/gateway-node-command-denied can be resolved to a config path, doctor --fix surfaces the suggested gateway.bind or gateway.nodes.commands.deny adjustment as skipped preview guidance. No change is applied, and the finding stays unrepaired until an operator reviews and modifies config or policy.
{
"plugins": {
"entries": {
"policy": {
"config": {
"workspaceRepairs": true,
},
},
},
},
}
Exit codes
| Command | 0 | 1 | 2 |
|---|---|---|---|
policy check | No findings at the threshold. | One or more findings met the threshold. | Argument or runtime failure. |
policy compare | The policy file is at least as strict as the baseline. | The policy file is invalid, missing, or weaker than baseline rules. | Argument or runtime failure. |
policy watch | No findings and accepted hash is current. | Findings exist or accepted attestation is stale. | Argument or runtime failure. |