Voice
Read Guardrail
OpenClaw plugin that prevents one agent from reading another agent's workspace, config, and credentials. Fills the gap where workspaceOnly enforces writes but not reads.
README
# openclaw-read-guardrail
A `before_tool_call` plugin for [OpenClaw](https://github.com/openclaw/openclaw) that enforces read-only workspace isolation for multi-agent setups. The upstream `tools.fs.workspaceOnly: true` config enforces write/edit but **not read**. This plugin fills that gap.
Built for setups where a social/public-facing agent shares a gateway with a private agent and must never read the private agent's workspace, config files, credentials, or environment variables.
## The Problem
OpenClaw's `workspaceOnly` restricts write, edit, and apply-patch tools to the agent's workspace. But the `read` tool, `image` tool, and `pdf` tool can access any path on the filesystem. In a multi-agent setup where one agent handles private DMs and another handles group chats with non-operator humans, this is a privacy gap. A prompt injection in the group chat could instruct the social agent to read `openclaw.json`, `.env`, or private workspace files and leak them into the conversation.
GitHub issues requesting this feature natively:
- [#12202](https://github.com/openclaw/openclaw/issues/12202) -- per-agent file path access control
- [#28869](https://github.com/openclaw/openclaw/issues/28869) -- tools.fs.allowPaths
Neither has shipped. This plugin fills the gap using the `before_tool_call` hook, the same pattern used by [EasyClaw's file-permissions plugin](https://github.com/gaoyangz77/easyclaw) (guards read/write/edit/image/apply-patch), [ClawBands](https://github.com/SeyZ/clawbands), [predicate-claw](https://github.com/PredicateSystems/predicate-claw), and [Equilibrium Guard](https://github.com/rizqcon/equilibrium-guard).
## What It Does
Registers a `before_tool_call` hook that intercepts `read`, `image`, `pdf`, and `diffs` tool calls. For the configured agent (default: "social"), it:
1. Extracts file paths from tool-specific parameters (read uses `path`, image uses `image`, pdf uses `pdf`/`pdfs`)
2. Filters out remote URIs (http, https, ftp, s3, data, etc.)
3. Normalizes local paths (null bytes, `~`, `$HOME`, `${HOME}`, `file://` URIs, `..` traversals, relative paths)
4. Checks against an always-blocked list (defense in depth)
5. Checks against an allowlist (default: agent workspace + /tmp/)
6. Returns `{ block: true }` if not allowed
The main/private agent is unrestricted. Unknown agents are unrestricted (fail-open for non-configured agents, fail-closed for missing paths).
### Why only read tools?
OpenClaw's `tools.fs.workspaceOnly` already enforces write, edit, and apply_patch to the agent's workspace. The `exec` tool is separately gated by `exec-approvals.json`. The **read** tool is the gap: `workspaceOnly` does not restrict it. This plugin fills exactly that gap plus the `image` and `pdf` tools which have the same unrestricted file-loading behavior.
If you need a plugin that guards both reads AND writes in a single hook (e.g., because you don't trust `workspaceOnly` or want unified logging), see [EasyClaw's file-permissions plugin](https://github.com/gaoyangz77/easyclaw) which guards read/write/edit/image/apply_patch together.
## Requirements
- **OpenClaw v2026.3.12 or later.** Tested on v2026.3.12.
- **`before_tool_call` hook must be wired up in your version.** This hook was reported as unwired in [#5943](https://github.com/openclaw/openclaw/issues/5943) (Feb 2026) and [#5513](https://github.com/openclaw/openclaw/issues/5513) (Jan 2026). It is working in v2026.3.12. We cannot confirm which version first wired it up. If your version is older than v2026.3.12, the plugin may load and register without error but the hook may never fire, silently allowing all reads. See [Troubleshooting](#troubleshooting) to verify.
## Install
### 1. Copy plugin files
```bash
mkdir -p ~/.openclaw/extensions/read-guardrail
cp index.ts openclaw.plugin.json ~/.openclaw/extensions/read-guardrail/
```
### 2. Verify the manifest has the id field
```bash
cat ~/.openclaw/extensions/read-guardrail/openclaw.plugin.json | jq .id
```
Must return `"read-guardrail"`. If it returns `null`, your manifest is missing the required `id` field and the gateway will refuse to start. The `name` field is display-only. The `id` field is what the gateway uses for discovery and config binding. OpenClaw does **not** infer id from the directory name.
### 3. Add to plugins.allow and plugins.entries
**Both steps are required on v2026.3.12+.** v2026.3.12 disabled implicit workspace plugin auto-load as a security fix ([GHSA-99qw-6mr3-36qr](https://github.com/openclaw/openclaw/security/advisories/GHSA-99qw-6mr3-36qr)). Without `plugins.allow`, the plugin silently fails to load with no error, only a warning buried in journalctl.
```bash
jq '
.plugins.allow = ((.plugins.allow // []) + ["read-guardrail"] | unique) |
.plugins.entries."read-guardrail" = {"enabled": true}
' ~/.openclaw/openclaw.json > /tmp/oc-patch.json && mv /tmp/oc-patch.json ~/.openclaw/openclaw.json
```
This command is safe to run multiple times (it deduplicates the allow array).
### 4. Restart gateway
```bash
openclaw gateway restart
```
This works on both macOS (launchd) and Linux (systemd). If you run OpenClaw as a system service on Linux, use `sudo systemctl restart openclaw` instead.
### 5. Verify
```bash
journalctl -u openclaw --since "1 min ago" | grep '\[read-guardrail\]'
```
You should see a line like:
```
[read-guardrail] Active v1.3.4. Guarding agent="social", tools=[read,image,pdf,diffs], 2 allowed paths, 6 always-blocked paths, workspaceRoot="<your-workspace-path>".
```
You should NOT see:
- `plugin not found` -- missing `plugins.allow` (see step 3)
- `plugin manifest requires id` -- missing `id` field in manifest (see step 2)
- `Unknown config key` -- you have an old version with the ALLOWED_KEYS bug (update to v1.3.4)
### 6. Test
**Block test:** In the social agent's channel, ask it to read `~/.openclaw/openclaw.json`. Should get "Access denied."
**Allow test:** Ask the social agent to read `SOUL.md` (or any file in its workspace). Should succeed.
## Adapt for Your Setup
The plugin auto-detects your home directory from `process.env.HOME` (Linux/macOS) or `process.env.USERPROFILE` (Windows). The `~`, `$HOME`, and `${HOME}` path expansions, the default allowed paths, and the always-blocked paths all use this detected value. **No source edits needed for home directory differences.**
You may still need to edit these constants for your agent setup:
| Constant | What to change | Default |
|----------|---------------|---------|
| `DEFAULT_SOCIAL_AGENT_ID` | Your restricted agent's id (from `openclaw.json` agents section) | `"social"` |
| `DEFAULT_ALLOWED_PATHS` | Your restricted agent's workspace path (with trailing slash) + any other allowed directories | `HOME_DIR + "/.openclaw/workspace-social/"`, `"/tmp/"` |
| `ALWAYS_BLOCKED` | Sensitive paths to block even if they fall within an allowed prefix. Update agent/workspace names to match your setup. | Config files, credentials, main agent workspace |
If your restricted agent uses a different workspace name (not `workspace-social`) or a different main agent workspace (not `workspace`), edit those path suffixes in the constants.
**Why not runtime config?** The manifest declares `configSchema` with `socialAgentId` and `allowedPaths` properties, and the gateway validates them. However, `api.config` in the plugin's `register()` function returns the full global `openclaw.json`, not plugin-scoped config. Reading plugin-scoped config would require `api.config?.plugins?.entries?.["read-guardrail"]?.config`. We use hardcoded defaults for simplicity. The configSchema is preserved so that if OpenClaw adds plugin-scoped config injection in the future, the schema is already in place.
## Architecture
### Hook priority
This plugin runs at priority 99. If you also use a write-guarding plugin (e.g., a privacy-guardrail at priority 100), it runs first since higher priority = earlier execution. There is no conflict because write-guarding plugins intercept write/edit/exec/bash while this plugin intercepts read/image/pdf/diffs.
If this is your only guardrail plugin, priority 99 is fine. The value only matters relative to other plugins using the same hook.
### Path normalization
The `normalizePath` function handles:
- Null byte stripping (OS truncation attack vector)
- `file://` URI scheme stripping with RFC 8089 authority handling
- `~` and `~/` home directory expansion (via `HOME_DIR` from `process.env.HOME`)
- `$HOME` and `${HOME}` expansion with boundary checks (prevents `$HOME_EXTRA` false match)
- Relative path resolution against workspace root
- `..` traversal resolution
### Tool parameter extraction
Param names verified from [docs.openclaw.ai/tools](https://docs.openclaw.ai/tools) (March 2026):
| Tool | Param | Type | Notes |
|------|-------|------|-------|
| read | path | string | Canonical. `file_path` aliased to `path` pre-hook per [#5943](https://github.com/openclaw/openclaw/issues/5943). |
| image | image | string | Path or URL. |
| pdf | pdf | string | Single path or URL. |
| pdf | pdfs | string[] | Array of paths/URLs. All local paths validated. |
| diffs | path | string | Display label only, not a file read. Defense in depth. |
If no path can be extracted from a guarded tool call, the hook **fails closed** (blocks the call).
### Known limitations
- **Does not guard write/edit/apply_patch.** These are already restricted by `tools.fs.workspaceOnly`. If you have `workspaceOnly` disabled or set to false, this plugin alone is not sufficient. Note that apply_patch has a known path traversal issue when sandbox is disabled ([#12173](https://github.com/openclaw/openclaw/issues/12173)).
- **Symlink resolution timing.** The hook sees the path the model sends, not resolved symlinks. Social reads shared files (SOUL.md, IDENTITY.md) via symlinks in its workspace. **Verified safe on v2026.3.12** by Eve on live system: the hook fires before the read tool's `execute` function, receiving raw model params, not
... (truncated)
voice
Comments
Sign in to leave a comment