Skip to content

Commit 5ad0488

Browse files
eddimanclaude
andcommitted
feat: add directory sandbox and KB access hooks for claude-cli
- sandbox-directory.sh: blocks file access outside project dir (all tools) - block-kb-read.sh: blocks direct KB file access, enforces CLI query pattern - Fix grep -oP (GNU-only) to jq in all hooks for macOS compatibility - Wire both new hooks into settings.json for Bash/Read/Edit/Write/Glob/Grep Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4e19be0 commit 5ad0488

5 files changed

Lines changed: 236 additions & 2 deletions

File tree

.claude/hooks/block-env-access.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ set -euo pipefail
1212
# The tool input is passed via stdin as JSON.
1313
# Extract the command field.
1414
INPUT=$(cat)
15-
COMMAND=$(echo "$INPUT" | grep -oP '"command"\s*:\s*"([^"]*)"' | head -1 | sed 's/.*"command"\s*:\s*"//;s/"$//' || true)
15+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
1616

1717
if [ -z "$COMMAND" ]; then
1818
exit 0 # No command found — allow

.claude/hooks/block-env-read.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
set -euo pipefail
1111

1212
INPUT=$(cat)
13-
FILE_PATH=$(echo "$INPUT" | grep -oP '"file_path"\s*:\s*"([^"]*)"' | head -1 | sed 's/.*"file_path"\s*:\s*"//;s/"$//' || true)
13+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)
1414

1515
if [ -z "$FILE_PATH" ]; then
1616
exit 0 # No file path — allow

