cc-safe-setup 29.4.0 → 29.5.0
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 +12 -5
- package/TROUBLESHOOTING.md +13 -0
- package/examples/auto-mode-safe-commands.sh +108 -0
- package/examples/checkpoint-tamper-guard.sh +47 -0
- package/examples/compound-command-allow.sh +163 -0
- package/examples/write-secret-guard.sh +125 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npx cc-safe-setup
|
|
|
12
12
|
|
|
13
13
|
Installs 8 safety hooks in ~10 seconds. Blocks `rm -rf /`, prevents pushes to main, catches secret leaks, validates syntax after every edit. Zero dependencies.
|
|
14
14
|
|
|
15
|
-
[**Getting Started**](https://yurukusa.github.io/cc-safe-setup/getting-started.html) · [**All Tools**](https://yurukusa.github.io/cc-safe-setup/hub.html) · [**Recipes**](https://yurukusa.github.io/cc-safe-setup/recipes.html) · [Validate your settings.json](https://yurukusa.github.io/cc-safe-setup/validator.html)
|
|
15
|
+
[**Getting Started**](https://yurukusa.github.io/cc-safe-setup/getting-started.html) · [**All Tools**](https://yurukusa.github.io/cc-safe-setup/hub.html) · [**Recipes**](https://yurukusa.github.io/cc-safe-setup/recipes.html) · [Validate your settings.json](https://yurukusa.github.io/cc-safe-setup/validator.html) · [**Check your score**](https://yurukusa.github.io/cc-health-check/) (`npx cc-health-check`)
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
cc-safe-setup
|
|
@@ -49,6 +49,8 @@ A Claude Code user [lost their entire C:\Users directory](https://github.com/ant
|
|
|
49
49
|
|
|
50
50
|
Claude Code ships with no safety hooks by default. This tool fixes that.
|
|
51
51
|
|
|
52
|
+
**Works with Auto Mode.** Claude Code's [Auto Mode sandboxing](https://www.anthropic.com/engineering/claude-code-sandboxing) provides container-level isolation. cc-safe-setup adds process-level hooks as defense-in-depth — catching destructive commands even outside sandboxed environments.
|
|
53
|
+
|
|
52
54
|
## What Gets Installed
|
|
53
55
|
|
|
54
56
|
| Hook | Prevents | Related Issues |
|
|
@@ -87,7 +89,7 @@ Each hook exists because a real incident happened without it.
|
|
|
87
89
|
| `--scan [--apply]` | Tech stack detection |
|
|
88
90
|
| `--export / --import` | Team config sharing |
|
|
89
91
|
| `--verify` | Test each hook |
|
|
90
|
-
| `--install-example <name>` | Install from
|
|
92
|
+
| `--install-example <name>` | Install from 335 examples |
|
|
91
93
|
| `--examples [filter]` | Browse examples by keyword |
|
|
92
94
|
| `--full` | All-in-one setup |
|
|
93
95
|
| `--status` | Check installed hooks |
|
|
@@ -211,11 +213,11 @@ npx cc-health-check
|
|
|
211
213
|
|
|
212
214
|
## Full Kit
|
|
213
215
|
|
|
214
|
-
cc-safe-setup gives you 8 essential hooks.
|
|
216
|
+
cc-safe-setup gives you 8 essential hooks. Want to know what else your setup needs?
|
|
215
217
|
|
|
216
|
-
**[Claude Code Ops Kit](https://yurukusa.github.io/cc-ops-kit-landing/?utm_source=github&utm_medium=readme&utm_campaign=safe-setup)** — 16 hooks +
|
|
218
|
+
Run `npx cc-health-check` (free, 20 checks) to see your current score. If it's below 80, the **[Claude Code Ops Kit](https://yurukusa.github.io/cc-ops-kit-landing/?utm_source=github&utm_medium=readme&utm_campaign=safe-setup)** fills the gaps — 16 hooks + 6 templates + 3 exclusive tools + install.sh. Pay What You Want.
|
|
217
219
|
|
|
218
|
-
Or
|
|
220
|
+
Or browse the free hooks: [claude-code-hooks](https://github.com/yurukusa/claude-code-hooks)
|
|
219
221
|
|
|
220
222
|
## Examples
|
|
221
223
|
|
|
@@ -368,6 +370,9 @@ See [Issue #1](https://github.com/yurukusa/cc-safe-setup/issues/1) for details.
|
|
|
368
370
|
- [Hooks Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/cheatsheet.html) — printable A4 quick reference
|
|
369
371
|
- [Ecosystem Comparison](https://yurukusa.github.io/cc-safe-setup/ecosystem.html) — all Claude Code hook projects compared
|
|
370
372
|
- [The incident that inspired this tool](https://github.com/anthropics/claude-code/issues/36339) — NTFS junction rm -rf
|
|
373
|
+
- [How to prevent rm -rf disasters](https://yurukusa.github.io/cc-safe-setup/prevent-rm-rf.html) — real incidents and the hook that stops them
|
|
374
|
+
- [How to prevent force-push to main](https://yurukusa.github.io/cc-safe-setup/prevent-force-push.html) — branch protection via hooks
|
|
375
|
+
- [How to prevent secret leaks](https://yurukusa.github.io/cc-safe-setup/prevent-secret-leaks.html) — stop git add . from committing .env
|
|
371
376
|
|
|
372
377
|
## FAQ
|
|
373
378
|
|
|
@@ -387,6 +392,8 @@ No. Each hook runs in ~10ms. They only fire on specific events (before tool use,
|
|
|
387
392
|
|
|
388
393
|
Found a false positive? Open an [issue](https://github.com/yurukusa/cc-safe-setup/issues/new?template=false_positive.md). Want a new hook? Open a [feature request](https://github.com/yurukusa/cc-safe-setup/issues/new?template=bug_report.md).
|
|
389
394
|
|
|
395
|
+
📘 **Want the full story?** [Production guide from 700+ hours of autonomous operation](https://zenn.dev/yurukusa/books/6076c23b1cb18b) — the incidents, fixes, and patterns behind every hook in this tool.
|
|
396
|
+
|
|
390
397
|
If cc-safe-setup saved your project from a destructive command, consider giving it a star — it helps others find this tool.
|
|
391
398
|
|
|
392
399
|
## License
|
package/TROUBLESHOOTING.md
CHANGED
|
@@ -160,6 +160,19 @@ exit 0
|
|
|
160
160
|
|
|
161
161
|
**Rule of thumb:** PreToolUse = block dangerous actions. PermissionRequest = allow trusted actions that trigger built-in prompts.
|
|
162
162
|
|
|
163
|
+
## "PermissionRequest hooks don't fire in `-p` mode"
|
|
164
|
+
|
|
165
|
+
**Known limitation** ([#35646](https://github.com/anthropics/claude-code/issues/35646)): In headless/pipe mode (`claude -p`), the protected-directory check short-circuits *before* PermissionRequest hooks fire. This means:
|
|
166
|
+
|
|
167
|
+
| Mode | PermissionRequest fires? | Hook workaround works? |
|
|
168
|
+
|------|-------------------------|----------------------|
|
|
169
|
+
| Interactive (`claude`) | ✅ Yes | ✅ Yes |
|
|
170
|
+
| Interactive + bypassPermissions | ✅ Yes | ✅ Yes |
|
|
171
|
+
| Pipe mode (`claude -p`) | ❌ No | ❌ No |
|
|
172
|
+
| Pipe + `--dangerously-skip-permissions` | ❌ No | ❌ No |
|
|
173
|
+
|
|
174
|
+
**Workaround:** Currently none for `-p` mode. If your automation needs to write to `.claude/`, use interactive mode with hooks instead. This is a Claude Code core issue — the fix requires the harness to route protected-dir checks through PermissionRequest in all modes.
|
|
175
|
+
|
|
163
176
|
## "Permission prompts still appear for compound commands"
|
|
164
177
|
|
|
165
178
|
This is a known Claude Code limitation, not a hook issue. `Bash(git:*)` doesn't match `cd /path && git log`.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# auto-mode-safe-commands.sh — Fix Auto Mode false positives on safe commands
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude Code's safety classifier blocks legitimate commands in auto mode
|
|
5
|
+
# - $() command substitution flagged as dangerous (#38537, 49 reactions)
|
|
6
|
+
# - Pipe chains flagged unnecessarily (#30435, 29 reactions)
|
|
7
|
+
# - Read-only commands requiring manual approval
|
|
8
|
+
#
|
|
9
|
+
# How it works: Maintains a whitelist of known-safe command patterns.
|
|
10
|
+
# When the classifier wrongly blocks them, this hook approves.
|
|
11
|
+
# Only approves commands that are genuinely read-only or development-safe.
|
|
12
|
+
#
|
|
13
|
+
# Usage: Add to settings.json as a PreToolUse hook
|
|
14
|
+
#
|
|
15
|
+
# {
|
|
16
|
+
# "hooks": {
|
|
17
|
+
# "PreToolUse": [{
|
|
18
|
+
# "matcher": "Bash",
|
|
19
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/auto-mode-safe-commands.sh" }]
|
|
20
|
+
# }]
|
|
21
|
+
# }
|
|
22
|
+
# }
|
|
23
|
+
|
|
24
|
+
INPUT=$(cat)
|
|
25
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
26
|
+
|
|
27
|
+
[ -z "$COMMAND" ] && exit 0
|
|
28
|
+
|
|
29
|
+
# Strip the command to its base (first word after pipes, &&, etc.)
|
|
30
|
+
# We check each component of compound commands
|
|
31
|
+
APPROVE=false
|
|
32
|
+
REASON=""
|
|
33
|
+
|
|
34
|
+
# --- Read-only commands (never modify state) ---
|
|
35
|
+
|
|
36
|
+
# File inspection
|
|
37
|
+
if echo "$COMMAND" | grep -qE '^\s*(cat|head|tail|less|more|wc|file|stat|du|df|ls|tree|find|which|whereis|type|realpath|readlink)\s'; then
|
|
38
|
+
APPROVE=true
|
|
39
|
+
REASON="Read-only file inspection"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Text search
|
|
43
|
+
if echo "$COMMAND" | grep -qE '^\s*(grep|rg|ag|ack|sed\s+-n|awk)\s'; then
|
|
44
|
+
APPROVE=true
|
|
45
|
+
REASON="Text search/extraction"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Git read-only
|
|
49
|
+
if echo "$COMMAND" | grep -qE '^\s*git\s+(status|log|diff|show|branch|tag|remote|stash\s+list|ls-files|ls-tree|rev-parse|describe|shortlog|blame|config\s+--get)'; then
|
|
50
|
+
APPROVE=true
|
|
51
|
+
REASON="Git read-only operation"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Package info (read-only)
|
|
55
|
+
if echo "$COMMAND" | grep -qE '^\s*(npm\s+(ls|list|info|view|outdated|audit)|pip\s+(list|show|freeze)|yarn\s+(list|info|why)|pnpm\s+(ls|list))\s*'; then
|
|
56
|
+
APPROVE=true
|
|
57
|
+
REASON="Package manager read-only"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Development tools (safe)
|
|
61
|
+
if echo "$COMMAND" | grep -qE '^\s*(echo|printf|date|env|printenv|uname|hostname|whoami|id|pwd|tput)\s*'; then
|
|
62
|
+
APPROVE=true
|
|
63
|
+
REASON="Environment inspection"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# --- Safe command substitution patterns ---
|
|
67
|
+
# $() is flagged by classifier but usually wraps read-only commands
|
|
68
|
+
|
|
69
|
+
# date/timestamp substitution
|
|
70
|
+
if echo "$COMMAND" | grep -qE '\$\(date\s'; then
|
|
71
|
+
# Only approve if the outer command is also safe
|
|
72
|
+
OUTER=$(echo "$COMMAND" | sed 's/\$([^)]*)/SUBST/g')
|
|
73
|
+
if echo "$OUTER" | grep -qE '^\s*(echo|printf|mkdir|touch|cp|mv)\s'; then
|
|
74
|
+
APPROVE=true
|
|
75
|
+
REASON="Safe command with date substitution"
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# --- JSON/YAML processing ---
|
|
80
|
+
if echo "$COMMAND" | grep -qE '^\s*(jq|yq|python3?\s+-c\s|python3?\s+-m\s+json)\s'; then
|
|
81
|
+
APPROVE=true
|
|
82
|
+
REASON="JSON/YAML processing"
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# --- Curl (read-only GET requests) ---
|
|
86
|
+
if echo "$COMMAND" | grep -qE '^\s*curl\s+-s' && ! echo "$COMMAND" | grep -qE '\s-X\s+(POST|PUT|PATCH|DELETE)'; then
|
|
87
|
+
APPROVE=true
|
|
88
|
+
REASON="HTTP GET request"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# --- Node.js/Python one-liners ---
|
|
92
|
+
if echo "$COMMAND" | grep -qE '^\s*(node|python3?)\s+-e\s'; then
|
|
93
|
+
# Only approve if no file system writes detected
|
|
94
|
+
if ! echo "$COMMAND" | grep -qE '(writeFile|fs\.write|open\(.*["\x27]w|unlink|rmdir)'; then
|
|
95
|
+
APPROVE=true
|
|
96
|
+
REASON="Script one-liner (no fs writes detected)"
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# --- Output the decision ---
|
|
101
|
+
if [ "$APPROVE" = true ]; then
|
|
102
|
+
jq -n --arg reason "$REASON" \
|
|
103
|
+
'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":$reason}}'
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# No opinion — let the default classifier handle it
|
|
108
|
+
exit 0
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# checkpoint-tamper-guard.sh — Block manipulation of hook state/checkpoint files
|
|
3
|
+
# Trigger: PreToolUse (Bash, Edit, Write)
|
|
4
|
+
# Prevents the model from bypassing hooks by editing their state files
|
|
5
|
+
# See: https://github.com/anthropics/claude-code/issues/38841
|
|
6
|
+
|
|
7
|
+
INPUT=$(cat)
|
|
8
|
+
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
9
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
10
|
+
|
|
11
|
+
# Directories/files containing hook state (customize as needed)
|
|
12
|
+
PROTECTED_PATTERNS=(
|
|
13
|
+
".claude/checkpoints"
|
|
14
|
+
".claude/hook-state"
|
|
15
|
+
".claude/hooks-disabled"
|
|
16
|
+
"session-call-count"
|
|
17
|
+
"compact-prep-done"
|
|
18
|
+
"subagent-tracker"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
check_path() {
|
|
22
|
+
local path="$1"
|
|
23
|
+
for pattern in "${PROTECTED_PATTERNS[@]}"; do
|
|
24
|
+
if [[ "$path" == *"$pattern"* ]]; then
|
|
25
|
+
echo "BLOCKED: Cannot manipulate hook state file: $path" >&2
|
|
26
|
+
echo "Hook state files are managed by hooks, not by the model." >&2
|
|
27
|
+
exit 2
|
|
28
|
+
fi
|
|
29
|
+
done
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Check Bash commands that write to protected paths
|
|
33
|
+
if [ -n "$CMD" ]; then
|
|
34
|
+
for pattern in "${PROTECTED_PATTERNS[@]}"; do
|
|
35
|
+
if echo "$CMD" | grep -qE "(echo|cat|tee|cp|mv|rm|chmod|chown|touch|truncate|>).*${pattern}"; then
|
|
36
|
+
echo "BLOCKED: Cannot manipulate hook state via command" >&2
|
|
37
|
+
exit 2
|
|
38
|
+
fi
|
|
39
|
+
done
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Check Edit/Write file paths
|
|
43
|
+
if [ -n "$FILE" ]; then
|
|
44
|
+
check_path "$FILE"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
exit 0
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# compound-command-allow.sh — Auto-approve compound commands when all parts are safe
|
|
3
|
+
#
|
|
4
|
+
# Solves: Permission prompts fire for compound commands like:
|
|
5
|
+
# cd /path && git log (#16561, 115 reactions)
|
|
6
|
+
# echo foo | grep bar (#28240, 84 reactions)
|
|
7
|
+
# npm test && npm run build (#30519, 58 reactions)
|
|
8
|
+
#
|
|
9
|
+
# How it works: Splits compound commands on &&, ||, ;, and |
|
|
10
|
+
# Checks each component against a safe-command whitelist.
|
|
11
|
+
# If ALL components are safe, auto-approves the entire command.
|
|
12
|
+
# If ANY component is unsafe, passes through (no opinion).
|
|
13
|
+
#
|
|
14
|
+
# This extends cd-git-allow to handle arbitrary compound commands.
|
|
15
|
+
#
|
|
16
|
+
# Usage: Add to settings.json as a PreToolUse hook
|
|
17
|
+
#
|
|
18
|
+
# {
|
|
19
|
+
# "hooks": {
|
|
20
|
+
# "PreToolUse": [{
|
|
21
|
+
# "matcher": "Bash",
|
|
22
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/compound-command-allow.sh" }]
|
|
23
|
+
# }]
|
|
24
|
+
# }
|
|
25
|
+
# }
|
|
26
|
+
|
|
27
|
+
INPUT=$(cat)
|
|
28
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
29
|
+
|
|
30
|
+
[ -z "$COMMAND" ] && exit 0
|
|
31
|
+
|
|
32
|
+
# Strip comments (lines starting with #) to avoid false matches
|
|
33
|
+
CLEAN=$(echo "$COMMAND" | sed 's/#.*$//' | tr '\n' ' ')
|
|
34
|
+
|
|
35
|
+
# Split on &&, ||, ;, and | (but not || inside [[ ]])
|
|
36
|
+
# Simple approach: split on these operators and check each part
|
|
37
|
+
IFS_ORIG="$IFS"
|
|
38
|
+
|
|
39
|
+
# Replace compound operators with a delimiter
|
|
40
|
+
PARTS=$(echo "$CLEAN" | sed 's/\s*&&\s*/\n/g; s/\s*||\s*/\n/g; s/\s*;\s*/\n/g; s/\s*|\s*/\n/g')
|
|
41
|
+
|
|
42
|
+
ALL_SAFE=true
|
|
43
|
+
|
|
44
|
+
while IFS= read -r part; do
|
|
45
|
+
# Trim whitespace
|
|
46
|
+
part=$(echo "$part" | sed 's/^\s*//;s/\s*$//')
|
|
47
|
+
[ -z "$part" ] && continue
|
|
48
|
+
|
|
49
|
+
# Extract the base command (first word)
|
|
50
|
+
BASE=$(echo "$part" | awk '{print $1}')
|
|
51
|
+
|
|
52
|
+
# Check against safe command list
|
|
53
|
+
case "$BASE" in
|
|
54
|
+
# Navigation
|
|
55
|
+
cd|pushd|popd|pwd)
|
|
56
|
+
;;
|
|
57
|
+
# File reading
|
|
58
|
+
cat|head|tail|less|more|wc|file|stat|du|df|ls|tree|find|which|whereis|type|realpath|readlink|basename|dirname)
|
|
59
|
+
;;
|
|
60
|
+
# Text processing (read-only)
|
|
61
|
+
grep|rg|ag|ack|sed|awk|sort|uniq|cut|tr|tee|xargs|column|fmt|fold|rev|nl|paste|join|comm)
|
|
62
|
+
# sed with -i is NOT read-only
|
|
63
|
+
if echo "$part" | grep -qE 'sed\s+.*-i'; then
|
|
64
|
+
ALL_SAFE=false
|
|
65
|
+
break
|
|
66
|
+
fi
|
|
67
|
+
;;
|
|
68
|
+
# Git (read-only operations)
|
|
69
|
+
git)
|
|
70
|
+
SUBCMD=$(echo "$part" | awk '{print $2}')
|
|
71
|
+
case "$SUBCMD" in
|
|
72
|
+
status|log|diff|show|branch|tag|remote|stash|ls-files|ls-tree|rev-parse|describe|shortlog|blame|config|worktree)
|
|
73
|
+
# git stash with push/pop/drop is not read-only
|
|
74
|
+
if echo "$part" | grep -qE 'git\s+stash\s+(push|pop|drop|apply|clear)'; then
|
|
75
|
+
ALL_SAFE=false
|
|
76
|
+
break
|
|
77
|
+
fi
|
|
78
|
+
;;
|
|
79
|
+
*)
|
|
80
|
+
ALL_SAFE=false
|
|
81
|
+
break
|
|
82
|
+
;;
|
|
83
|
+
esac
|
|
84
|
+
;;
|
|
85
|
+
# Node.js/npm (read-only)
|
|
86
|
+
node|npm|npx|yarn|pnpm)
|
|
87
|
+
SUBCMD=$(echo "$part" | awk '{print $2}')
|
|
88
|
+
case "$BASE" in
|
|
89
|
+
npm)
|
|
90
|
+
case "$SUBCMD" in
|
|
91
|
+
ls|list|info|view|outdated|audit|explain|why|help|config|prefix|root)
|
|
92
|
+
;;
|
|
93
|
+
test|run)
|
|
94
|
+
;; # npm test/run are generally safe
|
|
95
|
+
*)
|
|
96
|
+
ALL_SAFE=false
|
|
97
|
+
break
|
|
98
|
+
;;
|
|
99
|
+
esac
|
|
100
|
+
;;
|
|
101
|
+
node)
|
|
102
|
+
if echo "$part" | grep -qE 'node\s+-e\s'; then
|
|
103
|
+
if echo "$part" | grep -qE '(writeFile|fs\.write|unlink|rmdir|mkdirSync)'; then
|
|
104
|
+
ALL_SAFE=false
|
|
105
|
+
break
|
|
106
|
+
fi
|
|
107
|
+
elif echo "$part" | grep -qE 'node\s+-p\s'; then
|
|
108
|
+
: # node -p is safe (eval + print)
|
|
109
|
+
fi
|
|
110
|
+
;;
|
|
111
|
+
*)
|
|
112
|
+
;; # npx, yarn, pnpm — allow for now
|
|
113
|
+
esac
|
|
114
|
+
;;
|
|
115
|
+
# Python (read-only)
|
|
116
|
+
python|python3)
|
|
117
|
+
if echo "$part" | grep -qE 'python3?\s+(-c|-m\s+(json|py_compile|compileall|ast|tokenize|dis|inspect))'; then
|
|
118
|
+
: # Safe one-liners
|
|
119
|
+
elif echo "$part" | grep -qE 'python3?\s+-m\s+pytest'; then
|
|
120
|
+
: # pytest is safe
|
|
121
|
+
else
|
|
122
|
+
ALL_SAFE=false
|
|
123
|
+
break
|
|
124
|
+
fi
|
|
125
|
+
;;
|
|
126
|
+
# Shell builtins (safe)
|
|
127
|
+
echo|printf|true|false|test|\[|export|set|env|printenv|date|sleep|read|source|\.)
|
|
128
|
+
;;
|
|
129
|
+
# System info
|
|
130
|
+
uname|hostname|whoami|id|groups|uptime|free|top|ps|lsb_release|arch|nproc|getconf)
|
|
131
|
+
;;
|
|
132
|
+
# JSON/YAML processing
|
|
133
|
+
jq|yq)
|
|
134
|
+
;;
|
|
135
|
+
# curl (GET only)
|
|
136
|
+
curl)
|
|
137
|
+
if echo "$part" | grep -qE '\s-X\s+(POST|PUT|PATCH|DELETE)'; then
|
|
138
|
+
ALL_SAFE=false
|
|
139
|
+
break
|
|
140
|
+
fi
|
|
141
|
+
;;
|
|
142
|
+
# mkdir is generally safe
|
|
143
|
+
mkdir)
|
|
144
|
+
;;
|
|
145
|
+
# touch is generally safe
|
|
146
|
+
touch)
|
|
147
|
+
;;
|
|
148
|
+
*)
|
|
149
|
+
ALL_SAFE=false
|
|
150
|
+
break
|
|
151
|
+
;;
|
|
152
|
+
esac
|
|
153
|
+
done <<< "$PARTS"
|
|
154
|
+
|
|
155
|
+
IFS="$IFS_ORIG"
|
|
156
|
+
|
|
157
|
+
if [ "$ALL_SAFE" = true ]; then
|
|
158
|
+
jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"All components of compound command are safe"}}'
|
|
159
|
+
exit 0
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Unsafe component found — no opinion, let default handler decide
|
|
163
|
+
exit 0
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# write-secret-guard.sh — Block secrets from being written to files
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude writes API keys, tokens, and passwords directly into source files
|
|
5
|
+
# instead of using environment variables (#29910, 14 reactions)
|
|
6
|
+
# Existing secret-guard only covers Bash (git add .env).
|
|
7
|
+
# This hook covers Write and Edit tools.
|
|
8
|
+
#
|
|
9
|
+
# Detects: AWS keys (AKIA...), GitHub tokens (ghp_/gho_/ghs_),
|
|
10
|
+
# OpenAI keys (sk-), Anthropic keys (sk-ant-),
|
|
11
|
+
# Slack tokens (xoxb-/xoxp-), Stripe keys (sk_live_/pk_live_),
|
|
12
|
+
# Generic Bearer tokens, private keys, high-entropy strings
|
|
13
|
+
#
|
|
14
|
+
# Usage: Add to settings.json as a PreToolUse hook for Write AND Edit
|
|
15
|
+
#
|
|
16
|
+
# {
|
|
17
|
+
# "hooks": {
|
|
18
|
+
# "PreToolUse": [{
|
|
19
|
+
# "matcher": "Write",
|
|
20
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/write-secret-guard.sh" }]
|
|
21
|
+
# }, {
|
|
22
|
+
# "matcher": "Edit",
|
|
23
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/write-secret-guard.sh" }]
|
|
24
|
+
# }]
|
|
25
|
+
# }
|
|
26
|
+
# }
|
|
27
|
+
|
|
28
|
+
INPUT=$(cat)
|
|
29
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
30
|
+
|
|
31
|
+
# Get the content being written
|
|
32
|
+
if [ "$TOOL" = "Write" ]; then
|
|
33
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null)
|
|
34
|
+
FILEPATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
35
|
+
elif [ "$TOOL" = "Edit" ]; then
|
|
36
|
+
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null)
|
|
37
|
+
FILEPATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
38
|
+
else
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
[ -z "$CONTENT" ] && exit 0
|
|
43
|
+
|
|
44
|
+
# --- Allow known safe patterns ---
|
|
45
|
+
|
|
46
|
+
# Allow .env.example / .env.template (these contain placeholders)
|
|
47
|
+
if echo "$FILEPATH" | grep -qE '\.(example|template|sample)$'; then
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Allow test files
|
|
52
|
+
if echo "$FILEPATH" | grep -qE '(test|spec|mock|fixture|__test__|\.test\.)'; then
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# --- Detect secret patterns ---
|
|
57
|
+
|
|
58
|
+
BLOCKED=""
|
|
59
|
+
|
|
60
|
+
# AWS Access Key ID (AKIA followed by 16 uppercase alphanumeric)
|
|
61
|
+
if echo "$CONTENT" | grep -qE 'AKIA[0-9A-Z]{16}'; then
|
|
62
|
+
BLOCKED="AWS Access Key ID (AKIA...)"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# AWS Secret Access Key (40 char base64-like after specific prefixes)
|
|
66
|
+
if echo "$CONTENT" | grep -qE '(aws_secret_access_key|AWS_SECRET)\s*[=:]\s*[A-Za-z0-9/+=]{40}'; then
|
|
67
|
+
BLOCKED="AWS Secret Access Key"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# GitHub tokens
|
|
71
|
+
if echo "$CONTENT" | grep -qE '(ghp_|gho_|ghs_|ghr_|github_pat_)[A-Za-z0-9_]{20,}'; then
|
|
72
|
+
BLOCKED="GitHub token"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# OpenAI API key (sk-... or sk-proj-...)
|
|
76
|
+
if echo "$CONTENT" | grep -qE 'sk-[A-Za-z0-9_-]{20,}' && ! echo "$CONTENT" | grep -qE 'sk-ant-'; then
|
|
77
|
+
# Exclude Anthropic keys (handled separately)
|
|
78
|
+
BLOCKED="OpenAI API key (sk-...)"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Anthropic API key
|
|
82
|
+
if echo "$CONTENT" | grep -qE 'sk-ant-[A-Za-z0-9-]{20,}'; then
|
|
83
|
+
BLOCKED="Anthropic API key (sk-ant-...)"
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# Slack tokens
|
|
87
|
+
if echo "$CONTENT" | grep -qE '(xoxb-|xoxp-|xoxs-|xoxa-)[0-9A-Za-z-]{20,}'; then
|
|
88
|
+
BLOCKED="Slack token"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Stripe keys
|
|
92
|
+
if echo "$CONTENT" | grep -qE '(sk_live_|pk_live_|rk_live_)[A-Za-z0-9]{20,}'; then
|
|
93
|
+
BLOCKED="Stripe API key"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Google API key
|
|
97
|
+
if echo "$CONTENT" | grep -qE 'AIza[0-9A-Za-z_-]{35}'; then
|
|
98
|
+
BLOCKED="Google API key"
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# Private keys (PEM format)
|
|
102
|
+
if echo "$CONTENT" | grep -qE -- '-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----'; then
|
|
103
|
+
BLOCKED="Private key (PEM format)"
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Bearer token assignment (not in comments or docs)
|
|
107
|
+
if echo "$CONTENT" | grep -qE '(Authorization|Bearer|token)\s*[=:]\s*["\x27][A-Za-z0-9._-]{30,}["\x27]' \
|
|
108
|
+
&& ! echo "$FILEPATH" | grep -qiE '\.(md|txt|rst|adoc)$'; then
|
|
109
|
+
BLOCKED="Hardcoded Bearer/auth token"
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# Generic database connection strings with credentials
|
|
113
|
+
if echo "$CONTENT" | grep -qE '(mysql|postgres|mongodb|redis)://[^:]+:[^@]+@'; then
|
|
114
|
+
BLOCKED="Database connection string with credentials"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# --- Block if secret detected ---
|
|
118
|
+
|
|
119
|
+
if [ -n "$BLOCKED" ]; then
|
|
120
|
+
echo "BLOCKED: Secret detected in file write — $BLOCKED" >&2
|
|
121
|
+
echo "Use environment variables instead: process.env.KEY or os.environ['KEY']" >&2
|
|
122
|
+
exit 2
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
exit 0
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "29.
|
|
4
|
-
"description": "One command to make Claude Code safe.
|
|
3
|
+
"version": "29.5.0",
|
|
4
|
+
"description": "One command to make Claude Code safe. 335 example hooks + 8 built-in. 52 CLI commands. 1,760 tests. Works with Auto Mode.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cc-safe-setup": "index.mjs"
|
|
@@ -24,7 +24,14 @@
|
|
|
24
24
|
"syntax-check",
|
|
25
25
|
"context-window",
|
|
26
26
|
"wsl",
|
|
27
|
-
"wsl2"
|
|
27
|
+
"wsl2",
|
|
28
|
+
"auto-mode",
|
|
29
|
+
"defense-in-depth",
|
|
30
|
+
"force-push",
|
|
31
|
+
"secret-leak",
|
|
32
|
+
"destructive-command",
|
|
33
|
+
"claude-code-hooks",
|
|
34
|
+
"claude-code-safety"
|
|
28
35
|
],
|
|
29
36
|
"scripts": {
|
|
30
37
|
"test": "bash test.sh"
|