@aporthq/aport-agent-guardrails 1.0.11 → 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 +4 -3
- 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/OPENCLAW_TOOLS_AND_POLICIES.md +18 -0
- package/docs/RELEASE.md +1 -1
- package/docs/SECURITY_MODEL.md +27 -3
- package/docs/VERIFICATION_METHODS.md +1 -0
- package/docs/frameworks/claude-code.md +109 -0
- package/docs/frameworks/crewai.md +5 -0
- package/docs/frameworks/cursor.md +16 -7
- package/docs/frameworks/langchain.md +5 -0
- package/docs/launch/PRD-claude-code-guardrail.md +753 -0
- package/extensions/openclaw-aport/README.md +1 -1
- package/extensions/openclaw-aport/index.ts +60 -1
- package/package.json +2 -2
package/bin/lib/validation.sh
CHANGED
|
@@ -2,15 +2,32 @@
|
|
|
2
2
|
# validation.sh - Input validation functions for security
|
|
3
3
|
# Used to prevent command injection, path traversal, and other injection attacks
|
|
4
4
|
|
|
5
|
-
# Validate command string
|
|
6
|
-
# Returns 0 if safe, 1 if contains dangerous
|
|
5
|
+
# Validate command string for dangerous injection patterns.
|
|
6
|
+
# Returns 0 if safe, 1 if contains dangerous patterns.
|
|
7
|
+
# NOTE: Pipes (|), chains (&&/||), redirects (>), and variable refs ($) are legitimate
|
|
8
|
+
# shell syntax used by Claude Code, Cursor, and other AI tools. We block dangerous
|
|
9
|
+
# OPERATIONS (handled by built-in security patterns and blocked_patterns in the evaluator),
|
|
10
|
+
# not normal shell syntax. This function catches injection-specific patterns only.
|
|
7
11
|
validate_command_string() {
|
|
8
12
|
local cmd="$1"
|
|
9
13
|
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
# Block backtick command substitution (injection vector; $() is caught by patterns below)
|
|
15
|
+
if echo "$cmd" | grep -qF '`'; then
|
|
16
|
+
return 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Block $( ) command substitution containing dangerous commands
|
|
20
|
+
if echo "$cmd" | grep -qE '\$\([^)]*\b(rm|dd|mkfs|curl|wget|chmod|chown|sudo|kill|nc|netcat)\b'; then
|
|
21
|
+
return 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Block null bytes (string termination attack)
|
|
25
|
+
if printf '%s' "$cmd" | grep -qP '\x00' 2> /dev/null; then
|
|
26
|
+
return 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Block control characters (except tab/newline which are normal in shell)
|
|
30
|
+
if printf '%s' "$cmd" | grep -qP '[\x01-\x08\x0e-\x1f]' 2> /dev/null; then
|
|
14
31
|
return 1
|
|
15
32
|
fi
|
|
16
33
|
|
|
@@ -55,10 +72,13 @@ validate_passport_path() {
|
|
|
55
72
|
local abs_path
|
|
56
73
|
abs_path=$(readlink -f "$path" 2> /dev/null || realpath "$path" 2> /dev/null || echo "$path")
|
|
57
74
|
|
|
58
|
-
# Allowed base directories
|
|
75
|
+
# Allowed base directories for passport storage
|
|
59
76
|
local allowed_bases=(
|
|
60
77
|
"$HOME/.openclaw"
|
|
61
78
|
"$HOME/.aport"
|
|
79
|
+
"$HOME/.claude"
|
|
80
|
+
"$HOME/.cursor"
|
|
81
|
+
"$HOME/.n8n"
|
|
62
82
|
"/tmp/aport-"
|
|
63
83
|
)
|
|
64
84
|
|
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 ""
|
|
@@ -32,6 +32,7 @@ Per the **Open Agent Passport (OAP) spec**, the passport has a **limits** object
|
|
|
32
32
|
- **read** and **write** are **now mapped** to APort policies:
|
|
33
33
|
- **read** → `data.file.read.v1` (enforces path allowlists, blocked patterns for SSH keys/credentials/.env)
|
|
34
34
|
- **write** → `data.file.write.v1` (enforces path allowlists, blocks system directories, optional extension restrictions)
|
|
35
|
+
- **Middleware param spreading:** The LangChain and CrewAI Node middlewares automatically parse tool input JSON and spread parameters (e.g. `file_path`) into the top-level verification context. This is required because the API's policy schema validates `file_path` as a required field. Without this, `read`/`write` tool calls would fail with 400 Bad Request.
|
|
35
36
|
- Configure passport limits for file operations:
|
|
36
37
|
- `limits.data.file.read.allowed_paths` - Array of allowed path prefixes (e.g. `["/tmp/*", "/home/user/projects/*"]`)
|
|
37
38
|
- `limits.data.file.read.blocked_patterns` - Array of patterns to block (e.g. `["**/.ssh/**", "**/.env"]`)
|
|
@@ -41,6 +42,23 @@ Per the **Open Agent Passport (OAP) spec**, the passport has a **limits** object
|
|
|
41
42
|
|
|
42
43
|
---
|
|
43
44
|
|
|
45
|
+
## Passport-configurable path overrides
|
|
46
|
+
|
|
47
|
+
The `system.command.execute.v1` policy includes hardcoded security patterns that block access to sensitive system directories (`/etc/`, `/sys/`, `/proc/`, etc.), sensitive hidden files, credential files, and more. Passport owners can override **path-sensitivity heuristics** by setting `limits.allowed_paths` or `limits.allowed_directories` in the passport:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"limits": {
|
|
52
|
+
"allowed_paths": ["/root/", "/home/agent/work/"],
|
|
53
|
+
"allowed_commands": ["*"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
When `allowed_paths` is set and the command references one of those paths, overridable rules (like "Access to sensitive system directories" or "Access to secrets and credentials files") are skipped. **Catastrophic protections are never overridable** — fork bombs, `rm -rf /`, reverse shells, `nc`/`netcat`, and `find -exec rm` are always blocked regardless of passport config.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
44
62
|
## Summary table
|
|
45
63
|
|
|
46
64
|
| OpenClaw tool (examples) | APort policy | Passport limits (key) |
|
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
|
|
package/docs/SECURITY_MODEL.md
CHANGED
|
@@ -30,8 +30,9 @@ APort operates as a **pre-action authorization layer** that enforces policies **
|
|
|
30
30
|
|
|
31
31
|
**Unauthorized tool usage:**
|
|
32
32
|
- Agent tries to execute commands not in allowlist
|
|
33
|
-
- Commands match blocked patterns (`rm -rf`, `sudo`, `chmod 777`, etc.)
|
|
33
|
+
- Commands match blocked patterns (`rm -rf`, `sudo`, `chmod 777`, `nc`/`netcat`, `find -exec rm`, etc.)
|
|
34
34
|
- Shell escapes and interpreter bypasses (python -c, base64 encoding)
|
|
35
|
+
- Stderr redirects (`2>/dev/null`) are allowed; dangerous redirects to files are blocked
|
|
35
36
|
- Result: Only allowlisted, non-dangerous commands execute
|
|
36
37
|
|
|
37
38
|
**Resource exhaustion and limit violations:**
|
|
@@ -135,7 +136,7 @@ APort's three-layer security model:
|
|
|
135
136
|
- Use case: Testing custom policies before registering them
|
|
136
137
|
|
|
137
138
|
**Example policies (out of the box):**
|
|
138
|
-
- `system.command.execute.v1` - Shell commands (allowlist,
|
|
139
|
+
- `system.command.execute.v1` - Shell commands (allowlist, 50+ blocked patterns, passport `allowed_paths` override)
|
|
139
140
|
- `data.file.read.v1` / `data.file.write.v1` - File access control
|
|
140
141
|
- `web.fetch.v1` / `web.browser.v1` - Web requests and browser automation
|
|
141
142
|
- `messaging.message.send.v1` - Message rate limits, recipient allowlist
|
|
@@ -292,6 +293,7 @@ APort's three-layer security model:
|
|
|
292
293
|
APort uses secure defaults out of the box:
|
|
293
294
|
|
|
294
295
|
✅ `failClosed: true` - Block tools on errors (security over availability)
|
|
296
|
+
✅ `fail_open_on_api_error: false` - API infrastructure errors (4xx/5xx, network) also fail closed by default
|
|
295
297
|
✅ `allowUnmappedTools: false` - Unmapped tools blocked (deny-by-default)
|
|
296
298
|
✅ API mode recommended for production
|
|
297
299
|
✅ Passport status checked first (suspended/revoked → deny all)
|
|
@@ -335,6 +337,25 @@ APort uses secure defaults out of the box:
|
|
|
335
337
|
|
|
336
338
|
---
|
|
337
339
|
|
|
340
|
+
#### `fail_open_on_api_error: false` vs `fail_open_on_api_error: true`
|
|
341
|
+
|
|
342
|
+
**False (default, recommended):**
|
|
343
|
+
- API infrastructure errors (4xx/5xx, network failures, timeouts) → deny tool execution
|
|
344
|
+
- Same behavior as `failClosed: true` for API errors
|
|
345
|
+
|
|
346
|
+
**True (use with caution):**
|
|
347
|
+
- API infrastructure errors → allow tool execution with `[fail-open]` warning
|
|
348
|
+
- Genuine policy denials (HTTP 200 with `allow: false`) are **never** overridden—always denied
|
|
349
|
+
- Useful when API availability is unreliable but you still want policy enforcement when the API is reachable
|
|
350
|
+
|
|
351
|
+
**Security impact:** MEDIUM - Only API errors become allow; actual policy denials still block.
|
|
352
|
+
|
|
353
|
+
**When to use true:** Production environments where API downtime shouldn't halt agent operations, combined with monitoring for `[fail-open]` decisions.
|
|
354
|
+
|
|
355
|
+
**Config:** Set in `config.yaml` as `fail_open_on_api_error: true` or env var `APORT_FAIL_OPEN_ON_API_ERROR=1`.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
338
359
|
#### `allowUnmappedTools: false` vs `allowUnmappedTools: true`
|
|
339
360
|
|
|
340
361
|
**False (default, recommended):**
|
|
@@ -546,7 +567,10 @@ A: API for production (signed decisions, protected passport, global suspend). Lo
|
|
|
546
567
|
A: In API mode, policies come from APort API over HTTPS. In local mode, policies are embedded in bash scripts (protected by filesystem permissions). Custom policies can be passed in request body for testing.
|
|
547
568
|
|
|
548
569
|
**Q: What if the API is down?**
|
|
549
|
-
A: Default behavior (failClosed: true) denies tool calls. For high-availability scenarios, consider running a self-hosted agent-passport instance or use local mode as fallback.
|
|
570
|
+
A: Default behavior (failClosed: true) denies tool calls. You can set `fail_open_on_api_error: true` to allow on API infrastructure errors while still enforcing genuine policy denials. For high-availability scenarios, consider running a self-hosted agent-passport instance or use local mode as fallback.
|
|
571
|
+
|
|
572
|
+
**Q: Can passport owners override blocked path rules (e.g. allow /root/)?**
|
|
573
|
+
A: Yes. Set `limits.allowed_paths` (or `limits.allowed_directories`) in the passport to override path-sensitivity heuristics like "Access to sensitive system directories." Catastrophic protections (fork bombs, `rm -rf /`, reverse shells, `nc`/`netcat`) can **never** be overridden by passport config.
|
|
550
574
|
|
|
551
575
|
**Q: Can decisions be tampered with?**
|
|
552
576
|
A: Local mode: hash-protected (detects naive tampering). API mode: cryptographically signed (Ed25519, cannot forge).
|
|
@@ -93,5 +93,6 @@ So: **local is robust enough for the core policies** (exec, messaging, repo merg
|
|
|
93
93
|
|
|
94
94
|
- **Local (bash):** Useful for privacy, offline, and the core use cases (exec allowlist/blocklist, messaging recipient, repo/PR limits). For full OAP parity and future policy packs, use **API mode**.
|
|
95
95
|
- **API (default):** Recommended for production and when you want the same behavior as [APort in Goose](https://raw.githubusercontent.com/aporthq/.github/refs/heads/main/profile/APORT_GOOSE_ARCHITECTURE.md) and the full generic evaluator (JSON Schema, assurance, regions, evaluation_rules, signed decisions).
|
|
96
|
+
- **`fail_open_on_api_error`:** In API mode, set this config option to `true` if you want API infrastructure errors (4xx/5xx, network failures) to return allow instead of deny. Genuine policy denials (HTTP 200 with `allow: false`) are **never** overridden. Default: `false` (fail-closed on API errors). Set via config YAML or env var `APORT_FAIL_OPEN_ON_API_ERROR=1`.
|
|
96
97
|
|
|
97
98
|
The installer (`./bin/openclaw`) defaults to **API mode**; choose local only when you need to run without the network.
|
|
@@ -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.
|
|
@@ -95,10 +95,15 @@ withAPortGuardrail(() => {
|
|
|
95
95
|
});
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
### How tool parameters are handled
|
|
99
|
+
|
|
100
|
+
The Node middleware automatically spreads object tool inputs into the verification context. For example, `{ tool_input: { file_path: "/tmp/data.txt" } }` will pass `file_path` at the top level of the context, ensuring policies like `data.file.read.v1` receive required fields for validation.
|
|
101
|
+
|
|
98
102
|
## Config
|
|
99
103
|
|
|
100
104
|
- **Config path:** `~/.aport/crewai/config.yaml`, or `.aport/config.yaml` in the project root.
|
|
101
105
|
- **Mode:** `api` (default for production) or `local` (bash evaluator, no network). Same options as [LangChain](langchain.md) and OpenClaw.
|
|
106
|
+
- **`fail_open_on_api_error`**: Set to `true` in config to allow tool execution when the APort API is unreachable (genuine policy denials are never overridden). Default: `false` (fail-closed).
|
|
102
107
|
|
|
103
108
|
## Suspend (kill switch)
|
|
104
109
|
|
|
@@ -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)).
|
|
@@ -58,10 +58,15 @@ const callback = new APortGuardrailCallback(); // optional: { configPath: '...',
|
|
|
58
58
|
// On deny, the callback throws GuardrailViolationError.
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### How tool parameters are handled
|
|
62
|
+
|
|
63
|
+
The Node middleware automatically parses JSON tool input and spreads parameters (e.g. `file_path`, `command`) into the verification context. This ensures policies like `data.file.read.v1` and `data.file.write.v1` receive the required `file_path` field at the top level for proper validation.
|
|
64
|
+
|
|
61
65
|
## Config
|
|
62
66
|
|
|
63
67
|
- **Config:** `~/.aport/langchain/` or `.aport/config.yaml`
|
|
64
68
|
- **Usage:** Add the callback to your agent (see above).
|
|
69
|
+
- **`fail_open_on_api_error`**: Set to `true` in config to allow tool execution when the APort API is unreachable (genuine policy denials are never overridden). Default: `false` (fail-closed).
|
|
65
70
|
|
|
66
71
|
## Suspend (kill switch)
|
|
67
72
|
|