opsentry 1.8.0__py3-none-any.whl
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.
- opsentry/VERSION +1 -0
- opsentry/__init__.py +5 -0
- opsentry/cli.py +295 -0
- opsentry/config/__init__.py +0 -0
- opsentry/config/generate.py +414 -0
- opsentry/config/hook_templates/block-dangerous-commands.sh.j2 +102 -0
- opsentry/config/hook_templates/block-data-exfiltration.sh.j2 +94 -0
- opsentry/config/hook_templates/block-environment-escape.sh.j2 +99 -0
- opsentry/config/hook_templates/block-git-commands.sh.j2 +46 -0
- opsentry/config/hook_templates/block-package-install.sh.j2 +95 -0
- opsentry/config/hook_templates/block-pii-leakage.sh.j2 +100 -0
- opsentry/config/hook_templates/block-scope-escape.sh.j2 +115 -0
- opsentry/config/hook_templates/block-sensitive-files.sh.j2 +97 -0
- opsentry/config/init_wizard.py +201 -0
- opsentry/config/rule_texts/01_sensitive_files.md +15 -0
- opsentry/config/rule_texts/02_client_data.md +10 -0
- opsentry/config/rule_texts/03_credentials.md +17 -0
- opsentry/config/rule_texts/04_sql_safety.md +33 -0
- opsentry/config/rule_texts/05_dangerous_code.md +29 -0
- opsentry/config/rule_texts/06_frontend_xss.md +9 -0
- opsentry/config/rule_texts/07_git_operations_block_all.md +13 -0
- opsentry/config/rule_texts/07_git_operations_read_only.md +18 -0
- opsentry/config/rule_texts/08_destructive_commands.md +12 -0
- opsentry/config/rule_texts/09_database_safety.md +17 -0
- opsentry/config/rule_texts/10_network_requests.md +10 -0
- opsentry/config/rule_texts/11_code_quality.md +13 -0
- opsentry/config/rule_texts/12_data_exfiltration.md +10 -0
- opsentry/config/rule_texts/13_package_install.md +9 -0
- opsentry/config/rule_texts/14_scope_boundaries.md +8 -0
- opsentry/config/rule_texts/15_env_isolation.md +9 -0
- opsentry/config/rule_texts/16_resource_limits.md +8 -0
- opsentry/config/rule_texts/17_compliance_privacy.md +9 -0
- opsentry/config/rule_texts/18_when_in_doubt.md +10 -0
- opsentry/config/schema.py +142 -0
- opsentry/config/templates/generic.yaml +84 -0
- opsentry/deployment/.claude/commands/code-health.md +42 -0
- opsentry/deployment/.claude/commands/governance-check.md +29 -0
- opsentry/deployment/.claude/commands/security-audit.md +41 -0
- opsentry/deployment/baseline.py +440 -0
- opsentry/deployment/blocklog_audit.py +497 -0
- opsentry/deployment/claude/CLAUDE.md +300 -0
- opsentry/deployment/claude/hooks/block-dangerous-commands.sh +106 -0
- opsentry/deployment/claude/hooks/block-data-exfiltration.sh +99 -0
- opsentry/deployment/claude/hooks/block-environment-escape.sh +96 -0
- opsentry/deployment/claude/hooks/block-git-commands.sh +37 -0
- opsentry/deployment/claude/hooks/block-package-install.sh +103 -0
- opsentry/deployment/claude/hooks/block-pii-leakage.sh +86 -0
- opsentry/deployment/claude/hooks/block-scope-escape.sh +210 -0
- opsentry/deployment/claude/hooks/block-sensitive-files.sh +97 -0
- opsentry/deployment/claude/settings.json +72 -0
- opsentry/deployment/install.sh +343 -0
- opsentry/deployment/patrol.sh +266 -0
- opsentry/deployment/test.sh +728 -0
- opsentry/deployment/update.sh +19 -0
- opsentry/deployment/verify.sh +201 -0
- opsentry-1.8.0.dist-info/METADATA +158 -0
- opsentry-1.8.0.dist-info/RECORD +61 -0
- opsentry-1.8.0.dist-info/WHEEL +5 -0
- opsentry-1.8.0.dist-info/entry_points.txt +2 -0
- opsentry-1.8.0.dist-info/licenses/LICENSE +190 -0
- opsentry-1.8.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# block-package-install.sh
|
|
3
|
+
# PreToolUse hook that blocks package installations from untrusted or arbitrary sources.
|
|
4
|
+
# It detects pip, npm, gem, and go installs from custom registries, git URLs, and direct URLs.
|
|
5
|
+
# Curl/wget piped to package managers is also blocked.
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
LOG_FILE="$HOME/.claude/guardrail-blocks.log"
|
|
10
|
+
HOOK_NAME="block-package-install"
|
|
11
|
+
|
|
12
|
+
log_block() {
|
|
13
|
+
local reason="$1"
|
|
14
|
+
local detail="$2"
|
|
15
|
+
local timestamp
|
|
16
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
17
|
+
echo "{\"timestamp\":\"$timestamp\",\"hook\":\"$HOOK_NAME\",\"reason\":\"$reason\",\"detail\":\"$detail\",\"user\":\"$(whoami)\"}" >> "$LOG_FILE"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
INPUT=$(cat)
|
|
21
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
22
|
+
|
|
23
|
+
# Only check Bash commands
|
|
24
|
+
if [ "$TOOL_NAME" != "Bash" ]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
|
29
|
+
|
|
30
|
+
# Optional path prefix for matching commands invoked via full path
|
|
31
|
+
P='(/\S+/)?'
|
|
32
|
+
|
|
33
|
+
# === PIP INSTALL FROM ARBITRARY SOURCES ===
|
|
34
|
+
# Catches: pip install -i url, pip install --index-url url, pip install --index-url=url
|
|
35
|
+
# Also catches: python -m pip install, python3 -m pip install
|
|
36
|
+
if echo "$COMMAND" | grep -qE "(${P}pip[3]?|(python|python3)\s+-m\s+pip)\s+install\s.*(-i[ =]|--index-url[ =]|--extra-index-url[ =])"; then
|
|
37
|
+
log_block "pip install with custom index URL" "$COMMAND"
|
|
38
|
+
echo "BLOCKED: pip install with --index-url or -i pointing to a custom registry is not allowed. Use the default PyPI registry." >&2
|
|
39
|
+
exit 2
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if echo "$COMMAND" | grep -qE "(${P}pip[3]?|(python|python3)\s+-m\s+pip)\s+install\s.*git\+"; then
|
|
43
|
+
log_block "pip install from git URL" "$COMMAND"
|
|
44
|
+
echo "BLOCKED: pip install from a git URL is not allowed. Only install packages from PyPI." >&2
|
|
45
|
+
exit 2
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
if echo "$COMMAND" | grep -qE "(${P}pip[3]?|(python|python3)\s+-m\s+pip)\s+install\s.*https?://"; then
|
|
49
|
+
log_block "pip install from direct URL" "$COMMAND"
|
|
50
|
+
echo "BLOCKED: pip install from a direct URL is not allowed. Only install packages from PyPI." >&2
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# === NPM INSTALL FROM ARBITRARY SOURCES ===
|
|
55
|
+
# Catches both --registry url and --registry=url
|
|
56
|
+
if echo "$COMMAND" | grep -qE "${P}npm\s+install\s.*--registry[ =]"; then
|
|
57
|
+
log_block "npm install with custom registry" "$COMMAND"
|
|
58
|
+
echo "BLOCKED: npm install with --registry pointing to a custom registry is not allowed. Use the default npm registry." >&2
|
|
59
|
+
exit 2
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if echo "$COMMAND" | grep -qE "${P}npm\s+install\s.*git(\+https?|\+ssh|://)"; then
|
|
63
|
+
log_block "npm install from git URL" "$COMMAND"
|
|
64
|
+
echo "BLOCKED: npm install from a git URL is not allowed. Only install packages from the npm registry." >&2
|
|
65
|
+
exit 2
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if echo "$COMMAND" | grep -qE "${P}npm\s+install\s.*https?://"; then
|
|
69
|
+
log_block "npm install from tarball URL" "$COMMAND"
|
|
70
|
+
echo "BLOCKED: npm install from a direct URL is not allowed. Only install packages from the npm registry." >&2
|
|
71
|
+
exit 2
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
if echo "$COMMAND" | grep -qE "${P}npm\s+install\s+[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+"; then
|
|
75
|
+
log_block "npm install from GitHub shorthand" "$COMMAND"
|
|
76
|
+
echo "BLOCKED: npm install from GitHub shorthand (user/repo) is not allowed. Only install packages from the npm registry." >&2
|
|
77
|
+
exit 2
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# === GEM INSTALL FROM ARBITRARY SOURCES ===
|
|
81
|
+
# Catches both --source url and --source=url
|
|
82
|
+
if echo "$COMMAND" | grep -qE "${P}gem\s+install\s.*--source[ =]"; then
|
|
83
|
+
log_block "gem install with custom source" "$COMMAND"
|
|
84
|
+
echo "BLOCKED: gem install with --source pointing to a custom registry is not allowed. Use the default RubyGems registry." >&2
|
|
85
|
+
exit 2
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# === GO INSTALL FROM ARBITRARY SOURCES ===
|
|
89
|
+
if echo "$COMMAND" | grep -qE "${P}go\s+install\s+https?://"; then
|
|
90
|
+
log_block "go install from URL" "$COMMAND"
|
|
91
|
+
echo "BLOCKED: go install from a direct URL is not allowed. Use standard Go module paths." >&2
|
|
92
|
+
exit 2
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# === CURL/WGET PIPED TO PACKAGE MANAGERS ===
|
|
96
|
+
# Catches both "curl url | pip" and "curl url|pip" (no space)
|
|
97
|
+
if echo "$COMMAND" | grep -qE "(${P}curl|${P}wget)\s.*\|[ \t]*(pip[3]?|npm|gem|go)\s"; then
|
|
98
|
+
log_block "curl/wget piped to package manager" "$COMMAND"
|
|
99
|
+
echo "BLOCKED: Piping curl/wget output to a package manager is not allowed. Download files first, review them, then install." >&2
|
|
100
|
+
exit 2
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
exit 0
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# block-pii-leakage.sh
|
|
3
|
+
# PreToolUse hook -- blocks Claude Code from writing PII patterns into source files.
|
|
4
|
+
# Checks Write and Edit tool inputs for SSNs, credit card numbers, and similar patterns.
|
|
5
|
+
# Exit code 2 = block the action, stderr message goes to Claude as feedback.
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
LOG_FILE="$HOME/.claude/guardrail-blocks.log"
|
|
10
|
+
HOOK_NAME="block-pii-leakage"
|
|
11
|
+
|
|
12
|
+
log_block() {
|
|
13
|
+
local reason="$1"
|
|
14
|
+
local detail="$2"
|
|
15
|
+
local timestamp
|
|
16
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
17
|
+
echo "{\"timestamp\":\"$timestamp\",\"hook\":\"$HOOK_NAME\",\"reason\":\"$reason\",\"detail\":\"$detail\",\"user\":\"$(whoami)\"}" >> "$LOG_FILE"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
INPUT=$(cat)
|
|
21
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
22
|
+
|
|
23
|
+
# Only check Write, Edit, and Bash tools
|
|
24
|
+
if [ "$TOOL_NAME" != "Write" ] && [ "$TOOL_NAME" != "Edit" ] && [ "$TOOL_NAME" != "Bash" ]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Extract the content to scan based on tool type
|
|
29
|
+
CONTENT=""
|
|
30
|
+
if [ "$TOOL_NAME" = "Write" ]; then
|
|
31
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // ""')
|
|
32
|
+
elif [ "$TOOL_NAME" = "Edit" ]; then
|
|
33
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // ""')
|
|
34
|
+
elif [ "$TOOL_NAME" = "Bash" ]; then
|
|
35
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
if [ -z "$CONTENT" ]; then
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# ============================================================
|
|
43
|
+
# Default PII patterns (US locale)
|
|
44
|
+
# To add locale-specific patterns, append sections below.
|
|
45
|
+
# ============================================================
|
|
46
|
+
|
|
47
|
+
# --- US Social Security Number (XXX-XX-XXXX or XXX XX XXXX) ---
|
|
48
|
+
# Requires at least dashes or spaces as separators to avoid false positives on 9-digit numbers
|
|
49
|
+
SSN_MATCH=$(echo "$CONTENT" | grep -oE '\b[0-9]{3}[- ][0-9]{2}[- ][0-9]{4}\b' | head -1 || true)
|
|
50
|
+
if [ -n "$SSN_MATCH" ]; then
|
|
51
|
+
# Allow well-known synthetic test SSNs
|
|
52
|
+
IS_SYNTHETIC=$(echo "$SSN_MATCH" | grep -cE '^(000-00-0000|555-55-5555|000 00 0000|555 55 5555)$' || true)
|
|
53
|
+
if [ "$IS_SYNTHETIC" -eq 0 ]; then
|
|
54
|
+
log_block "US SSN pattern detected" "$SSN_MATCH"
|
|
55
|
+
echo "BLOCKED: Content contains a pattern matching a US Social Security Number ($SSN_MATCH). Use synthetic test data like 000-00-0000 or 555-55-5555 instead. Never include real PII in source code." >&2
|
|
56
|
+
exit 2
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# --- Credit Card Numbers (Visa, Mastercard, Amex, Discover) ---
|
|
61
|
+
# Catches: 4111-1111-1111-1111, 4111 1111 1111 1111, and 4111111111111111
|
|
62
|
+
CC_MATCH=$(echo "$CONTENT" | grep -oE '\b(4[0-9]{3}|5[1-5][0-9]{2}|6(011|5[0-9]{2}))[- ]?[0-9]{4}[- ]?[0-9]{4}[- ]?[0-9]{1,4}\b' | head -1 || true)
|
|
63
|
+
if [ -z "$CC_MATCH" ]; then
|
|
64
|
+
# Try Amex pattern separately: 3[47]XX-XXXXXX-XXXXX or 3[47]XXXXXXXXXXXXX
|
|
65
|
+
CC_MATCH=$(echo "$CONTENT" | grep -oE '\b3[47][0-9]{2}[- ]?[0-9]{6}[- ]?[0-9]{5}\b' | head -1 || true)
|
|
66
|
+
fi
|
|
67
|
+
if [ -n "$CC_MATCH" ]; then
|
|
68
|
+
log_block "Credit card number pattern detected" "$CC_MATCH"
|
|
69
|
+
echo "BLOCKED: Content contains a pattern matching a credit card number. Use synthetic test data like 4111-1111-1111-1111 instead. Never include real PII in source code." >&2
|
|
70
|
+
exit 2
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# ============================================================
|
|
74
|
+
# Optional locale: KR (Korean Resident Registration Number)
|
|
75
|
+
# Remove or comment this section if not needed.
|
|
76
|
+
# ============================================================
|
|
77
|
+
|
|
78
|
+
# --- Korean Resident Registration Number (YYMMDD-NNNNNNN) ---
|
|
79
|
+
RRN_MATCH=$(echo "$CONTENT" | grep -oE '\b[0-9]{6}-[0-9]{7}\b' | head -1 || true)
|
|
80
|
+
if [ -n "$RRN_MATCH" ]; then
|
|
81
|
+
log_block "Korean RRN pattern detected" "$RRN_MATCH"
|
|
82
|
+
echo "BLOCKED: Content contains a pattern matching a Korean Resident Registration Number ($RRN_MATCH). Use synthetic test data instead. Never include real PII in source code." >&2
|
|
83
|
+
exit 2
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
exit 0
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# block-scope-escape.sh -- PreToolUse hook that prevents the agent from modifying
|
|
3
|
+
# its own guardrails in ~/.claude/ and from writing to dangerous system locations.
|
|
4
|
+
# Exit code 2 = block the action, stderr message goes to Claude as feedback.
|
|
5
|
+
#
|
|
6
|
+
# Path checks use word-boundary anchoring (whitespace, quote, =, backtick, or
|
|
7
|
+
# start-of-line) so that legitimate paths containing system-path substrings
|
|
8
|
+
# are NOT blocked. Specifically:
|
|
9
|
+
# - project-local .claude/ directories (e.g., ~/Desktop/repos/x/.claude/)
|
|
10
|
+
# are allowed; only $HOME/.claude/ is the protected install target
|
|
11
|
+
# - ~/Library/ on macOS is allowed (only system /Library/ is blocked)
|
|
12
|
+
# - /var/folders/ (macOS per-user temp) and /var/log/ are allowed
|
|
13
|
+
# File paths on Read/Write/Edit are resolved via realpath (with fallback)
|
|
14
|
+
# so symlinks pointing into $HOME/.claude/ are still caught.
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
LOG_FILE="$HOME/.claude/guardrail-blocks.log"
|
|
19
|
+
HOOK_NAME="block-scope-escape"
|
|
20
|
+
|
|
21
|
+
log_block() {
|
|
22
|
+
local reason="$1"
|
|
23
|
+
local detail="$2"
|
|
24
|
+
local timestamp
|
|
25
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
26
|
+
echo "{\"timestamp\":\"$timestamp\",\"hook\":\"$HOOK_NAME\",\"reason\":\"$reason\",\"detail\":\"$detail\",\"user\":\"$(whoami)\"}" >> "$LOG_FILE"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Path resolution helpers
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
# Expand a leading ~/ to $HOME, then run realpath if available.
|
|
34
|
+
# For non-existent paths (e.g., Write to a new file), resolve the parent
|
|
35
|
+
# directory and re-attach the basename. Falls back to the unresolved path
|
|
36
|
+
# when realpath is not installed (best-effort symlink resolution).
|
|
37
|
+
resolve_path() {
|
|
38
|
+
local p="$1"
|
|
39
|
+
# Expand a leading ~/ via substring offset (avoid `${p#~/}` because bash
|
|
40
|
+
# applies tilde expansion to the ~ inside the pattern, which silently
|
|
41
|
+
# produces ~/foo -> $HOME/~/foo instead of $HOME/foo).
|
|
42
|
+
case "$p" in
|
|
43
|
+
"~") p="$HOME" ;;
|
|
44
|
+
"~/"*) p="$HOME/${p:2}" ;;
|
|
45
|
+
esac
|
|
46
|
+
|
|
47
|
+
if [ -e "$p" ]; then
|
|
48
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
49
|
+
realpath "$p" 2>/dev/null || echo "$p"
|
|
50
|
+
else
|
|
51
|
+
echo "$p"
|
|
52
|
+
fi
|
|
53
|
+
return
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Path doesn't exist yet (e.g., Write to a new file). Resolve the parent
|
|
57
|
+
# so a symlinked parent dir is still caught.
|
|
58
|
+
local parent base resolved_parent
|
|
59
|
+
parent=$(dirname "$p")
|
|
60
|
+
base=$(basename "$p")
|
|
61
|
+
if [ -e "$parent" ] && command -v realpath >/dev/null 2>&1; then
|
|
62
|
+
resolved_parent=$(realpath "$parent" 2>/dev/null || echo "$parent")
|
|
63
|
+
echo "$resolved_parent/$base"
|
|
64
|
+
else
|
|
65
|
+
echo "$p"
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Return 0 (success) iff the resolved absolute path is $HOME/.claude or
|
|
70
|
+
# any descendant. Compares against BOTH the unresolved $HOME and the
|
|
71
|
+
# realpath-resolved $HOME so the macOS /var -> /private/var symlink (which
|
|
72
|
+
# affects tempdirs and any path-resolved file_path) does not cause a miss.
|
|
73
|
+
is_under_home_claude() {
|
|
74
|
+
local resolved="$1"
|
|
75
|
+
local home_claude="$HOME/.claude"
|
|
76
|
+
local resolved_home_claude="$home_claude"
|
|
77
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
78
|
+
local resolved_home
|
|
79
|
+
resolved_home=$(realpath "$HOME" 2>/dev/null || echo "$HOME")
|
|
80
|
+
resolved_home_claude="$resolved_home/.claude"
|
|
81
|
+
fi
|
|
82
|
+
case "$resolved" in
|
|
83
|
+
"$home_claude") return 0 ;;
|
|
84
|
+
"$home_claude"/*) return 0 ;;
|
|
85
|
+
"$resolved_home_claude") return 0 ;;
|
|
86
|
+
"$resolved_home_claude"/*) return 0 ;;
|
|
87
|
+
esac
|
|
88
|
+
return 1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
INPUT=$(cat)
|
|
92
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# A. Self-modification prevention -- Read, Write, Edit
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
if [ "$TOOL_NAME" = "Read" ] || [ "$TOOL_NAME" = "Write" ] || [ "$TOOL_NAME" = "Edit" ]; then
|
|
99
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
100
|
+
|
|
101
|
+
if [ -n "$FILE_PATH" ]; then
|
|
102
|
+
RESOLVED=$(resolve_path "$FILE_PATH")
|
|
103
|
+
if is_under_home_claude "$RESOLVED"; then
|
|
104
|
+
log_block "Self-modification via $TOOL_NAME" "$FILE_PATH"
|
|
105
|
+
echo "BLOCKED: Access to '$FILE_PATH' (resolves to '$RESOLVED') is denied. Modifying files in ~/.claude/ is forbidden -- this directory contains security guardrails that must not be read or altered by the agent." >&2
|
|
106
|
+
exit 2
|
|
107
|
+
fi
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# File-based tools have no additional scope-boundary check; allow.
|
|
111
|
+
exit 0
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# B. Bash branch
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
if [ "$TOOL_NAME" = "Bash" ]; then
|
|
119
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
|
120
|
+
|
|
121
|
+
# Boundary character class: start-of-line, whitespace, =, backtick, single
|
|
122
|
+
# or double quote. Used to anchor system path matches so substring matches
|
|
123
|
+
# against /Library/ inside ~/Library/ don't fire.
|
|
124
|
+
# In double-quoted bash strings: \` for backtick, \" for dquote, \$ for $.
|
|
125
|
+
BOUNDARY="(^|[[:space:]=\`\"'])"
|
|
126
|
+
END_BOUNDARY="(/|\$|[[:space:]\"'])"
|
|
127
|
+
|
|
128
|
+
# --- B1. Self-modification: ~/.claude/ in any form ---
|
|
129
|
+
|
|
130
|
+
# Literal forms (uniquely identifiable, fixed-string match is safe):
|
|
131
|
+
CLAUDE_LITERAL_FORMS=(
|
|
132
|
+
'~/.claude'
|
|
133
|
+
'$HOME/.claude'
|
|
134
|
+
'${HOME}/.claude'
|
|
135
|
+
)
|
|
136
|
+
for form in "${CLAUDE_LITERAL_FORMS[@]}"; do
|
|
137
|
+
if echo "$COMMAND" | grep -qF "$form"; then
|
|
138
|
+
log_block "Self-modification via Bash (literal $form)" "$COMMAND"
|
|
139
|
+
echo "BLOCKED: Command references $form. Modifying guardrail files is forbidden -- this directory contains security guardrails that must not be altered by the agent." >&2
|
|
140
|
+
exit 2
|
|
141
|
+
fi
|
|
142
|
+
done
|
|
143
|
+
|
|
144
|
+
# Expanded HOME path (e.g., /Users/utku/.claude/...). Anchor to a path-arg
|
|
145
|
+
# boundary so /Users/utku/Desktop/repos/x/.claude/ does NOT match.
|
|
146
|
+
# Escape the dot in $HOME for regex (other regex specials are extremely
|
|
147
|
+
# rare in usernames; we accept that residual).
|
|
148
|
+
HOME_RE="${HOME//./\\.}"
|
|
149
|
+
EXPANDED_CLAUDE_REGEX="${BOUNDARY}${HOME_RE}/\\.claude${END_BOUNDARY}"
|
|
150
|
+
if echo "$COMMAND" | grep -qE "$EXPANDED_CLAUDE_REGEX"; then
|
|
151
|
+
log_block "Self-modification via Bash (expanded HOME path)" "$COMMAND"
|
|
152
|
+
echo "BLOCKED: Command references the expanded \$HOME/.claude/ path. Modifying guardrail files is forbidden -- this directory contains security guardrails that must not be altered by the agent." >&2
|
|
153
|
+
exit 2
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# --- B2. Scope boundary enforcement: writes to system locations ---
|
|
157
|
+
# Only trigger when the command actually writes (>, >>, cp, mv, tee, install,
|
|
158
|
+
# rsync, dd, chmod, sed -i).
|
|
159
|
+
WRITE_INDICATORS='(>|>>|(^|[[:space:]])(cp|mv|tee|install|rsync|dd|chmod)[[:space:]]|sed[[:space:]]+-[a-zA-Z]*i)'
|
|
160
|
+
|
|
161
|
+
if echo "$COMMAND" | grep -qE "$WRITE_INDICATORS"; then
|
|
162
|
+
# System paths anchored at path-arg boundary. ~/Library/ is NOT matched
|
|
163
|
+
# because the boundary requires whitespace/quote/start before /Library/.
|
|
164
|
+
SYS_REGEX="${BOUNDARY}/(etc|usr|opt|System|Library)${END_BOUNDARY}"
|
|
165
|
+
if echo "$COMMAND" | grep -qE "$SYS_REGEX"; then
|
|
166
|
+
MATCH=$(echo "$COMMAND" | grep -oE '/(etc|usr|opt|System|Library)/' | head -1 || true)
|
|
167
|
+
log_block "Write to system path: $MATCH" "$COMMAND"
|
|
168
|
+
echo "BLOCKED: Command attempts to write to '$MATCH' (system path). Writing to system directories is forbidden. Limit file operations to your project directory or your home directory." >&2
|
|
169
|
+
exit 2
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# /var/ — block by default, exempt /var/log/ (logging) and /var/folders/
|
|
173
|
+
# (macOS per-user temp dir, where mktemp puts things). /var/tmp/ remains
|
|
174
|
+
# blocked per CLAUDE.md rule 12 (data exfiltration prevention).
|
|
175
|
+
VAR_REGEX="${BOUNDARY}/var/"
|
|
176
|
+
if echo "$COMMAND" | grep -qE "$VAR_REGEX"; then
|
|
177
|
+
# Pull every /var/<segment> reference out of the command and check
|
|
178
|
+
# whether at least one is NOT in the allow list. Allow segments:
|
|
179
|
+
# log, folders. Anything else (cache, tmp, lib, www, ...) is blocked.
|
|
180
|
+
VAR_BAD=""
|
|
181
|
+
while IFS= read -r seg; do
|
|
182
|
+
case "$seg" in
|
|
183
|
+
/var/log|/var/log/*) ;;
|
|
184
|
+
/var/folders|/var/folders/*) ;;
|
|
185
|
+
*) VAR_BAD="$seg"; break ;;
|
|
186
|
+
esac
|
|
187
|
+
done < <(echo "$COMMAND" | grep -oE '/var/[a-zA-Z0-9_.-]+' || true)
|
|
188
|
+
if [ -n "$VAR_BAD" ]; then
|
|
189
|
+
log_block "Write to /var/ system path: $VAR_BAD" "$COMMAND"
|
|
190
|
+
echo "BLOCKED: Command attempts to write to '$VAR_BAD'. Writing to /var/ is forbidden except /var/log/ (logging) and /var/folders/ (macOS temp). Limit file operations to your project directory." >&2
|
|
191
|
+
exit 2
|
|
192
|
+
fi
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
# Shell config files. Match the filename at a boundary so foo.bashrc.bak
|
|
196
|
+
# doesn't false-positive on ".bashrc" substring.
|
|
197
|
+
SHELL_CONFIG_REGEX="(^|[[:space:]=\`\"'/])\\.(bashrc|zshrc|bash_profile|zprofile|profile)(\$|[[:space:]\"'])"
|
|
198
|
+
if echo "$COMMAND" | grep -qE "$SHELL_CONFIG_REGEX"; then
|
|
199
|
+
MATCH=$(echo "$COMMAND" | grep -oE '\.(bashrc|zshrc|bash_profile|zprofile|profile)' | head -1 || true)
|
|
200
|
+
log_block "Write to shell config: $MATCH" "$COMMAND"
|
|
201
|
+
echo "BLOCKED: Command attempts to modify shell config file '$MATCH'. Modifying shell configuration is forbidden. Ask the developer to make shell changes manually." >&2
|
|
202
|
+
exit 2
|
|
203
|
+
fi
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
exit 0
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# Unknown tool -- allow by default
|
|
210
|
+
exit 0
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# block-sensitive-files.sh
|
|
3
|
+
# PreToolUse hook — blocks Claude Code from reading sensitive files
|
|
4
|
+
# Exit code 2 = block the action, stderr message goes to Claude as feedback
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
LOG_FILE="$HOME/.claude/guardrail-blocks.log"
|
|
9
|
+
HOOK_NAME="block-sensitive-files"
|
|
10
|
+
|
|
11
|
+
log_block() {
|
|
12
|
+
local reason="$1"
|
|
13
|
+
local detail="$2"
|
|
14
|
+
local timestamp
|
|
15
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
16
|
+
echo "{\"timestamp\":\"$timestamp\",\"hook\":\"$HOOK_NAME\",\"reason\":\"$reason\",\"detail\":\"$detail\",\"user\":\"$(whoami)\"}" >> "$LOG_FILE"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
INPUT=$(cat)
|
|
20
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
21
|
+
|
|
22
|
+
# Get the file path depending on the tool
|
|
23
|
+
FILE_PATH=""
|
|
24
|
+
if [ "$TOOL_NAME" = "Read" ]; then
|
|
25
|
+
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
26
|
+
elif [ "$TOOL_NAME" = "Bash" ]; then
|
|
27
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
|
|
28
|
+
# Check if bash command references sensitive files
|
|
29
|
+
SENSITIVE_PATTERNS=(
|
|
30
|
+
'\.env'
|
|
31
|
+
'credentials\.'
|
|
32
|
+
'secret'
|
|
33
|
+
'token'
|
|
34
|
+
'\.pem'
|
|
35
|
+
'\.key$'
|
|
36
|
+
'\.crt$'
|
|
37
|
+
'\.p12'
|
|
38
|
+
'\.pfx'
|
|
39
|
+
'\.keystore'
|
|
40
|
+
'\.jks'
|
|
41
|
+
'\.ssh/'
|
|
42
|
+
'\.aws/'
|
|
43
|
+
'\.azure/'
|
|
44
|
+
'\.gcloud/'
|
|
45
|
+
'snowflake\.config'
|
|
46
|
+
'profiles\.yml'
|
|
47
|
+
'connections\.toml'
|
|
48
|
+
)
|
|
49
|
+
for pattern in "${SENSITIVE_PATTERNS[@]}"; do
|
|
50
|
+
if echo "$COMMAND" | grep -qiE "$pattern"; then
|
|
51
|
+
log_block "Matched pattern: $pattern" "$COMMAND"
|
|
52
|
+
echo "BLOCKED: Command references a sensitive file pattern ($pattern). Do not access credential files, .env files, or secret files. Ask the developer to provide the specific values you need." >&2
|
|
53
|
+
exit 2
|
|
54
|
+
fi
|
|
55
|
+
done
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# For Read tool — check against blocked patterns
|
|
60
|
+
if [ -n "$FILE_PATH" ]; then
|
|
61
|
+
BLOCKED_PATTERNS=(
|
|
62
|
+
'\.env$'
|
|
63
|
+
'\.env\.'
|
|
64
|
+
'credentials\.json'
|
|
65
|
+
'credentials\.yaml'
|
|
66
|
+
'credentials\.toml'
|
|
67
|
+
'secret'
|
|
68
|
+
'token'
|
|
69
|
+
'\.pem$'
|
|
70
|
+
'\.key$'
|
|
71
|
+
'\.crt$'
|
|
72
|
+
'\.p12$'
|
|
73
|
+
'\.pfx$'
|
|
74
|
+
'\.keystore$'
|
|
75
|
+
'\.jks$'
|
|
76
|
+
'/\.ssh/'
|
|
77
|
+
'/\.aws/'
|
|
78
|
+
'/\.azure/'
|
|
79
|
+
'/\.gcloud/'
|
|
80
|
+
'snowflake\.config'
|
|
81
|
+
'profiles\.yml'
|
|
82
|
+
'connections\.toml'
|
|
83
|
+
'/secrets/'
|
|
84
|
+
'/credentials/'
|
|
85
|
+
'/private/'
|
|
86
|
+
'/keys/'
|
|
87
|
+
)
|
|
88
|
+
for pattern in "${BLOCKED_PATTERNS[@]}"; do
|
|
89
|
+
if echo "$FILE_PATH" | grep -qiE "$pattern"; then
|
|
90
|
+
log_block "Matched pattern: $pattern" "$FILE_PATH"
|
|
91
|
+
echo "BLOCKED: Access to '$FILE_PATH' is denied by company security policy. This file matches a protected pattern ($pattern). Ask the developer to provide the specific non-sensitive values you need." >&2
|
|
92
|
+
exit 2
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
exit 0
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"deny": [
|
|
4
|
+
"Read(**/.env)",
|
|
5
|
+
"Read(**/.env.*)",
|
|
6
|
+
"Read(**/.env.local)",
|
|
7
|
+
"Read(**/.env.production)",
|
|
8
|
+
"Read(**/.env.staging)",
|
|
9
|
+
"Read(**/credentials.json)",
|
|
10
|
+
"Read(**/credentials.yaml)",
|
|
11
|
+
"Read(**/credentials.toml)",
|
|
12
|
+
"Read(**/*secret*)",
|
|
13
|
+
"Read(**/*secrets*)",
|
|
14
|
+
"Read(**/*token*)",
|
|
15
|
+
"Read(**/*tokens*)",
|
|
16
|
+
"Read(**/*.pem)",
|
|
17
|
+
"Read(**/*.key)",
|
|
18
|
+
"Read(**/*.crt)",
|
|
19
|
+
"Read(**/*.p12)",
|
|
20
|
+
"Read(**/*.pfx)",
|
|
21
|
+
"Read(**/*.keystore)",
|
|
22
|
+
"Read(**/*.jks)",
|
|
23
|
+
"Read(**/.ssh/*)",
|
|
24
|
+
"Read(**/.aws/*)",
|
|
25
|
+
"Read(**/.azure/*)",
|
|
26
|
+
"Read(**/.gcloud/*)",
|
|
27
|
+
"Read(**/snowflake.config)",
|
|
28
|
+
"Read(**/profiles.yml)",
|
|
29
|
+
"Read(**/connections.toml)",
|
|
30
|
+
"Read(**/secrets/**)",
|
|
31
|
+
"Read(**/credentials/**)",
|
|
32
|
+
"Read(**/private/**)",
|
|
33
|
+
"Read(**/keys/**)",
|
|
34
|
+
"Bash(rm -rf *)",
|
|
35
|
+
"Bash(rm -r *)",
|
|
36
|
+
"Bash(sudo *)",
|
|
37
|
+
"Bash(chmod 777 *)",
|
|
38
|
+
"Bash(git *)",
|
|
39
|
+
"Bash(curl * | sh)",
|
|
40
|
+
"Bash(curl * | bash)",
|
|
41
|
+
"Bash(wget * | sh)",
|
|
42
|
+
"Bash(wget * | bash)",
|
|
43
|
+
"Bash(mkfs*)",
|
|
44
|
+
"Bash(dd *)",
|
|
45
|
+
"Bash(fdisk*)",
|
|
46
|
+
"Bash(kill -9 *)",
|
|
47
|
+
"Bash(snowsql*)",
|
|
48
|
+
"Bash(mysql *-p*)",
|
|
49
|
+
"Bash(psql *)",
|
|
50
|
+
|
|
51
|
+
"Bash(ssh *)",
|
|
52
|
+
"Bash(scp *)",
|
|
53
|
+
"Bash(docker run *)",
|
|
54
|
+
"Bash(docker exec *)",
|
|
55
|
+
"Bash(docker cp *)",
|
|
56
|
+
"Bash(docker build *)",
|
|
57
|
+
"Bash(terraform apply*)",
|
|
58
|
+
"Bash(terraform destroy*)",
|
|
59
|
+
"Bash(terraform import*)",
|
|
60
|
+
"Bash(kubectl delete *)",
|
|
61
|
+
"Bash(kubectl exec *)",
|
|
62
|
+
"Bash(kubectl apply *)",
|
|
63
|
+
"Bash(nc *)",
|
|
64
|
+
"Bash(netcat *)",
|
|
65
|
+
"Bash(ncat *)",
|
|
66
|
+
|
|
67
|
+
"Read(**/.claude/*)",
|
|
68
|
+
"Write(**/.claude/*)",
|
|
69
|
+
"Edit(**/.claude/*)"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|