@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 CHANGED
@@ -60,7 +60,7 @@ Your agent should **only do what you explicitly allow**. APort runs in the hook
60
60
 
61
61
  | Policy | What it guards |
62
62
  |--------|----------------|
63
- | **system.command.execute.v1** | Shell commands — allowlist, 40+ blocked patterns (`rm -rf`, `sudo`, injection) |
63
+ | **system.command.execute.v1** | Shell commands — allowlist, 50+ blocked patterns (`rm -rf`, `sudo`, `nc`, `find -exec rm`, injection); passport `allowed_paths` override for path rules |
64
64
  | **mcp.tool.execute.v1** | MCP tool calls — server allowlist, rate limits |
65
65
  | **messaging.message.send.v1** | Message sends — rate caps, capability checks |
66
66
  | **agent.session.create.v1** / **agent.tool.register.v1** | Sessions and tool registration |
@@ -86,12 +86,13 @@ The **kill switch suspends the agent**: once active, every tool call is denied u
86
86
 
87
87
  **Two ways to use APort:** (1) **Guardrails (CLI/setup)** — run the installer to create your passport and config; (2) **Core (library)** — use the evaluator or framework callback in your app so each tool call is verified. Each framework doc ([LangChain](docs/frameworks/langchain.md), [CrewAI](docs/frameworks/crewai.md), [Cursor](docs/frameworks/cursor.md), [OpenClaw](docs/frameworks/openclaw.md)) describes both and how to use them for that framework (Python and Node where applicable).
88
88
 
89
- **Production-ready today:** OpenClaw (plugin + full installer), Cursor (hooks), **Python** LangChain/CrewAI (`pip install aport-agent-guardrails-langchain` / `aport-agent-guardrails-crewai`), and **Node** (CLI + `@aporthq/aport-agent-guardrails-core`, `-langchain`, `-crewai`, `-cursor` published via CI). See [Deployment readiness](docs/DEPLOYMENT_READINESS.md).
89
+ **Production-ready today:** OpenClaw (plugin + full installer), Cursor (hooks), **Claude Code** (PreToolUse hook), **Python** LangChain/CrewAI (`pip install aport-agent-guardrails-langchain` / `aport-agent-guardrails-crewai`), and **Node** (CLI + `@aporthq/aport-agent-guardrails-core`, `-langchain`, `-crewai`, `-cursor`, `-claude-code` published via CI). See [Deployment readiness](docs/DEPLOYMENT_READINESS.md).
90
90
 
91
91
  | Framework | Doc | Integration | Install |
92
92
  |-----------|-----|--------------|--------|
93
93
  | **OpenClaw** | [docs/frameworks/openclaw.md](docs/frameworks/openclaw.md) | `before_tool_call` plugin | `npx @aporthq/aport-agent-guardrails openclaw` |
94
94
  | **Cursor** | [docs/frameworks/cursor.md](docs/frameworks/cursor.md) | `beforeShellExecution` / `preToolUse` hooks → writes `~/.cursor/hooks.json`. **Runtime enforcement is the bash hook;** the Node package `@aporthq/aport-agent-guardrails-cursor` is a helper only (Evaluator, `getHookPath()`). | `npx @aporthq/aport-agent-guardrails cursor` |
95
+ | **Claude Code** | [docs/frameworks/claude-code.md](docs/frameworks/claude-code.md) | PreToolUse hook → writes `~/.claude/settings.json` (Claude Code format; not Cursor). | `npx @aporthq/aport-agent-guardrails claude-code` |
95
96
  | **LangChain / LangGraph** | [docs/frameworks/langchain.md](docs/frameworks/langchain.md) | **Python:** `APortCallback` (`on_tool_start`) | `npx @aporthq/aport-agent-guardrails langchain` then `pip install aport-agent-guardrails-langchain` + `aport-langchain setup` |
96
97
  | **CrewAI** | [docs/frameworks/crewai.md](docs/frameworks/crewai.md) | **Python:** `@before_tool_call` hook, `register_aport_guardrail` | `npx @aporthq/aport-agent-guardrails crewai` then `pip install aport-agent-guardrails-crewai` + `aport-crewai setup` |
97
98
  | **n8n** | [docs/frameworks/n8n.md](docs/frameworks/n8n.md) | *Coming soon* — custom node and runtime in progress | — |
@@ -335,7 +336,7 @@ graph TB
335
336
  **✅ Pre-action authorization (agent misbehavior):**
336
337
  - **Prompt injection** - Hook-based enforcement; agent cannot bypass via prompts
337
338
  - **Malicious skills** - Third-party OpenClaw skills validated before execution
