Skip to content

Repository files navigation

SpiceClaw

Fine-grained, deterministic permissions for OpenClaw agents.

⚠️ Status: proof-of-concept. This project is currently experimental and will change wihout notice. File an issue if you are planning to use this for a production use case.

Overview

By default, an OpenClaw agent has full agency, and the only guardrail most setups have is whatever gets written into an AGENTS.md prompt — which is non-deterministic: the same rule can support two equally defensible readings depending on how the model interprets it that turn (see the Crustodian example below).

Two questions worth asking about any OpenClaw agent you run: do you know what it can do right now? And are you confident it can't do anything else? For most OpenClaw setups, the honest answer to the second question is no.

SpiceClaw is OpenClaw (an open-source assistant that runs on your own machine) plus SpiceDB (a purpose-built, Zanzibar-inspired permissions database maintained by AuthZed): a plugin that gives OpenClaw agents fine-grained, deterministic permissions. An agent keeps every capability OpenClaw already gives it — SpiceClaw doesn't remove features — but it can't use a tool without an explicit grant, and it can't be prompted into an action nobody pre-authorized. It does not fork OpenClaw or manage its process lifecycle: you point an existing OpenClaw instance at it.

Getting started

Linux with systemd only, today. spiceclawd is supervised via systemctl; there is no launchd implementation yet, so the daemon half of the install does not run on macOS.

You need Node 22+, pnpm (corepack enable), and Docker (for the SpiceDB sidecar).

1. Do you already have OpenClaw running?

If yes — your OpenClaw needs to be at or above the plugin's compat floor (2026.6.9) and must have already generated its own config (~/.openclaw/openclaw.json by default, or set OPENCLAW_CONFIG_PATH) — spiceclaw install refuses if that file doesn't exist yet. Skip to step 2.

If noinstall OpenClaw

2. One-time OS provisioning (root, once per host)

spiceclaw install will not perform this silently on your behalf — it fails loudly with this exact guidance if either is missing:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin spiceclaw-admin
sudo groupadd spiceclaw-relay
sudo usermod -aG spiceclaw-relay spiceclaw-admin
sudo usermod -aG spiceclaw-relay <the-user-openclaw-itself-runs-as>

3. Clone this repo and build the CLI

SpiceClaw retrofits onto OpenClaw locally, so this checkout needs to live on the same machine as the OpenClaw instance you're pointing it at:

git clone https://github.com/authzed/spiceclaw.git
cd spiceclaw
pnpm install && pnpm -r build
bash scripts/install-cli.sh   # symlinks the built CLI into a bin dir on your PATH

Without the symlink step, invoke in place via pnpm --filter @spiceclaw/cli exec spiceclaw <cmd>. The CLI resolves the plugin, the daemon build, the compose files, and the SpiceDB schema relative to this checkout — keep it in place after installing, don't move or delete it.

spiceclawd (step 4) runs as its own dedicated spiceclaw-admin OS user, unrelated to yours, and needs to traverse into this checkout to load its built admind/dist/main.js — traverse only, not read your other files. If your home directory is 750 or tighter (common on hardened hosts), that traversal fails with a misleading Cannot find module error. Fix once, per host:

chmod o+x "$HOME"   # lets other users pass THROUGH your home dir; doesn't make it listable

Cloning somewhere world-traversable instead (e.g. /opt/spiceclaw) avoids this entirely.

4. Install SpiceClaw onto OpenClaw

Monitor-first, no-lockout, reversible. Always pass --gateway-user (or set SPICECLAW_GATEWAY_USER) to the OS user OpenClaw's own gateway process runs as — install refuses outright if that resolves to root:

sudo mkdir -p /var/log/spiceclaw && sudo chown spiceclaw-admin:spiceclaw-admin /var/log/spiceclaw
sudo env "PATH=$PATH" OPENCLAW_CONFIG_PATH="$HOME/.openclaw/openclaw.json" \
  spiceclaw install --owner <your-user-id> --gateway-user <openclaw-user> \
  --audit-log /var/log/spiceclaw/audit.log
spiceclaw uninstall       # (later) rolls back to vanilla OpenClaw (config restored byte-for-byte)

--audit-log is optional but worth setting now: install bakes whatever's given at install time into spiceclawd's unit, and step 7's review step has nothing to show without it. (SPICECLAW_AUDIT_LOG still works too, as a lower-precedence fallback.)

