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.
Files changed (61) hide show
  1. opsentry/VERSION +1 -0
  2. opsentry/__init__.py +5 -0
  3. opsentry/cli.py +295 -0
  4. opsentry/config/__init__.py +0 -0
  5. opsentry/config/generate.py +414 -0
  6. opsentry/config/hook_templates/block-dangerous-commands.sh.j2 +102 -0
  7. opsentry/config/hook_templates/block-data-exfiltration.sh.j2 +94 -0
  8. opsentry/config/hook_templates/block-environment-escape.sh.j2 +99 -0
  9. opsentry/config/hook_templates/block-git-commands.sh.j2 +46 -0
  10. opsentry/config/hook_templates/block-package-install.sh.j2 +95 -0
  11. opsentry/config/hook_templates/block-pii-leakage.sh.j2 +100 -0
  12. opsentry/config/hook_templates/block-scope-escape.sh.j2 +115 -0
  13. opsentry/config/hook_templates/block-sensitive-files.sh.j2 +97 -0
  14. opsentry/config/init_wizard.py +201 -0
  15. opsentry/config/rule_texts/01_sensitive_files.md +15 -0
  16. opsentry/config/rule_texts/02_client_data.md +10 -0
  17. opsentry/config/rule_texts/03_credentials.md +17 -0
  18. opsentry/config/rule_texts/04_sql_safety.md +33 -0
  19. opsentry/config/rule_texts/05_dangerous_code.md +29 -0
  20. opsentry/config/rule_texts/06_frontend_xss.md +9 -0
  21. opsentry/config/rule_texts/07_git_operations_block_all.md +13 -0
  22. opsentry/config/rule_texts/07_git_operations_read_only.md +18 -0
  23. opsentry/config/rule_texts/08_destructive_commands.md +12 -0
  24. opsentry/config/rule_texts/09_database_safety.md +17 -0
  25. opsentry/config/rule_texts/10_network_requests.md +10 -0
  26. opsentry/config/rule_texts/11_code_quality.md +13 -0
  27. opsentry/config/rule_texts/12_data_exfiltration.md +10 -0
  28. opsentry/config/rule_texts/13_package_install.md +9 -0
  29. opsentry/config/rule_texts/14_scope_boundaries.md +8 -0
  30. opsentry/config/rule_texts/15_env_isolation.md +9 -0
  31. opsentry/config/rule_texts/16_resource_limits.md +8 -0
  32. opsentry/config/rule_texts/17_compliance_privacy.md +9 -0
  33. opsentry/config/rule_texts/18_when_in_doubt.md +10 -0
  34. opsentry/config/schema.py +142 -0
  35. opsentry/config/templates/generic.yaml +84 -0
  36. opsentry/deployment/.claude/commands/code-health.md +42 -0
  37. opsentry/deployment/.claude/commands/governance-check.md +29 -0
  38. opsentry/deployment/.claude/commands/security-audit.md +41 -0
  39. opsentry/deployment/baseline.py +440 -0
  40. opsentry/deployment/blocklog_audit.py +497 -0
  41. opsentry/deployment/claude/CLAUDE.md +300 -0
  42. opsentry/deployment/claude/hooks/block-dangerous-commands.sh +106 -0
  43. opsentry/deployment/claude/hooks/block-data-exfiltration.sh +99 -0
  44. opsentry/deployment/claude/hooks/block-environment-escape.sh +96 -0
  45. opsentry/deployment/claude/hooks/block-git-commands.sh +37 -0
  46. opsentry/deployment/claude/hooks/block-package-install.sh +103 -0
  47. opsentry/deployment/claude/hooks/block-pii-leakage.sh +86 -0
  48. opsentry/deployment/claude/hooks/block-scope-escape.sh +210 -0
  49. opsentry/deployment/claude/hooks/block-sensitive-files.sh +97 -0
  50. opsentry/deployment/claude/settings.json +72 -0
  51. opsentry/deployment/install.sh +343 -0
  52. opsentry/deployment/patrol.sh +266 -0
  53. opsentry/deployment/test.sh +728 -0
  54. opsentry/deployment/update.sh +19 -0
  55. opsentry/deployment/verify.sh +201 -0
  56. opsentry-1.8.0.dist-info/METADATA +158 -0
  57. opsentry-1.8.0.dist-info/RECORD +61 -0
  58. opsentry-1.8.0.dist-info/WHEEL +5 -0
  59. opsentry-1.8.0.dist-info/entry_points.txt +2 -0
  60. opsentry-1.8.0.dist-info/licenses/LICENSE +190 -0
  61. 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
+ }