338
- - **Unauthorized commands** - Allowlist + 40+ blocked patterns (rm -rf, sudo, etc.)
339
+ - **Unauthorized commands** - Allowlist + 50+ blocked patterns (rm -rf, sudo, nc, find -exec rm, etc.)
339
340
  - **Data exfiltration** - File access, messaging, web requests controlled by policy
340
341
  - **Resource limits** - Rate limits, size caps, transaction amounts enforced
341
342
 
@@ -6,7 +6,7 @@
6
6
  # agent-guardrails openclaw ap_xxx # openclaw with agent_id (pass-through)
7
7
  # agent-guardrails --framework=langchain
8
8
  # agent-guardrails -f crewai
9
- # Supported: openclaw | langchain | crewai | cursor (n8n coming soon)
9
+ # Supported: openclaw | langchain | crewai | cursor | claude-code (n8n coming soon)
10
10
  #
11
11
  # Backward compatibility: All arguments after the framework are passed through to
12
12
  # the framework script. For OpenClaw, bin/openclaw receives them (e.g. agent_id).
@@ -72,14 +72,14 @@ if [[ -z "$framework" ]]; then
72
72
  noninteractive="${APORT_NONINTERACTIVE:-${CI:-}}"
73
73
  if [[ -n "$noninteractive" ]]; then
74
74
  echo "[aport] ERROR: Multiple frameworks detected: $detected_list" >&2
75
- echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor" >&2
75
+ echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor | claude-code" >&2
76
76
  exit 1
77
77
  fi
78
78
  echo ""
79
79
  echo " APort Agent Guardrails — Framework setup"
80
80
  echo " ─────────────────────────────────────────"
81
81
  echo " Multiple frameworks detected: $detected_list"
82
- echo " Choose one: openclaw | langchain | crewai | cursor"
82
+ echo " Choose one: openclaw | langchain | crewai | cursor | claude-code"
83
83
  echo " Example: npx @aporthq/agent-guardrails --framework=openclaw"
84
84
  echo ""
85
85
  read -p " Framework [${framework:-openclaw}]: " choice
@@ -95,14 +95,14 @@ if [[ -z "$framework" ]]; then
95
95
  noninteractive="${APORT_NONINTERACTIVE:-${CI:-}}"
96
96
  if [[ -n "$noninteractive" ]]; then
97
97
  echo "[aport] ERROR: No framework detected in current directory." >&2
98
- echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor" >&2
98
+ echo " Set APORT_FRAMEWORK or use --framework=openclaw | langchain | crewai | cursor | claude-code" >&2
99
99
  exit 1
100
100
  fi
101
101
  echo ""
102
102
  echo " APort Agent Guardrails — Framework setup"
103
103
  echo " ─────────────────────────────────────────"
104
104
  echo " No framework detected in current directory."
105
- echo " Choose: openclaw | langchain | crewai | cursor"
105
+ echo " Choose: openclaw | langchain | crewai | cursor | claude-code"
106
106
  echo " Example: npx @aporthq/agent-guardrails openclaw"
107
107
  echo " npx @aporthq/agent-guardrails --framework=langchain"
108
108
  echo ""
@@ -111,6 +111,13 @@ if [[ -z "$framework" ]]; then
111
111
  fi
112
112
 
113
113
  framework="$(echo "$framework" | tr '[:upper:]' '[:lower:]')"
114
+
115
+ # SECURITY: Validate framework name (alphanumeric + hyphen only, prevents path traversal)
116
+ if [[ ! "$framework" =~ ^[a-z0-9-]+$ ]]; then
117
+ echo "[aport] ERROR: Invalid framework name: $framework (alphanumeric and hyphens only)" >&2
118
+ exit 1
119
+ fi
120
+
114
121
  script="$FRAMEWORKS_DIR/${framework}.sh"
115
122
 
116
123
  # n8n: custom node not yet available; warn before running wizard-only script
@@ -129,5 +136,5 @@ if [[ "$framework" == "openclaw" ]]; then
129
136
  fi
130
137
 
131
138
  echo "[aport] ERROR: Unknown or unsupported framework: $framework" >&2