Not bare sudo spiceclaw install: sudo resets both PATH and HOME by default, and this command depends on each surviving from your shell, not root's —

  • PATH — step 3's install-cli.sh always symlinks into a per-user bin dir (npm prefix -g's bin, ~/.npm-global/bin, or ~/.local/bin) so it never needs root to install itself, but that means it's on your PATH, not sudo's own secure_path; without it, sudo spiceclaw install fails outright with "command not found."
  • HOMEinstall defaults --config to ~/.openclaw/openclaw.json; under a bare sudo, ~ resolves to /root, not your real OpenClaw config. $HOME above is expanded by your own shell before sudo runs, so it still names your real home directory even though the command executes as root.

There's no separate step for SpiceDB — this one command brings it up for you, along with everything else: preflight-checks your OpenClaw version, starts the SpiceDB sidecar (docker compose -f docker/spicedb-compose.yml up) and loads its schema, generates spiceclawd's credential and launches it in monitor mode, seeds you as owner, syncs OpenClaw's tool policy into the graph, and registers the plugin in OpenClaw's config.

It prints the ONE variable the plugin needs in OpenClaw's own process environment — SPICECLAW_ADMIND_SOCKET_PATH — and nothing SpiceDB-shaped at all: the plugin holds zero SPICEDB_* variables and talks to spiceclawd's check-relay over that socket instead of dialing SpiceDB directly.

Just want the SpiceDB + spiceclawd stack running on its own, without retrofitting an existing OpenClaw? spiceclaw up [--owner <id>] brings up the same sidecar + schema + daemon by itself — see Command reference.

5. Point OpenClaw's own process environment at the socket, then restart OpenClaw yourself

spiceclaw install does not restart OpenClaw. Set the variable step 4 printed wherever OpenClaw itself loads its env — its systemd unit's Environment=, an env file it sources, docker run -e ... if it's containerized — and restart it through whatever mechanism you normally use.

6. Verify it actually loaded

sudo env "PATH=$PATH" OPENCLAW_CONFIG_PATH="$HOME/.openclaw/openclaw.json" \
  spiceclaw doctor --gateway-user <openclaw-user> --openclaw-version <your-openclaw-version>

Same PATH/HOME reasons as step 4. --gateway-user matters here too: without it, doctor under sudo resolves to root and several checks fail as a result — it loudly caveats this rather than guessing, but it's easy to misread as a real problem. --openclaw-version (get it from openclaw --version) is optional but worth passing: without it, the version check [WARN]s that nothing was actually verified, rather than reporting a real compatibility result.

Then check OpenClaw's own gateway log for these two signals:

registered tool: request_elevation

and spiceclaw-authz listed in the gateway's own plugin-count log line (http server listening (N plugins: ..., spiceclaw-authz, ...)).

7. Review before enforcing, then turn it on

spiceclaw audit --denied-only --log /var/log/spiceclaw/audit.log   # (or rely on SPICECLAW_AUDIT_LOG)
sudo env "PATH=$PATH" spiceclaw mode enforce                       # prompts; add --yes for non-interactive

audit needs --log/SPICECLAW_AUDIT_LOG pointed at whatever path you set in step 4 — without it, there's nothing to review. mode needs sudo (it rewrites spiceclawd's unit file directly) and your real PATH, same reasons as step 4 — but not OPENCLAW_CONFIG_PATH or --gateway-user; neither applies to this command. At a real terminal (not a script), drop --yes and you'll get a Type 'yes' to confirm: prompt instead — that's invariant 7's human-consent gate, not a bug.

Try it out

With enforcement on, ask the agent to run something destructive it doesn't already hold a grant for — an exec, a file write. Instead of a hard deny, the agent drafts a time-boxed grant and asks a human to approve it right in the same chat:

agent: (calls request_elevation for "exec", ttlSeconds=900)
        → "still valid for 15m — reply /approve <planId> in this chat,
           or /approve if it's your only pending request"
human:  /approve
        → "approved elevation-request <planId>."

Or work it from a terminal instead of chat:

spiceclaw admin pending [--agent <id>] [--kind permission-change|elevation-request]
spiceclaw admin approve <planId>
spiceclaw admin reject  <planId> [--reason <text>]

How it works

Effective agent permissions need to live outside OpenClaw's reach, decide deterministically, apply just-in-time, and be expressive enough to scope a grant to one tool, one argument shape, one time window. OpenClaw's own policy is static config that governs what an agent can see — tool visibility. SpiceClaw is the layer underneath it that governs what an agent can actually execute.

