← Back to Plugins
Voice

Resource Boundary

liu51115 By liu51115 👁 10 views ▲ 0 votes

OpenClaw plugin: per-agent file path boundary enforcement via before_tool_call hooks

GitHub

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

Loading comments...