132
- echo " Supported: openclaw, langchain, crewai, cursor (n8n coming soon)" >&2
139
+ echo " Supported: openclaw, langchain, crewai, cursor, claude-code (n8n coming soon)" >&2
133
140
  exit 1
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bash
2
+ # APort Claude Code hook: reads tool_name + tool_input from JSON stdin,
3
+ # maps to APort policy, calls guardrail, outputs hookSpecificOutput deny or exit 0.
4
+ # Exit 0 = allow, exit 2 = block. Other exits = hook error (Claude Code may fail-open).
5
+ # Output format: Claude Code official schema (hookSpecificOutput.permissionDecision), NOT Cursor format.
6
+
7
+ set -e
8
+
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
10
+ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
11
+ GUARDRAIL="$ROOT_DIR/bin/aport-guardrail-bash.sh"
12
+
13
+ # Path resolver: probes ~/.claude, ~/.cursor, ~/.openclaw, etc.
14
+ # shellcheck source=bin/aport-resolve-paths.sh
15
+ . "$ROOT_DIR/bin/aport-resolve-paths.sh"
16
+
17
+ # Read stdin
18
+ INPUT=""
19
+ if [ -t 0 ]; then
20
+ INPUT='{}'
21
+ else
22
+ INPUT="$(cat)"
23
+ fi
24
+
25
+ # No input = allow (fail-open for bad input)
26
+ [ -z "$INPUT" ] && exit 0
27
+
28
+ # Parse tool_name and tool_input (requires jq)
29
+ if ! command -v jq &> /dev/null; then
30
+ echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"🛡️ APort: jq is required"}}'
31
+ exit 2
32
+ fi
33
+
34
+ # Parse with error handling: jq failure must exit 2 (deny), never undefined exit codes
35
+ set +e
36
+ TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name // "unknown"' 2> /dev/null)"
37
+ JQ_EXIT=$?
38
+ set -e
39
+ if [ "$JQ_EXIT" -ne 0 ] || [ -z "$TOOL_NAME" ]; then
40
+ TOOL_NAME="unknown"
41
+ fi
42
+ set +e
43
+ TOOL_INPUT="$(echo "$INPUT" | jq -c '.tool_input // {}' 2> /dev/null)"
44
+ JQ_EXIT=$?
45
+ set -e
46
+ if [ "$JQ_EXIT" -ne 0 ] || [ -z "$TOOL_INPUT" ]; then
47
+ TOOL_INPUT='{}'
48
+ fi
49
+
50
+ # Safe jq extraction: returns '{}' on any jq error instead of crashing under set -e
51
+ safe_jq() {
52
+ local input="$1" filter="$2"
53
+ local result
54
+ result="$(echo "$input" | jq -c "$filter" 2> /dev/null)" || result='{}'
55
+ [ -z "$result" ] && result='{}'
56
+ echo "$result"
57
+ }
58
+
59
+ # Deny helper: outputs Claude Code hookSpecificOutput JSON and exits 2
60
+ deny() {
61
+ local reason="$1"
62
+ jq -n --arg reason "$reason" \
63
+ --arg event "PreToolUse" \
64
+ '{hookSpecificOutput:{hookEventName:$event,permissionDecision:"deny",permissionDecisionReason:$reason}}'
65
+ exit 2
66
+ }
67
+
68
+ # Tool name passed to guardrail (must match aport-guardrail-bash.sh case patterns)
69
+ GUARDRAIL_TOOL=""
70
+ CONTEXT_JSON="{}"
71
+
72
+ case "$TOOL_NAME" in
73
+ Bash)
74
+ GUARDRAIL_TOOL="bash"
75
+ CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{command: (.command // "")}')"
76
+ ;;
77
+ Read | Glob | LS | Grep | TodoRead | ToolSearch | AskUserQuestion)
78
+ # Read-family + user-interaction tools: allow without calling evaluator
79
+ exit 0
80
+ ;;
81
+ TaskGet | TaskList | TaskOutput | CronList)
82
+ # Read-only task/cron queries: allow without evaluator
83
+ exit 0
84
+ ;;
85
+ EnterPlanMode | ExitPlanMode)
86
+ # Internal state transitions: allow without evaluator
87
+ exit 0
88
+ ;;
89
+ Write | Edit | MultiEdit | NotebookEdit | TodoWrite)
90
+ GUARDRAIL_TOOL="write"
91
+ CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{file_path: (.file_path // .path // "")}')"
92
+ ;;
93
+ WebSearch | WebFetch)
94
+ GUARDRAIL_TOOL="webfetch"
95
+ CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{url: (.url // .query // "")}')"
96
+ ;;
97
+ Browser)
98
+ GUARDRAIL_TOOL="browser"
99
+ CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{url: (.url // "")}')"
100
+ ;;
101
+ Task | TaskCreate | TaskUpdate | TaskStop | Agent | Skill | EnterWorktree)
102
+ GUARDRAIL_TOOL="session.create"
103
+ CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{description: (.description // .prompt // "")}')"
104
+ ;;
105
+ CronCreate | CronDelete)
106
+ GUARDRAIL_TOOL="cron"
107
+ CONTEXT_JSON="$(safe_jq "$TOOL_INPUT" '{description: (.description // .schedule // "")}')"
108
+ ;;
109
+ mcp__*)
110
+ GUARDRAIL_TOOL="mcp.tool"
111
+ CONTEXT_JSON="$TOOL_INPUT"
112
+ ;;
113
+ unknown | *)
114
+ # Unknown tool: fail-closed (deny)
115
+ deny "🛡️ APort: unknown tool '$TOOL_NAME' — fail-closed policy"
116
+ ;;
117
+ esac
118
+
119
+ # Use a per-invocation decision file to avoid race conditions with concurrent tool calls
120
+ HOOK_DECISION_FILE="${OPENCLAW_DECISION_FILE:-}"
121
+ if [ -n "$HOOK_DECISION_FILE" ]; then
122
+ HOOK_DECISION_FILE="${HOOK_DECISION_FILE%.json}-$$.json"
123
+ export OPENCLAW_DECISION_FILE="$HOOK_DECISION_FILE"
124
+ fi
125
+
126
+ # Call core evaluator (guardrail expects tool name, not policy ID)
127
+ set +e
128
+ "$GUARDRAIL" "$GUARDRAIL_TOOL" "$CONTEXT_JSON" 2> /dev/null
129
+ GUARDRAIL_EXIT=$?
130
+ set -e
131
+
132
+ # Clean up per-invocation decision file on exit
133
+ cleanup_decision() { [ -n "$HOOK_DECISION_FILE" ] && rm -f "$HOOK_DECISION_FILE" 2> /dev/null; }
134
+
135
+ if [ "$GUARDRAIL_EXIT" -eq 0 ]; then
136
+ cleanup_decision
137
+ exit 0
138
+ fi
139
+
140
+ # Deny: read reason from decision file
141
+ REASON="Policy denied this action."
142
+ if [ -n "$HOOK_DECISION_FILE" ] && [ -f "$HOOK_DECISION_FILE" ] && command -v jq &> /dev/null; then
143
+ R="$(jq -r '.reasons[0].message // empty' "$HOOK_DECISION_FILE" 2> /dev/null)"
144
+ [ -n "$R" ] && REASON="$R"
145
+ fi
146
+ cleanup_decision
147
+ deny "🛡️ APort: $REASON"
@@ -503,6 +503,11 @@ else
503
503
  EOF
504
504
  fi
505
505
 
506
+ # Archive existing passport before overwrite
507
+ if [ -f "$PASSPORT_FILE" ]; then
508
+ cp "$PASSPORT_FILE" "${PASSPORT_FILE}.bak"
509
+ fi
510
+
506
511
  # Format JSON with jq if available
507
512
  if command -v jq &> /dev/null; then
508
513
  jq . "$PASSPORT_FILE.tmp" > "$PASSPORT_FILE"
@@ -44,8 +44,9 @@ if [ -n "$DEBUG_APORT" ]; then
44
44
  echo "DEBUG: CONTEXT length=${#CONTEXT_JSON}" >&2
45
45
  fi
46
46
 
47
- # Ensure APort data dir exists (for decision.json, audit.log)
47
+ # Ensure APort data dir exists (for decision.json, audit.log) with restricted permissions
48
48
  mkdir -p "$(dirname "$AUDIT_LOG")"
49
+ chmod 700 "$(dirname "$AUDIT_LOG")" 2> /dev/null || true
49
50
 
50
51
  # Function to load policy from upstream or local-overrides
51
52
  load_policy() {
@@ -160,9 +161,11 @@ write_decision() {
160
161
  local final_json
161
162
  final_json=$(echo "$base_json" | jq -c --arg h "$content_hash" '. + {content_hash: $h}')
162
163
  echo "$final_json" > "$DECISION_FILE"
164
+ chmod 600 "$DECISION_FILE" 2> /dev/null || true
163
165
 
164
166
  # Update chain state for next decision (best-effort; do not block or fail the script)
165
167
  echo "{\"last_decision_id\":\"$decision_id\",\"last_content_hash\":\"$content_hash\"}" > "$chain_state" 2> /dev/null || true
168
+ chmod 600 "$chain_state" 2> /dev/null || true
166
169
 
167
170
  audit_context=""
168
171
  if [ -n "${CONTEXT_SUMMARY:-}" ]; then
@@ -514,8 +517,9 @@ if [[ "$POLICY_ID" == "data.file.write.v1" ]]; then
514
517
  fi
515
518
  done < <(echo "$LIMITS" | jq -r '.blocked_paths[]? // empty')
516
519
 
517
- # Check allowed extensions (if configured)
518
- if [ -n "$(echo "$LIMITS" | jq -r '.allowed_extensions | length' 2> /dev/null)" ]; then
520
+ # Check allowed extensions (only when the list has entries)
521
+ _ext_count="$(echo "$LIMITS" | jq -r 'if .allowed_extensions then (.allowed_extensions | length) else 0 end' 2> /dev/null || echo "0")"
522
+ if [ "$_ext_count" -gt 0 ] 2> /dev/null; then
519
523
  FILE_EXT=$(echo "$FILE_PATH" | grep -o '\.[^.]*$' | tr '[:upper:]' '[:lower:]')
520
524
  if [ -n "$FILE_EXT" ]; then
521
525
  EXT_ALLOWED=false
@@ -14,8 +14,24 @@ resolve_aport_paths() {
14
14
  local passport_path
15
15
  local data_dir
16
16
 
17
+ # Source validation if available (for validate_passport_path)
18
+ local _resolve_script_dir
19
+ _resolve_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+ if [ -f "$_resolve_script_dir/lib/validation.sh" ]; then
21
+ # shellcheck source=lib/validation.sh
22
+ . "$_resolve_script_dir/lib/validation.sh" 2> /dev/null || true
23
+ elif [ -f "$_resolve_script_dir/../bin/lib/validation.sh" ]; then
24
+ . "$_resolve_script_dir/../bin/lib/validation.sh" 2> /dev/null || true
25
+ fi
26
+
17
27
  # 1) Explicit path set and file exists → use it (plugin or wrapper)
18
28
  if [ -n "${OPENCLAW_PASSPORT_FILE:-}" ] && [ -f "$OPENCLAW_PASSPORT_FILE" ]; then
29
+ # Validate env-provided path if validator is available
30
+ if type validate_passport_path &> /dev/null; then
31
+ if ! validate_passport_path "$OPENCLAW_PASSPORT_FILE"; then
32
+ echo "[aport] WARN: OPENCLAW_PASSPORT_FILE path failed validation: $OPENCLAW_PASSPORT_FILE" >&2
33
+ fi
34
+ fi
19
35
  data_dir="$(dirname "$OPENCLAW_PASSPORT_FILE")"
20
36
  passport_path="$OPENCLAW_PASSPORT_FILE"
21
37
  # 2) Explicit path set but file missing → legacy: try parent dir (e.g. .../openclaw/passport.json)
@@ -31,7 +47,7 @@ resolve_aport_paths() {
31
47
  # 3) No env → probe framework-specific default paths (where each framework stores data), then OpenClaw legacy
32
48
  else
33
49
  config_dir=""
34
- for candidate in "$HOME/.cursor" "$HOME/.openclaw" "$HOME/.aport/langchain" "$HOME/.aport/crewai" "$HOME/.n8n"; do
50
+ for candidate in "$HOME/.claude" "$HOME/.cursor" "$HOME/.openclaw" "$HOME/.aport/langchain" "$HOME/.aport/crewai" "$HOME/.n8n"; do
35
51
  if [ -f "${candidate}/aport/passport.json" ]; then
36
52
  config_dir="$candidate"
37
53
  break
@@ -218,11 +218,17 @@ if [ -f "$AUDIT_LOG" ] && [ -s "$AUDIT_LOG" ]; then
218
218
  # Parse log line: [timestamp] tool=X decision_id=Y allow=Z ... context="..." (optional)
219
219
  timestamp=$(echo "$line" | sed -n 's/.*\[\([^]]*\)\].*/\1/p')
220
220
  tool=$(echo "$line" | sed -n 's/.*tool=\([^ ]*\).*/\1/p')
221
+ decision_id=$(echo "$line" | sed -n 's/.*decision_id=\([^ ]*\).*/\1/p')
221
222
  allow=$(echo "$line" | sed -n 's/.*allow=\([^ ]*\).*/\1/p')
222
223
  context=$(echo "$line" | sed -n 's/.*context="\([^"]*\)".*/\1/p')
223
224
  timestamp=${timestamp:-unknown}
224
225
  tool=${tool:-unknown}
225
226
  allow=${allow:-unknown}
227
+ # Synthetic decision_id for old log formats that lack one
228
+ if [ -z "$decision_id" ]; then
229
+ ctx_slug=$(printf '%.16s' "${context:-}" | tr ' ' '_')
230
+ decision_id="${timestamp}-${tool}-${ctx_slug}"
231
+ fi
226
232
 
227
233
  short_time=$(echo "$timestamp" | cut -d' ' -f1-2 | cut -d'.' -f1)
228
234
  # Show capability + context when present (e.g. "exec.run | cat test.md"); truncate long context
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code framework installer/setup.
3
+ # Runs passport wizard and writes ~/.claude/settings.json with PreToolUse hook
4
+ # pointing at aport-claude-code-hook.sh. Format is Claude Code official schema
5
+
6
+ LIB="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")/../lib" && pwd)"
7
+ # shellcheck source=../lib/common.sh
8
+ source "$LIB/common.sh"
9
+ # shellcheck source=../lib/passport.sh
10
+ source "$LIB/passport.sh"
11
+ # shellcheck source=../lib/config.sh
12
+ source "$LIB/config.sh"
13
+
14
+ run_setup() {
15
+ log_info "Setting up APort guardrails for Claude Code..."
16
+ config_dir="$(get_config_dir claude-code)"
17
+ config_dir="${config_dir/#\~/$HOME}"
18
+ mkdir -p "$config_dir/aport"
19
+ chmod 700 "$config_dir/aport"
20
+
21
+ export APORT_FRAMEWORK=claude-code
22
+ run_passport_wizard "$@"
23
+
24
+ # Harden permissions on passport (contains policy/capabilities)
25
+ [ -f "$config_dir/aport/passport.json" ] && chmod 600 "$config_dir/aport/passport.json"
26
+
27
+ # Resolve absolute path to hook script (works from repo or npx package)
28
+ HOOK_SCRIPT="${APORT_CLAUDE_CODE_HOOK_SCRIPT:-}"
29
+ if [ -z "$HOOK_SCRIPT" ]; then
30
+ ROOT_FOR_HOOK="$(cd "$LIB/../.." && pwd)"
31
+ HOOK_SCRIPT="$ROOT_FOR_HOOK/bin/aport-claude-code-hook.sh"
32
+ fi
33
+ if [ ! -f "$HOOK_SCRIPT" ]; then
34
+ log_warn "Hook script not found at $HOOK_SCRIPT; settings.json will reference it (create the file for hooks to work)."
35
+ else
36
+ HOOK_SCRIPT="$(cd "$(dirname "$HOOK_SCRIPT")" && pwd)/$(basename "$HOOK_SCRIPT")"
37
+ fi
38
+
39
+ CLAUDE_DIR="${APORT_CLAUDE_CODE_CONFIG_DIR:-$HOME/.claude}"
40
+ CLAUDE_DIR="${CLAUDE_DIR/#\~/$HOME}"
41
+ SETTINGS_FILE="$CLAUDE_DIR/settings.json"
42
+ mkdir -p "$CLAUDE_DIR"
43
+
44
+ _write_claude_settings "$SETTINGS_FILE" "$HOOK_SCRIPT"
45
+ chmod 600 "$SETTINGS_FILE"
46
+
47
+ echo ""
48
+ echo " Next steps (Claude Code):"
49
+ echo " ─────────────────────────"
50
+ echo " 1. Settings written to: $SETTINGS_FILE"
51
+ echo " 2. Hook script: $HOOK_SCRIPT"
52
+ echo " 3. Restart Claude Code so the PreToolUse hook is picked up."
53
+ echo " 4. Tool use will be checked by APort policy (exit 2 = block)."
54
+ echo ""
55
+ echo " Audit log: $config_dir/aport/audit.log"
56
+ echo ""
57
+ echo " Note: claude --dangerously-skip-permissions bypasses ALL hooks including APort."
58
+ echo " See: docs/frameworks/claude-code.md"
59
+ echo ""
60
+ }
61
+
62
+ # Write ~/.claude/settings.json in Claude Code official format (PreToolUse, matcher "*").
63
+ # Merges with existing settings.json without clobbering other settings.
64
+ _write_claude_settings() {
65
+ local file="$1"
66
+ local cmd="$2"
67
+
68
+ if [ -f "$file" ] && command -v jq &> /dev/null; then
69
+ if jq -e '.hooks' "$file" &> /dev/null; then
70
+ # Merge: add APort to PreToolUse array, dedup by command
71
+ local tmpfile
72
+ tmpfile="$(mktemp "${file}.XXXXXX")"
73
+ jq -c --arg cmd "$cmd" '
74
+ (.hooks.PreToolUse // []) as $p |
75
+ .hooks.PreToolUse = ($p | map(select(
76
+ (.hooks[0].command != $cmd)
77
+ )) | . + [{"matcher":"*","hooks":[{"type":"command","command":$cmd}]}])
78
+ ' "$file" > "$tmpfile" && mv "$tmpfile" "$file"
79
+ return
80
+ fi
81
+ fi
82
+
83
+ # Write fresh settings (Claude Code format: hooks.PreToolUse, matcher "*")
84
+ if command -v jq &> /dev/null; then
85
+ jq -n --arg cmd "$cmd" '{
86
+ hooks: {
87
+ PreToolUse: [{"matcher":"*","hooks":[{"type":"command","command":$cmd}]}]
88
+ }
89
+ }' > "$file"
90
+ else
91
+ # Escape special JSON characters in cmd path (quotes, backslashes)
92
+ local escaped_cmd
93
+ escaped_cmd="$(printf '%s' "$cmd" | sed 's/\\/\\\\/g; s/"/\\"/g')"
94
+ cat > "$file" << EOF
95
+ {
96
+ "hooks": {
97
+ "PreToolUse": [
98
+ {
99
+ "matcher": "*",
100
+ "hooks": [
101
+ {
102
+ "type": "command",
103
+ "command": "${escaped_cmd}"
104
+ }
105
+ ]
106
+ }
107
+ ]
108
+ }
109
+ }
110
+ EOF
111
+ fi
112
+ }
113
+
114
+ run_setup "$@"
@@ -14,8 +14,11 @@ run_setup() {
14
14
  log_info "Setting up APort guardrails for CrewAI..."
15
15
  config_dir="$(write_config_template crewai)"
16
16
  mkdir -p "$config_dir/aport"
17
+ chmod 700 "$config_dir/aport"
17
18
  export APORT_FRAMEWORK=crewai
18
19
  run_passport_wizard "$@"
20
+ # Harden permissions on passport (contains policy/capabilities)
21
+ [ -f "$config_dir/aport/passport.json" ] && chmod 600 "$config_dir/aport/passport.json"
19
22
  echo ""
20
23
  echo " Next steps (CrewAI):"
21
24
  echo " ───────────────────"
@@ -16,10 +16,14 @@ run_setup() {
16
16
  # Passport and data live under Cursor's config dir (~/.cursor/aport/ by default).
17
17
  config_dir="$(get_config_dir cursor)"
18
18
  mkdir -p "$config_dir/aport"
19
+ chmod 700 "$config_dir/aport"
19
20
 
20
21
  export APORT_FRAMEWORK=cursor
21
22
  run_passport_wizard "$@"
22
23
 
24
+ # Harden permissions on passport (contains policy/capabilities)
25
+ [ -f "$config_dir/aport/passport.json" ] && chmod 600 "$config_dir/aport/passport.json"
26
+
23
27
  # Resolve absolute path to hook script (works from repo or npx package)
24
28
  HOOK_SCRIPT="${APORT_CURSOR_HOOK_SCRIPT:-}"
25
29
  if [ -z "$HOOK_SCRIPT" ]; then
@@ -48,6 +52,7 @@ run_setup() {
48
52
  .hooks.beforeShellExecution = ($b | map(select(.command != $cmd)) | . + [{ "command": $cmd }]) |
49
53
  .hooks.preToolUse = ($p | map(select(.command != $cmd)) | . + [{ "command": $cmd }])
50
54
  ')
55
+ [ -f "$CURSOR_HOOKS_FILE" ] && cp "$CURSOR_HOOKS_FILE" "${CURSOR_HOOKS_FILE}.bak"
51
56
  echo "$NEW_HOOKS" > "$CURSOR_HOOKS_FILE"
52
57
  else
53
58
  _write_cursor_hooks_file "$CURSOR_HOOKS_FILE" "$HOOK_SCRIPT"
@@ -55,6 +60,7 @@ run_setup() {
55
60
  else
56
61
  _write_cursor_hooks_file "$CURSOR_HOOKS_FILE" "$HOOK_SCRIPT"
57
62
  fi
63
+ chmod 600 "$CURSOR_HOOKS_FILE"
58
64
 
59
65
  echo ""
60
66
  echo " Next steps (Cursor):"
@@ -64,7 +70,7 @@ run_setup() {
64
70
  echo " 3. Restart Cursor (or reload window) so hooks are picked up."
65
71
  echo " 4. Shell commands and tool use will be checked by APort policy (exit 2 = block)."
66
72
  echo ""
67
- echo " Same script works for VS Code + Copilot and Claude Code see: docs/frameworks/cursor.md"
73
+ echo " Same script works for VS Code + Copilot. For Claude Code use the dedicated integration: docs/frameworks/claude-code.md"
68
74
  echo ""
69
75
  }
70
76
 
@@ -80,12 +86,14 @@ _write_cursor_hooks_file() {
80
86
  }
81
87
  }' > "$file"
82
88
  else
89
+ local escaped_cmd
90
+ escaped_cmd="$(printf '%s' "$cmd" | sed 's/\\/\\\\/g; s/"/\\"/g')"
83
91
  cat > "$file" << EOF
84
92
  {
85
93
  "version": 1,
86
94
  "hooks": {
87
- "beforeShellExecution": [{"command": "$cmd"}],
88
- "preToolUse": [{"command": "$cmd"}]
95
+ "beforeShellExecution": [{"command": "${escaped_cmd}"}],
96
+ "preToolUse": [{"command": "${escaped_cmd}"}]
89
97
  }
90
98
  }
91
99
  EOF
@@ -14,8 +14,11 @@ run_setup() {
14
14
  log_info "Setting up APort guardrails for LangChain..."
15
15
  config_dir="$(write_config_template langchain)"
16
16
  mkdir -p "$config_dir/aport"
17
+ chmod 700 "$config_dir/aport"
17
18
  export APORT_FRAMEWORK=langchain
18
19
  run_passport_wizard "$@"
20
+ # Harden permissions on passport (contains policy/capabilities)
21
+ [ -f "$config_dir/aport/passport.json" ] && chmod 600 "$config_dir/aport/passport.json"
19
22
  echo ""
20
23
  echo " Next steps (LangChain):"
21
24
  echo " ───────────────────────"
@@ -20,8 +20,11 @@ run_setup() {
20
20
  log_info "Setting up APort guardrails for n8n..."
21
21
  config_dir="$(write_config_template n8n)"
22
22
  mkdir -p "$config_dir/aport"
23
+ chmod 700 "$config_dir/aport"
23
24
  export APORT_FRAMEWORK=n8n
24
25
  run_passport_wizard "$@"
26
+ # Harden permissions on passport (contains policy/capabilities)
27
+ [ -f "$config_dir/aport/passport.json" ] && chmod 600 "$config_dir/aport/passport.json"
25
28
  echo ""
26
29
  echo " Next steps (n8n):"
27
30
  echo " ────────────────"
@@ -5,14 +5,16 @@
5
5
  # shellcheck source=./common.sh
6
6
  source "$(dirname "${BASH_SOURCE[0]:-.}")/common.sh"
7
7
 
8
- # Placeholder: allowlist check logic can be shared between bash evaluator and API
9
- # Returns 0 if command is allowed, 1 if denied
8
+ # Stub: command allowlist checking is implemented directly in aport-guardrail-bash.sh
9
+ # (safe_prefix_match + blocked_patterns). This file is kept for backward compatibility
10
+ # but callers MUST NOT rely on this function for security enforcement.
10
11
  check_command_allowed() {
11
12
  local command_line="$1"
12
13
  local allowed_list="${2:-*}"
13
- # TODO: Implement against passport allowed_commands + blocked patterns
14
14
  [[ -z "$command_line" ]] && return 1
15
- return 0
15
+ # SECURITY: Not implemented here — use aport-guardrail-bash.sh for actual enforcement
16
+ echo "[aport] WARN: check_command_allowed is a stub; use aport-guardrail-bash.sh" >&2
17
+ return 1
16
18
  }
17
19
 
18
20
  export -f check_command_allowed
package/bin/lib/config.sh CHANGED
@@ -15,6 +15,7 @@ get_config_dir() {
15
15
  crewai) echo "${APORT_CREWAI_CONFIG_DIR:-$HOME/.aport/crewai}" ;;
16
16
  n8n) echo "${APORT_N8N_CONFIG_DIR:-$HOME/.n8n}" ;;
17
17
  cursor) echo "${APORT_CURSOR_CONFIG_DIR:-$HOME/.cursor}" ;;
18
+ claude-code) echo "${APORT_CLAUDE_CODE_CONFIG_DIR:-$HOME/.claude}" ;;
18
19
  *) echo "${APORT_CONFIG_DIR:-$HOME/.aport}" ;;
19
20
  esac
20
21
  }
package/bin/lib/detect.sh CHANGED
@@ -32,7 +32,12 @@ detect_frameworks_list() {
32
32
  grep -qi 'crewai' "$dir/requirements.txt" 2> /dev/null && list+=(crewai)
33
33
  fi
34
34
 
35
- # Dedupe preserving order (first occurrence wins). Safe for set -u when list is empty.
35
+ # Claude Code: detect claude binary or ~/.claude directory
36
+ if command -v claude &> /dev/null || [[ -d "$HOME/.claude" ]]; then
37
+ list+=(claude-code)
38
+ fi
39
+
40
+ # Dedupe preserving order
36
41
  local seen=() out=()
37
42
  if [[ ${#list[@]} -gt 0 ]]; then
38
43
  for fw in "${list[@]}"; do