flowchart LR
    channels["channels / clients<br/>Telegram · Discord · Slack<br/>Signal · WhatsApp · Control UI"]

    subgraph openclaw["OpenClaw (unmodified)"]
        plugin["SpiceClaw plugin (in-process)<br/>channel admission · tool dispatch (+memory)<br/>outbound send · delimiter wrapping"]
    end

    admind["spiceclawd (admin-relay daemon)<br/>own OS user · holds the<br/>write-capable SpiceDB credential"]
    spicedb[("SpiceDB sidecar<br/>schema · checks · grants")]
    cli["spiceclaw CLI<br/>(own operator credential)"]

    channels -- dial-out admission --> plugin
    plugin -- "unix socket (check-relay —<br/>no SpiceDB creds in this process)" --> admind
    admind --> spicedb
    cli -- admin reads / confirm-gated writes --> spicedb
Loading
  • Plugin (in-process) — the agent-loop seams: dial-out channel admission, tool dispatch, memory read/write (memory is registered as tools, so before_tool_call gates it), outbound send, and untrusted-output delimiters — via OpenClaw's plugin hook surface. It performs every check by asking spiceclawd over a unix socket; it never opens a SpiceDB connection itself and never writes the graph.
  • spiceclawd (standalone daemon) — the only process that holds a write-capable SpiceDB credential. It serves the plugin's read-only check-relay, owns the out-of-band enforcement-mode toggle, and applies chat-approved permission changes (spiceclaw admin approve/reject) atomically.
  • SpiceDB sidecar + a shared authz module — the one place all authorization logic lives (schema, checks, grants); the plugin and spiceclawd are thin call-sites into it. The CLI talks to SpiceDB directly under its own operator credential for admin reads and confirm-gated writes.

Three principles

  1. Permissions live outside OpenClaw's reach. spiceclawd runs as its own OS user and is the only process holding SpiceDB credentials — a fully compromised OpenClaw gateway process still can't read or forge a grant.
  2. Decisions are deterministic. No model is ever asked "should this be allowed?" — every grant is written ahead of time, out of band, by a human or an approved plan; the plugin's runtime check is a graph lookup, not an inference.
  3. Expressive enough for what OpenClaw actually does. OpenClaw's static config sets tool visibility; SpiceClaw sets the grants underneath it — scoped per tool, with optional per-argument limits and TTLs, effective immediately, no gateway restart required.

Configure

Configuration is environment-driven. The essentials (full table in INSTALL.md §Reference):

Var Meaning Default
SPICEDB_ENDPOINT SpiceDB gRPC endpoint, read by spiceclawd/the CLI (never by the plugin, which holds zero SPICEDB_* vars)
SPICEDB_TOKEN SpiceDB preshared key for the sidecar (generate a real one; rotate the dev placeholder) — shared by the sidecar itself and the CLI's own seed/migrate/check/etc. commands. Not spiceclawd's write-capable credential — that is a separate secret (ADMIND_SPICEDB_TOKEN at generation time), delivered to spiceclawd via its own dedicated 0600 file, never this env var (see "Secrets are generated" below) ""
POSTGRES_PASSWORD Postgres datastore password for the SpiceDB sidecar (dev default spiceclaw-pg-dev; set a real one before any network-exposed deployment) spiceclaw-pg-dev
SPICEDB_FLOOR_MODE fail-closed floor (deny or owner-only — never wider) deny
SPICECLAW_DEMO 1 enables the out-of-band mode toggle (non-production only) unset (pinned enforce)
SPICECLAW_PROFILE production refuses demo outright unset

Secrets are generated, never defaulted. spiceclaw install resolves + persists the SpiceDB preshared key and postgres password into a 0600 docker/secrets/spiceclaw.env (generated values shown exactly once; a re-run reuses the file), and generates spiceclawd's own separate write-capable credential. Real values (not the dev placeholders) are required before any network-exposed deployment.

Since install itself runs under sudo (step 4), that 0600 file would otherwise land root-owned — even though it lives in your own checkout and every operator command above (check, inspect, who-can, grant, revoke, ...) is meant to use it as you, not root. install chowns it back to the invoking user automatically (via sudo's own SUDO_UID/SUDO_GID), so this needs no manual fixup.

About

Fine-grained permissions for OpenClaw agents

Resources

Code of conduct

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages