@aporthq/aport-agent-guardrails 1.0.12 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/bin/agent-guardrails +13 -6
- package/bin/aport-claude-code-hook.sh +147 -0
- package/bin/aport-create-passport.sh +5 -0
- package/bin/aport-guardrail-bash.sh +7 -3
- package/bin/aport-resolve-paths.sh +17 -1
- package/bin/aport-status.sh +6 -0
- package/bin/frameworks/claude-code.sh +114 -0
- package/bin/frameworks/crewai.sh +3 -0
- package/bin/frameworks/cursor.sh +11 -3
- package/bin/frameworks/langchain.sh +3 -0
- package/bin/frameworks/n8n.sh +3 -0
- package/bin/lib/allowlist.sh +6 -4
- package/bin/lib/config.sh +1 -0
- package/bin/lib/detect.sh +6 -1
- package/bin/lib/validation.sh +27 -7
- package/bin/openclaw +10 -4
- package/docs/RELEASE.md +1 -1
- package/docs/frameworks/claude-code.md +109 -0
- package/docs/frameworks/cursor.md +16 -7
- package/docs/launch/PRD-claude-code-guardrail.md +753 -0
- package/extensions/openclaw-aport/index.ts +13 -0
- package/package.json +2 -2
package/bin/openclaw
CHANGED
|
@@ -374,6 +374,12 @@ YAML
|
|
|
374
374
|
fi
|
|
375
375
|
fi
|
|
376
376
|
|
|
377
|
+
# Backup a file before overwriting (creates .bak); defined here before first use
|
|
378
|
+
backup_file() {
|
|
379
|
+
local f="$1"
|
|
380
|
+
[ -f "$f" ] && cp "$f" "${f}.bak"
|
|
381
|
+
}
|
|
382
|
+
|
|
377
383
|
# So the default config (openclaw.json) has plugin mode/apiUrl - gateway often loads it, not config.yaml
|
|
378
384
|
OPENCLAW_JSON="$CONFIG_DIR/openclaw.json"
|
|
379
385
|
if [ -f "$OPENCLAW_JSON" ] && command -v jq &>/dev/null; then
|
|
@@ -411,7 +417,7 @@ YAML
|
|
|
411
417
|
| .source = "path"
|
|
412
418
|
| .sourcePath = $plugin_path
|
|
413
419
|
| .installPath = $plugin_path)
|
|
414
|
-
' "$OPENCLAW_JSON" > "$OPENCLAW_JSON.tmp" 2>/dev/null && mv "$OPENCLAW_JSON.tmp" "$OPENCLAW_JSON"; then
|
|
420
|
+
' "$OPENCLAW_JSON" > "$OPENCLAW_JSON.tmp" 2>/dev/null && { backup_file "$OPENCLAW_JSON"; mv "$OPENCLAW_JSON.tmp" "$OPENCLAW_JSON"; }; then
|
|
415
421
|
if [ "$USE_HOSTED_PASSPORT" = true ]; then
|
|
416
422
|
echo " ✅ Merged plugin config into $OPENCLAW_JSON (mode=$PLUGIN_MODE, hosted passport, allowUnmappedTools=$ALLOW_UNMAPPED)"
|
|
417
423
|
else
|
|
@@ -484,7 +490,7 @@ if [ -f "$PASSPORT_FILE" ] && command -v jq &>/dev/null; then
|
|
|
484
490
|
.max_execution_time = (.limits.max_execution_time // 300)) |
|
|
485
491
|
.limits |= del(.allowed_commands, .blocked_patterns, .max_execution_time)
|
|
486
492
|
else . end)
|
|
487
|
-
' "$PASSPORT_FILE" > "$PASSPORT_FILE.tmp" && mv "$PASSPORT_FILE.tmp" "$PASSPORT_FILE"
|
|
493
|
+
' "$PASSPORT_FILE" > "$PASSPORT_FILE.tmp" && { backup_file "$PASSPORT_FILE"; mv "$PASSPORT_FILE.tmp" "$PASSPORT_FILE"; }
|
|
488
494
|
echo " ✅ Passport normalized."
|
|
489
495
|
fi
|
|
490
496
|
echo " 📋 Updating passport allowed_commands (default commands: bash, sh, ls, mkdir, npm, …)..."
|
|
@@ -501,12 +507,12 @@ if [ -f "$PASSPORT_FILE" ] && command -v jq &>/dev/null; then
|
|
|
501
507
|
--argjson default "$DEFAULT_CMDS" \
|
|
502
508
|
'$existing + $default | unique')
|
|
503
509
|
fi
|
|
504
|
-
jq --argjson merged "$MERGED" '.limits["system.command.execute"].allowed_commands = $merged' "$PASSPORT_FILE" > "$PASSPORT_FILE.tmp" && mv "$PASSPORT_FILE.tmp" "$PASSPORT_FILE"
|
|
510
|
+
jq --argjson merged "$MERGED" '.limits["system.command.execute"].allowed_commands = $merged' "$PASSPORT_FILE" > "$PASSPORT_FILE.tmp" && { backup_file "$PASSPORT_FILE"; mv "$PASSPORT_FILE.tmp" "$PASSPORT_FILE"; }
|
|
505
511
|
else
|
|
506
512
|
# New nested block: default to ["*"] per README (blocked_patterns still apply)
|
|
507
513
|
MERGED='["*"]'
|
|
508
514
|
jq --argjson merged "$MERGED" \
|
|
509
|
-
'.limits["system.command.execute"] = ((.limits["system.command.execute"] // {}) | .allowed_commands = $merged | .blocked_patterns = (.blocked_patterns // ["rm -rf", "sudo"]) | .max_execution_time = (.max_execution_time // 300))' "$PASSPORT_FILE" > "$PASSPORT_FILE.tmp" && mv "$PASSPORT_FILE.tmp" "$PASSPORT_FILE"
|
|
515
|
+
'.limits["system.command.execute"] = ((.limits["system.command.execute"] // {}) | .allowed_commands = $merged | .blocked_patterns = (.blocked_patterns // ["rm -rf", "sudo"]) | .max_execution_time = (.max_execution_time // 300))' "$PASSPORT_FILE" > "$PASSPORT_FILE.tmp" && { backup_file "$PASSPORT_FILE"; mv "$PASSPORT_FILE.tmp" "$PASSPORT_FILE"; }
|
|
510
516
|
fi
|
|
511
517
|
echo " ✅ Passport updated: default commands are in allowed_commands."
|
|
512
518
|
echo ""
|
package/docs/RELEASE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Release process and version policy
|
|
2
2
|
|
|
3
|
-
**Current release:** 1.0.
|
|
3
|
+
**Current release:** 1.0.13 (see [CHANGELOG.md](../CHANGELOG.md)).
|
|
4
4
|
|
|
5
5
|
We keep **one version number** across all published packages (Node core, Python core, and every framework adapter). That avoids “core is 1.2 but CLI is 0.9” and keeps the story simple for users and support.
|
|
6
6
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# APort Agent Guardrail — Claude Code
|
|
2
|
+
|
|
3
|
+
Claude Code's **PreToolUse** hook runs as a separate process before each tool executes — outside Claude's reasoning context. The model cannot reason past it. This integration registers APort's guardrail with that hook so every tool use (Bash, Write, WebSearch, etc.) is checked against your passport before it runs.
|
|
4
|
+
|
|
5
|
+
**Why this is different from prompts:** A developer shared a session where Claude Code said "That file is outside my writable sandbox" then immediately escaped the sandbox when told to. Advisory guardrails live inside the model's context and can be reasoned around. The PreToolUse hook runs outside that context and cannot be bypassed. See [HN thread 47256614](https://news.ycombinator.com/item?id=47256614).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
- **Settings file:** Claude Code uses `~/.claude/settings.json` (user-level) or `.claude/settings.json` (project-level). This is **not** `~/.cursor/hooks.json` — different location and JSON structure.
|
|
12
|
+
- **PreToolUse hook:** The hook receives JSON on stdin with `tool_name` and `tool_input`, runs the APort guardrail, and must output Claude Code's exact deny format on stdout when blocking: `hookSpecificOutput.permissionDecision: "deny"`. Exit 0 = allow; exit 2 = block.
|
|
13
|
+
- **Hook script:** `bin/aport-claude-code-hook.sh` — maps all Claude Code tool names (Bash, Read, Write, Edit, MultiEdit, Glob, LS, Grep, WebSearch, WebFetch, Browser, TodoRead, TodoWrite, Task, MCP tools) to APort policies and calls the core evaluator.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @aporthq/aport-agent-guardrails claude-code
|
|
21
|
+
# or
|
|
22
|
+
npx @aporthq/aport-agent-guardrails --framework=claude-code
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This runs the **passport wizard** and writes **`~/.claude/settings.json`** with the APort hook registered for **all tools** via `"matcher": "*"`. Default passport path: **`~/.claude/aport/passport.json`**. Restart Claude Code after setup so the PreToolUse hook is picked up.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## What's protected (tool → policy)
|
|
30
|
+
|
|
31
|
+
| Claude Code tool | APort policy | Default |
|
|
32
|
+
|--------------------|---------------------------|----------|
|
|
33
|
+
| Bash | system.command.execute.v1 | Enforce |
|
|
34
|
+
| Read, Glob, LS, Grep, TodoRead | data.file.read.v1 | **Allow by default** (no evaluator call) |
|
|
35
|
+
| Write, Edit, MultiEdit, TodoWrite | data.file.write.v1 | Enforce |
|
|
36
|
+
| WebSearch, WebFetch | web.fetch.v1 | Enforce |
|
|
37
|
+
| Browser | web.browser.v1 | Enforce |
|
|
38
|
+
| Task | agent.session.create.v1 | Enforce |
|
|
39
|
+
| mcp__<server>__<tool> | mcp.tool.execute.v1 | Enforce |
|
|
40
|
+
| **Unknown tool** | — | **Denied (fail-closed)** |
|
|
41
|
+
|
|
42
|
+
Read-family tools (Read, Glob, LS, Grep, TodoRead) exit 0 immediately without calling the evaluator to save ~40ms per file read. The HN incident was about Bash escaping a sandbox — not reads.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## What's NOT protected
|
|
47
|
+
|
|
48
|
+
- **`claude --dangerously-skip-permissions`** — This flag bypasses **all** hooks including PreToolUse. When set, APort is completely inactive. This cannot be mitigated in code; it is an intentional override. Document it prominently and do not rely on APort when this flag is used.
|
|
49
|
+
- **You typing in your terminal** — The hook runs only when the Claude Code agent is about to use a tool. Commands you run yourself are not intercepted.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Testing the guardrail
|
|
54
|
+
|
|
55
|
+
From the repo root (or where the hook script lives):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Allow: Read-family (exit 0, no output)
|
|
59
|
+
echo '{"tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' | bin/aport-claude-code-hook.sh
|
|
60
|
+
echo "Exit: $?"
|
|
61
|
+
|
|
62
|
+
# Allow: Bash with allowed command (exit 0)
|
|
63
|
+
echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' | bin/aport-claude-code-hook.sh
|
|
64
|
+
echo "Exit: $?"
|
|
65
|
+
|
|
66
|
+
# Deny: Bash with blocked pattern (exit 2, hookSpecificOutput JSON)
|
|
67
|
+
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/x"}}' | bin/aport-claude-code-hook.sh
|
|
68
|
+
echo "Exit: $?"
|
|
69
|
+
|
|
70
|
+
# Deny: Unknown tool (fail-closed, exit 2)
|
|
71
|
+
echo '{"tool_name":"UnknownTool","tool_input":{}}' | bin/aport-claude-code-hook.sh
|
|
72
|
+
echo "Exit: $?"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Audit log and config
|
|
78
|
+
|
|
79
|
+
- **Audit log:** `~/.claude/aport/audit.log` (when using default config dir).
|
|
80
|
+
- **Passport:** `~/.claude/aport/passport.json` (default). Path resolver probes `~/.claude` first, then `~/.cursor`, `~/.openclaw`, etc.
|
|
81
|
+
- **Status:** `bin/aport-status.sh` (uses same path resolution).
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Suspend / resume
|
|
86
|
+
|
|
87
|
+
Same as all frameworks: **passport is the source of truth**. Set passport `status` to `suspended` (or `active` to resume). The guardrail denies every call until the passport is active again.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Why this is different from the Cursor doc
|
|
92
|
+
|
|
93
|
+
The Cursor integration uses `~/.cursor/hooks.json` and outputs `permission: allow|deny`. Claude Code uses `~/.claude/settings.json` and expects **`hookSpecificOutput.permissionDecision`** on deny. The output formats are incompatible. Do not use the Cursor hook script for Claude Code; use this integration instead.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Node package (optional)
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm install @aporthq/aport-agent-guardrails-claude-code
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { Evaluator, getHookPath } from '@aporthq/aport-agent-guardrails-claude-code';
|
|
105
|
+
|
|
106
|
+
const hookPath = getHookPath(); // default: ~/.claude/aport-claude-code-hook.sh
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Runtime enforcement is done by the **bash hook**; the package is for programmatic use and hook path resolution.
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
# APort Agent Guardrail — Cursor (and VS Code Copilot
|
|
1
|
+
# APort Agent Guardrail — Cursor (and VS Code Copilot)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Update (v1.0.13):** The claim that the cursor hook works for Claude Code is incorrect.
|
|
4
|
+
> The cursor hook outputs `permission: allow/deny` — Claude Code expects `hookSpecificOutput.permissionDecision`.
|
|
5
|
+
> A dedicated Claude Code integration is now available:
|
|
6
|
+
> ```bash
|
|
7
|
+
> npx @aporthq/aport-agent-guardrails claude-code
|
|
8
|
+
> ```
|
|
9
|
+
> See [docs/frameworks/claude-code.md](./claude-code.md).
|
|
10
|
+
|
|
11
|
+
Cursor and VS Code with GitHub Copilot support **config-driven hooks** that run before shell execution or tool use. The **APort hook script** reads JSON from stdin, calls the existing APort guardrail (policy + passport), and returns allow/deny; **exit 2** blocks the action.
|
|
4
12
|
|
|
5
13
|
## Two ways to use APort
|
|
6
14
|
|
|
@@ -17,9 +25,9 @@ For Cursor, you almost always use **Guardrails (CLI)** once to install the hook;
|
|
|
17
25
|
|
|
18
26
|
- **Hooks:** Cursor uses `~/.cursor/hooks.json` (or `.cursor/hooks.json` in the project). Hooks such as `beforeShellExecution` and `preToolUse` run a command (our script). The host sends JSON to stdin and reads JSON from stdout; **exit code 2** = block.
|
|
19
27
|
- **VS Code Copilot:** Agent hooks (Preview) use `~/.claude/settings.json` or `.github/hooks/*.json` with `PreToolUse`; same idea: command, stdin JSON, stdout JSON, exit 2 = block.
|
|
20
|
-
- **Claude Code:** `~/.claude/settings.json
|
|
28
|
+
- **Claude Code:** Uses `~/.claude/settings.json` with a **different** output format (`hookSpecificOutput.permissionDecision`). Use the **dedicated Claude Code integration** instead of this Cursor hook — see [claude-code.md](./claude-code.md).
|
|
21
29
|
|
|
22
|
-
Our script accepts Cursor- and Copilot-style payloads (e.g. `command`, or `tool`/`input`), maps to the **system.command.execute** policy, calls the bash guardrail, and returns `permission: allow|deny` plus optional `agentMessage`.
|
|
30
|
+
Our script accepts Cursor- and Copilot-style payloads (e.g. `command`, or `tool`/`input`), maps to the **system.command.execute** policy, calls the bash guardrail, and returns `permission: allow|deny` plus optional `agentMessage`.
|
|
23
31
|
|
|
24
32
|
**Hook script path:** The hook script (`aport-cursor-hook.sh`) resolves `bin/aport-guardrail-bash.sh` relative to its own directory (script dir → parent = package root). When you install via **npx**, the installer writes the path to the script inside the npx cache (e.g. `…/node_modules/@aporthq/aport-agent-guardrails/bin/aport-cursor-hook.sh`), so the guardrail script is found at `…/bin/aport-guardrail-bash.sh`. If you copy the hook script elsewhere, ensure `bin/aport-guardrail-bash.sh` exists at the same relative location or set `APORT_GUARDRAIL_SCRIPT` (or equivalent) so the hook can find the evaluator.
|
|
25
33
|
|
|
@@ -121,10 +129,11 @@ Then run `bin/aport-status.sh` and `cat ~/.cursor/aport/audit.log` to confirm th
|
|
|
121
129
|
|
|
122
130
|
Same as all frameworks: **passport is the source of truth**. Set passport `status` to `suspended` (or `active` to resume). The guardrail denies every call until the passport is active again.
|
|
123
131
|
|
|
124
|
-
## Using the same script in VS Code (Copilot)
|
|
132
|
+
## Using the same script in VS Code (Copilot)
|
|
125
133
|
|
|
126
134
|
- **VS Code + GitHub Copilot:** Add a PreToolUse hook in `~/.claude/settings.json` (or project `.claude/settings.json`, or `.github/hooks/*.json`) that runs the same script. See [Agent hooks (Preview)](https://code.visualstudio.com/docs/copilot/customization/hooks).
|
|
127
|
-
|
|
135
|
+
|
|
136
|
+
For **Claude Code**, use the [dedicated Claude Code integration](./claude-code.md) instead — it uses the correct output format (`hookSpecificOutput.permissionDecision`) and supports all Claude Code tool types.
|
|
128
137
|
|
|
129
138
|
The script accepts multiple input shapes (e.g. `command`, `tool`/`input`) and returns the host-expected JSON; **exit 0** = allow, **exit 2** = block.
|
|
130
139
|
|
|
@@ -156,4 +165,4 @@ Runtime enforcement in Cursor is done by the **hook script**, not by this packag
|
|
|
156
165
|
|
|
157
166
|
## Status
|
|
158
167
|
|
|
159
|
-
Implemented (Story E). **APort Agent Guardrail for Cursor.** Installer: `npx @aporthq/aport-agent-guardrails cursor`; hook script: `bin/aport-cursor-hook.sh`; config: `~/.cursor/hooks.json`. Same script usable for VS Code Copilot
|
|
168
|
+
Implemented (Story E). **APort Agent Guardrail for Cursor.** Installer: `npx @aporthq/aport-agent-guardrails cursor`; hook script: `bin/aport-cursor-hook.sh`; config: `~/.cursor/hooks.json`. Same script usable for VS Code Copilot. For Claude Code, use the dedicated integration: `npx @aporthq/aport-agent-guardrails claude-code` (see [claude-code.md](./claude-code.md)).
|