.claude/hooks/block-kb-read.sh

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env bash
2+
# block-kb-read.sh — PreToolUse hook for file-access tools
3+
# Blocks direct read/write/search access to knowledge base directories.
4+
# KBs must be queried via: .venv/bin/python -m adjutant kb query <name> "<question>"
5+
# Returns exit 2 to deny the tool call.
6+
7+
set -euo pipefail
8+
9+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-}"
10+
if [ -z "$PROJECT_DIR" ]; then
11+
exit 0
12+
fi
13+
14+
REGISTRY="$PROJECT_DIR/knowledge_bases/registry.yaml"
15+
if [ ! -f "$REGISTRY" ]; then
16+
exit 0 # No registry — nothing to protect
17+
fi
18+
19+
# Parse KB paths from registry.yaml
20+
KB_PATHS=()
21+
while IFS= read -r line; do
22+
path=$(echo "$line" | sed -n 's/.*path:\s*//p' | xargs)
23+
[ -z "$path" ] && continue
24+
if [[ "$path" != /* ]]; then
25+
path="$PROJECT_DIR/$path"
26+
fi
27+
if [ -e "$path" ]; then
28+
path=$(cd "$path" 2>/dev/null && pwd -P) || path="$path"
29+
fi
30+
KB_PATHS+=("$path")
31+
done < "$REGISTRY"
32+
33+
# Also block the knowledge_bases/ directory itself
34+
KB_PATHS+=("$PROJECT_DIR/knowledge_bases")
35+
36+
[ ${#KB_PATHS[@]} -eq 0 ] && exit 0
37+
38+
DENY_MSG="DENIED: Direct KB file access is not allowed. Use: .venv/bin/python -m adjutant kb query <name> \\\"<question>\\\""
39+
40+
check_kb_path() {
41+
local target="$1"
42+
[ -z "$target" ] && return 0
43+
44+
target="${target/#\~/$HOME}"
45+
if [[ "$target" != /* ]]; then
46+
target="$PROJECT_DIR/$target"
47+
fi
48+
if [ -e "$target" ]; then
49+
resolved=$(cd "$(dirname "$target")" 2>/dev/null && pwd -P)/$(basename "$target") || resolved="$target"
50+
else
51+
resolved="$target"
52+
fi
53+
54+
for kb in "${KB_PATHS[@]}"; do
55+
if [[ "$resolved" = "$kb"* ]]; then
56+
echo "{\"result\": \"$DENY_MSG\"}" >&2
57+
exit 2
58+
fi
59+
done
60+
}
61+
62+
INPUT=$(cat)
63+
TOOL_NAME="${CLAUDE_TOOL_NAME:-}"
64+
65+
case "$TOOL_NAME" in
66+
Read|Edit|Write)
67+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)
68+
check_kb_path "$FILE_PATH"
69+
;;
70+
Glob|Grep)
71+
SEARCH_PATH=$(echo "$INPUT" | jq -r '.tool_input.path // empty' 2>/dev/null || true)
72+
check_kb_path "$SEARCH_PATH"
73+
;;
74+
Bash)
75+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
76+
[ -z "$COMMAND" ] && exit 0
77+
78+
for kb in "${KB_PATHS[@]}"; do
79+
if echo "$COMMAND" | grep -qF "$kb"; then
80+
echo "{\"result\": \"$DENY_MSG\"}" >&2
81+
exit 2
82+
fi
83+
done
84+
# Check for relative references to knowledge_bases/
85+
if echo "$COMMAND" | grep -qE 'knowledge_bases'; then
86+
echo "{\"result\": \"$DENY_MSG\"}" >&2
87+
exit 2
88+
fi
89+
;;
90+
esac
91+
92+
exit 0

.claude/hooks/sandbox-directory.sh

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env bash
2+
# sandbox-directory.sh — PreToolUse hook for all file-access tools
3+
# Blocks tool calls that target paths outside the project directory.
4+
# Returns exit 2 to deny the tool call.
5+
6+
set -euo pipefail
7+
8+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-}"
9+
if [ -z "$PROJECT_DIR" ]; then
10+
exit 0 # Can't enforce without knowing the project dir
11+
fi
12+
13+
# Resolve project dir to absolute path (follow symlinks)
14+
PROJECT_DIR=$(cd "$PROJECT_DIR" && pwd -P)
15+
16+
INPUT=$(cat)
17+
TOOL_NAME="${CLAUDE_TOOL_NAME:-}"
18+
19+
check_path() {
20+
local raw_path="$1"
21+
[ -z "$raw_path" ] && return 0
22+
23+
# Expand ~ to home directory
24+
raw_path="${raw_path/#\~/$HOME}"
25+
26+
# Resolve to absolute path
27+
if [[ "$raw_path" = /* ]]; then
28+
if [ -e "$raw_path" ]; then
29+
resolved=$(cd "$(dirname "$raw_path")" 2>/dev/null && pwd -P)/$(basename "$raw_path")
30+
else
31+
resolved="$raw_path"
32+
fi
33+
else
34+
resolved="$PROJECT_DIR/$raw_path"
35+
fi
36+
37+
if [[ "$resolved" != "$PROJECT_DIR"* ]]; then
38+
echo "{\"result\": \"DENIED: Access outside project directory is not allowed. Path: $raw_path\"}" >&2
39+
exit 2
40+
fi
41+
}
42+
43+
case "$TOOL_NAME" in
44+
Read|Edit|Write)
45+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)
46+
check_path "$FILE_PATH"
47+
;;
48+
Glob|Grep)
49+
SEARCH_PATH=$(echo "$INPUT" | jq -r '.tool_input.path // empty' 2>/dev/null || true)
50+
if [ -n "$SEARCH_PATH" ]; then
51+
check_path "$SEARCH_PATH"
52+
fi
53+
;;
54+
Bash)
55+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
56+
[ -z "$COMMAND" ] && exit 0
57+
58+
# Allow safe commands that don't access arbitrary paths
59+
if echo "$COMMAND" | grep -qE '^\s*(git\s|\.venv/|python\s+-m\s+adjutant)'; then
60+
exit 0
61+
fi
62+
63+
# Scan for absolute paths outside project dir
64+
for path in $(echo "$COMMAND" | grep -oE '(/[a-zA-Z][a-zA-Z0-9_./-]+|~/[a-zA-Z0-9_./-]+)' || true); do
65+
expanded="${path/#\~/$HOME}"
66+
if [[ "$expanded" = /* ]] && [[ "$expanded" != "$PROJECT_DIR"* ]] && [[ "$expanded" != "/dev/"* ]]; then
67+
echo "{\"result\": \"DENIED: Bash command references path outside project directory: $path\"}" >&2
68+
exit 2
69+
fi
70+
done
71+
;;
72+
esac
73+
74+
exit 0

.claude/settings.json

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
{
2020
"type": "command",
2121
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-env-access.sh"
22+
},
23+
{
24+
"type": "command",
25+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/sandbox-directory.sh"
26+
},
27+
{
28+
"type": "command",
29+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-kb-read.sh"
2230
}
2331
]
2432
},
@@ -28,6 +36,66 @@
2836
{
2937
"type": "command",
3038
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-env-read.sh"
39+
},
40+
{
41+
"type": "command",
42+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/sandbox-directory.sh"
43+
},
44+
{
45+
"type": "command",
46+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-kb-read.sh"
47+
}
48+
]
49+
},
50+
{
51+
"matcher": "Edit",
52+
"hooks": [
53+
{
54+
"type": "command",
55+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/sandbox-directory.sh"
56+
},
57+
{
58+
"type": "command",
59+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-kb-read.sh"
60+
}
61+
]
62+
},
63+
{
64+
"matcher": "Write",
65+
"hooks": [
66+
{
67+
"type": "command",
68+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/sandbox-directory.sh"
69+
},
70+
{
71+
"type": "command",
72+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-kb-read.sh"
73+
}
74+
]
75+
},
76+
{
77+
"matcher": "Glob",
78+
"hooks": [
79+
{
80+
"type": "command",
81+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/sandbox-directory.sh"
82+
},
83+
{
84+
"type": "command",
85+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-kb-read.sh"
86+
}
87+
]
88+
},
89+
{
90+
"matcher": "Grep",
91+
"hooks": [
92+
{
93+
"type": "command",
94+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/sandbox-directory.sh"
95+
},
96+
{
97+
"type": "command",
98+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-kb-read.sh"
3199
}
32100
]
33101
}

0 commit comments

Comments
 (0)