Voice
Resource Boundary
OpenClaw plugin: per-agent file path boundary enforcement via before_tool_call hooks
Install
npm install
npm
Configuration Example
{
"plugins": {
"entries": {
"resource-boundary": {
"enabled": true,
"config": {
"defaultMode": "allow",
"alwaysAllowPaths": [
"/etc/**",
"/usr/**",
"/bin/**",
"/tmp/**",
"/var/**",
"/opt/homebrew/**"
],
"agents": {
"my-architect-agent": {
"mode": "deny-external",
"allowedPaths": [
"/home/user/.openclaw/**"
],
"oneReadPaths": [
"/home/user/projects/**",
"/opt/homebrew/lib/node_modules/**"
],
"oneReadWindow": 30,
"blockedTools": ["read", "write", "edit", "exec"],
"exemptTools": ["web_search", "web_fetch", "memory_search"],
"blockMessage": "Delegate to Claude Code via sessions_spawn(runtime: \"acp\", agentId: \"claude\")."
}
}
}
}
}
}
}
README
# resource-boundary
An [OpenClaw](https://github.com/openclaw/openclaw) plugin that enforces per-agent file path boundaries at the tool execution layer.
## The Problem
AI agents with file system access tend to investigate. Ask one to fix a bug and it'll spend 15 turns reading source code, tracing imports, and exploring directories β burning tokens and time on work that should be delegated to a coding agent.
System prompt rules ("don't read files outside your workspace") are suggestions. Agents rationalize around them. This plugin makes the boundary structural: a `before_tool_call` hook that blocks file operations outside configured scopes before they execute.
## How It Works
```
Agent calls read("/opt/homebrew/lib/node_modules/openclaw/dist/some-file.js")
β
Plugin intercepts via before_tool_call hook
β
Path doesn't match allowedPaths or alwaysAllowPaths
β
Tool call blocked β agent gets: "Path outside your scope. Delegate to Claude Code."
β
Agent spawns a coding subagent instead of investigating itself
```
**This is not a security tool.** It's a behavioral constraint. Agents can still spawn subagents that run without restrictions. The goal is forcing delegation, not preventing access.
## Features
- **Per-agent configuration** β each agent gets its own scope; unconfigured agents are unrestricted
- **Glob patterns** via [picomatch](https://github.com/micromatch/picomatch) β `**`, `*`, `?`, braces, negation
- **One-read exception** β configurable "peek" allowance for external paths (one read per directory within a time window, then blocked)
- **Path resolution** β handles symlinks (`/opt/homebrew/bin/python3` β `/opt/homebrew/Cellar/...`), `~` expansion, relative paths
- **Exec command parsing** β best-effort path extraction from shell commands
- **Case-insensitive tool matching** β works regardless of tool name casing
- **Dotfile support** β `.env`, `.git/`, `.openclaw/` all matched correctly
- **Fail-open** β if the plugin errors, the tool call proceeds (gateway stays up)
## Installation
```bash
# From local directory
openclaw plugins install /path/to/resource-boundary
# Restart to load
openclaw gateway restart
```
## Configuration
Add to your `openclaw.json`:
```json
{
"plugins": {
"entries": {
"resource-boundary": {
"enabled": true,
"config": {
"defaultMode": "allow",
"alwaysAllowPaths": [
"/etc/**",
"/usr/**",
"/bin/**",
"/tmp/**",
"/var/**",
"/opt/homebrew/**"
],
"agents": {
"my-architect-agent": {
"mode": "deny-external",
"allowedPaths": [
"/home/user/.openclaw/**"
],
"oneReadPaths": [
"/home/user/projects/**",
"/opt/homebrew/lib/node_modules/**"
],
"oneReadWindow": 30,
"blockedTools": ["read", "write", "edit", "exec"],
"exemptTools": ["web_search", "web_fetch", "memory_search"],
"blockMessage": "Delegate to Claude Code via sessions_spawn(runtime: \"acp\", agentId: \"claude\")."
}
}
}
}
}
}
}
```
### Config Reference
#### Global
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `defaultMode` | `"allow" \| "deny-external"` | `"allow"` | Default for agents not listed in `agents` |
| `alwaysAllowPaths` | `string[]` | `[]` | Glob patterns always permitted (system dirs, package managers) |
| `agents` | `object` | `{}` | Per-agent boundary configuration |
#### Per-Agent
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `mode` | `"allow" \| "deny-external"` | β | `allow` = no restrictions; `deny-external` = enforce boundaries |
| `allowedPaths` | `string[]` | `[]` | Full access β agent's workspace, config dirs |
| `oneReadPaths` | `string[]` | `[]` | Limited access β one read per directory per window |
| `oneReadWindow` | `number` | `30` | Seconds before the one-read counter resets |
| `blockedTools` | `string[]` | all | Tools to enforce boundaries on |
| `exemptTools` | `string[]` | `[]` | Tools that always pass through |
| `blockMessage` | `string` | `""` | Custom message appended to block reason |
### Evaluation Order
For each file path in a tool call:
1. **`alwaysAllowPaths`** β system directories, package managers β β
allow
2. **`allowedPaths`** β agent's workspace β β
allow
3. **`oneReadPaths`** β external code, first read per directory β β
allow (once)
4. **Everything else** β β block
## One-Read Exception
The one-read exception lets an agent peek at external files without committing to a full investigation. It tracks reads per **parent directory** β so reading two files in the same directory counts as investigation and triggers a block.
```
read("/opt/code/project/src/index.ts") β β
allowed (first in /opt/code/project/src/)
read("/opt/code/project/src/utils.ts") β β blocked (second in same directory)
read("/opt/code/project/test/test.ts") β β
allowed (first in /opt/code/project/test/)
```
After `oneReadWindow` seconds, the counter resets and the agent can read again.
## Exec Command Handling
For `exec` tool calls, the plugin does best-effort path extraction from the command string:
- **Absolute paths**: `/foo/bar/baz` β extracted and checked
- **Tilde paths**: `~/foo/bar` β expanded and checked
- **Relative paths**: `./src/file.ts` β resolved against cwd and checked
- **No extractable paths**: allowed with a warning logged
The plugin doesn't try to fully parse shell commands. Complex pipes or subshells may not have all paths extracted β this is intentional (fail-open).
## Agent Isolation
Agents not listed in the `agents` config default to `defaultMode` (usually `"allow"`). This means:
- **Subagents** spawned via `sessions_spawn` run under their own `agentId` and aren't restricted
- **Claude Code ACP** sessions run as a different agent β unrestricted
- Only the configured agent is constrained
This is the key design: the architect agent is bounded, but its workers are free.
## Requirements
- OpenClaw 3.28+
- Node.js 20+
- `picomatch` v4+ (bundled)
## Development
```bash
cd resource-boundary
npm install
npm test # run tests once
npm run test:watch # watch mode
```
## License
MIT
voice
Comments
Sign in to leave a comment