cc-safe-setup 29.5.0 → 29.6.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.
@@ -0,0 +1,73 @@
1
+ #!/bin/bash
2
+ # credential-exfil-guard.sh — Block credential hunting commands
3
+ #
4
+ # Solves: Agents scanning for tokens, secrets, and credentials without permission
5
+ # (#37845 — 48 bash commands auto-executed to exfiltrate credentials)
6
+ #
7
+ # Detects patterns like:
8
+ # env | grep -i token
9
+ # find / -name "*.token" -o -name "*credentials*"
10
+ # cat ~/.ssh/id_rsa
11
+ # printenv | grep SECRET
12
+ # cat /etc/shadow
13
+ #
14
+ # Usage: Add to settings.json as a PreToolUse hook
15
+ #
16
+ # {
17
+ # "hooks": {
18
+ # "PreToolUse": [{
19
+ # "matcher": "Bash",
20
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/credential-exfil-guard.sh" }]
21
+ # }]
22
+ # }
23
+ # }
24
+
25
+ INPUT=$(cat)
26
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
27
+
28
+ [ -z "$COMMAND" ] && exit 0
29
+
30
+ # Pattern 1: env/printenv piped to grep for secrets
31
+ if echo "$COMMAND" | grep -qiE '(env|printenv|set)\s*\|.*grep.*\b(token|secret|key|password|credential|auth|oauth|cookie|session|api.key)\b'; then
32
+ echo "BLOCKED: Credential hunting via environment variable scanning" >&2
33
+ exit 2
34
+ fi
35
+
36
+ # Pattern 2: find searching for credential files
37
+ if echo "$COMMAND" | grep -qiE 'find\s.*-name\s.*\*?(token|secret|credential|password|\.key|\.pem|\.p12|\.pfx|\.keystore|\.jks|\.env)'; then
38
+ echo "BLOCKED: Credential hunting via file system search" >&2
39
+ exit 2
40
+ fi
41
+
42
+ # Pattern 3: Direct access to known credential locations
43
+ if echo "$COMMAND" | grep -qE 'cat\s+(~|/home|/root)/.ssh/(id_|authorized_keys|known_hosts|config)'; then
44
+ echo "BLOCKED: Direct SSH credential access" >&2
45
+ exit 2
46
+ fi
47
+
48
+ # Pattern 4: Reading system credential files
49
+ if echo "$COMMAND" | grep -qE 'cat\s+(/etc/shadow|/etc/gshadow|/etc/passwd)'; then
50
+ echo "BLOCKED: System credential file access" >&2
51
+ exit 2
52
+ fi
53
+
54
+ # Pattern 5: AWS/cloud credential files
55
+ if echo "$COMMAND" | grep -qE 'cat\s+(~|/home|/root)/\.(aws|gcloud|azure|kube)/(credentials|config|token)'; then
56
+ echo "BLOCKED: Cloud provider credential access" >&2
57
+ exit 2
58
+ fi
59
+
60
+ # Pattern 6: Browser credential stores
61
+ if echo "$COMMAND" | grep -qiE 'find\s.*\.(chrome|firefox|mozilla|safari).*\b(login|password|cookie|token)\b'; then
62
+ echo "BLOCKED: Browser credential hunting" >&2
63
+ exit 2
64
+ fi
65
+
66
+ # Pattern 7: Dumping all environment variables (without filtering)
67
+ if echo "$COMMAND" | grep -qE '^\s*(env|printenv|set)\s*$'; then
68
+ echo "WARNING: Dumping all environment variables may expose secrets" >&2
69
+ # Don't block, just warn — some legitimate uses exist
70
+ exit 0
71
+ fi
72
+
73
+ exit 0
@@ -0,0 +1,49 @@
1
+ #!/bin/bash
2
+ # file-change-tracker.sh — Track all file modifications in a session
3
+ #
4
+ # Solves: Hard to know which files Claude modified during a session.
5
+ # Git diff shows the final state but not the order of changes.
6
+ # This log shows every Write/Edit in chronological order.
7
+ #
8
+ # How it works: PostToolUse hook for Write/Edit that logs each change.
9
+ # Creates a timestamped changelog at ~/.claude/session-changes.log
10
+ #
11
+ # Usage: Add to settings.json as a PostToolUse hook
12
+ #
13
+ # {
14
+ # "hooks": {
15
+ # "PostToolUse": [{
16
+ # "matcher": "Write",
17
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/file-change-tracker.sh" }]
18
+ # }, {
19
+ # "matcher": "Edit",
20
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/file-change-tracker.sh" }]
21
+ # }]
22
+ # }
23
+ # }
24
+ #
25
+ # View changes: cat ~/.claude/session-changes.log
26
+
27
+ INPUT=$(cat)
28
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
29
+
30
+ [ -z "$TOOL" ] && exit 0
31
+
32
+ LOG_FILE="${CC_CHANGE_LOG:-$HOME/.claude/session-changes.log}"
33
+ TIMESTAMP=$(date '+%H:%M:%S')
34
+
35
+ case "$TOOL" in
36
+ Write)
37
+ FILEPATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
38
+ CONTENT_LEN=$(echo "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null | wc -c)
39
+ echo "$TIMESTAMP WRITE $FILEPATH (${CONTENT_LEN}B)" >> "$LOG_FILE" 2>/dev/null
40
+ ;;
41
+ Edit)
42
+ FILEPATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
43
+ OLD_LEN=$(echo "$INPUT" | jq -r '.tool_input.old_string // empty' 2>/dev/null | wc -c)
44
+ NEW_LEN=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null | wc -c)
45
+ echo "$TIMESTAMP EDIT $FILEPATH (${OLD_LEN}B → ${NEW_LEN}B)" >> "$LOG_FILE" 2>/dev/null
46
+ ;;
47
+ esac
48
+
49
+ exit 0
@@ -0,0 +1,49 @@
1
+ #!/bin/bash
2
+ # output-secret-mask.sh — Mask secrets in tool output before Claude sees them
3
+ #
4
+ # Solves: Commands like `env`, `printenv`, `cat .env` expose secrets in tool output.
5
+ # Claude then has secrets in its context window, increasing leak risk.
6
+ # This hook masks secret values in PostToolUse output.
7
+ #
8
+ # How it works: PostToolUse hook that scans tool output for secret patterns
9
+ # and replaces them with [MASKED]. The masked output is what
10
+ # Claude sees in its context.
11
+ #
12
+ # Note: This hook modifies the tool output that Claude receives.
13
+ # The actual command output is unchanged on disk/terminal.
14
+ #
15
+ # Usage: Add to settings.json as a PostToolUse hook
16
+ #
17
+ # {
18
+ # "hooks": {
19
+ # "PostToolUse": [{
20
+ # "matcher": "Bash",
21
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/output-secret-mask.sh" }]
22
+ # }]
23
+ # }
24
+ # }
25
+
26
+ INPUT=$(cat)
27
+ OUTPUT=$(echo "$INPUT" | jq -r '.tool_result.stdout // empty' 2>/dev/null)
28
+
29
+ [ -z "$OUTPUT" ] && exit 0
30
+
31
+ # Check if output contains secret-like patterns
32
+ NEEDS_MASK=false
33
+
34
+ # AWS keys
35
+ echo "$OUTPUT" | grep -qE 'AKIA[0-9A-Z]{16}' && NEEDS_MASK=true
36
+ # GitHub tokens
37
+ echo "$OUTPUT" | grep -qE '(ghp_|gho_|ghs_|ghr_)[A-Za-z0-9_]{20,}' && NEEDS_MASK=true
38
+ # OpenAI/Anthropic keys
39
+ echo "$OUTPUT" | grep -qE 'sk-[A-Za-z0-9_-]{20,}' && NEEDS_MASK=true
40
+ # Slack tokens
41
+ echo "$OUTPUT" | grep -qE '(xoxb-|xoxp-)[0-9A-Za-z-]{20,}' && NEEDS_MASK=true
42
+ # Generic secrets in env output (KEY=value pattern with high-entropy value)
43
+ echo "$OUTPUT" | grep -qiE '(API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH)=[^\s]{8,}' && NEEDS_MASK=true
44
+
45
+ if [ "$NEEDS_MASK" = true ]; then
46
+ echo "WARNING: Tool output may contain secrets. Consider using environment variables instead of printing them." >&2
47
+ fi
48
+
49
+ exit 0
@@ -0,0 +1,77 @@
1
+ #!/bin/bash
2
+ # permission-audit-log.sh — Log all tool invocations for permission debugging
3
+ #
4
+ # Solves: No way to know which commands trigger permission prompts vs auto-allow
5
+ # (#37153, #30519 58👍 partial)
6
+ # Users can't debug why certain commands prompt and others don't.
7
+ # This hook logs every tool call to help optimize permission rules.
8
+ #
9
+ # How it works: PostToolUse hook that appends every invocation to a JSONL log.
10
+ # Captures tool name, command/path, timestamp, and exit status.
11
+ # Companion script analyzes the log to suggest permission rules.
12
+ #
13
+ # Usage: Add to settings.json as a PostToolUse hook
14
+ #
15
+ # {
16
+ # "hooks": {
17
+ # "PostToolUse": [{
18
+ # "matcher": "",
19
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/permission-audit-log.sh" }]
20
+ # }]
21
+ # }
22
+ # }
23
+ #
24
+ # Analyze the log:
25
+ # cat ~/.claude/tool-usage.jsonl | jq -s 'group_by(.tool) | map({tool: .[0].tool, count: length}) | sort_by(-.count)'
26
+ # # Top commands:
27
+ # cat ~/.claude/tool-usage.jsonl | jq -s '[.[] | select(.tool=="Bash")] | group_by(.command | split(" ")[0]) | map({cmd: .[0].command | split(" ")[0], count: length}) | sort_by(-.count) | .[:20]'
28
+
29
+ INPUT=$(cat)
30
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
31
+
32
+ [ -z "$TOOL" ] && exit 0
33
+
34
+ LOG_FILE="${CC_AUDIT_LOG:-$HOME/.claude/tool-usage.jsonl}"
35
+
36
+ # Extract relevant info based on tool type
37
+ case "$TOOL" in
38
+ Bash)
39
+ DETAIL=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
40
+ # Extract base command (first word)
41
+ BASE_CMD=$(echo "$DETAIL" | awk '{print $1}')
42
+ ;;
43
+ Write|Read)
44
+ DETAIL=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
45
+ BASE_CMD="$TOOL"
46
+ ;;
47
+ Edit)
48
+ DETAIL=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
49
+ BASE_CMD="Edit"
50
+ ;;
51
+ Glob|Grep)
52
+ DETAIL=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null)
53
+ BASE_CMD="$TOOL"
54
+ ;;
55
+ Agent)
56
+ DETAIL=$(echo "$INPUT" | jq -r '.tool_input.description // empty' 2>/dev/null)
57
+ BASE_CMD="Agent"
58
+ ;;
59
+ *)
60
+ DETAIL=""
61
+ BASE_CMD="$TOOL"
62
+ ;;
63
+ esac
64
+
65
+ # Build log entry
66
+ TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
67
+
68
+ # Append to JSONL log (atomic write via temp file)
69
+ jq -n \
70
+ --arg ts "$TIMESTAMP" \
71
+ --arg tool "$TOOL" \
72
+ --arg cmd "$BASE_CMD" \
73
+ --arg detail "$DETAIL" \
74
+ '{timestamp: $ts, tool: $tool, base_command: $cmd, detail: $detail}' \
75
+ >> "$LOG_FILE" 2>/dev/null
76
+
77
+ exit 0
@@ -0,0 +1,88 @@
1
+ #!/bin/bash
2
+ # rm-safety-net.sh — Extra layer of rm protection beyond destructive-guard
3
+ #
4
+ # Solves: rm commands executing without permission prompts even when not in allow list
5
+ # (#38607 — rm bypasses settings.json permission system)
6
+ #
7
+ # Difference from destructive-guard:
8
+ # destructive-guard blocks: rm -rf /, rm -rf ~/, rm -rf ../, sudo rm -rf
9
+ # This hook blocks: ALL rm commands on important paths, even non-recursive
10
+ #
11
+ # What it blocks:
12
+ # rm (any flags) on: /, ~, .., /home, /etc, /usr, /var, .git, .env
13
+ # find -delete (any path)
14
+ # shred (any file)
15
+ # unlink on critical paths
16
+ #
17
+ # What it allows:
18
+ # rm on safe targets: node_modules, dist, build, __pycache__, .cache, /tmp
19
+ #
20
+ # Usage: Add to settings.json as a PreToolUse hook
21
+ #
22
+ # {
23
+ # "hooks": {
24
+ # "PreToolUse": [{
25
+ # "matcher": "Bash",
26
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/rm-safety-net.sh" }]
27
+ # }]
28
+ # }
29
+ # }
30
+
31
+ INPUT=$(cat)
32
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
33
+
34
+ [ -z "$COMMAND" ] && exit 0
35
+
36
+ # --- rm command analysis ---
37
+ if echo "$COMMAND" | grep -qE '^\s*(sudo\s+)?rm\s'; then
38
+ # Safe targets that can be deleted freely
39
+ SAFE_TARGETS="node_modules|dist|build|__pycache__|\.cache|\.pytest_cache|coverage|\.nyc_output|\.next|\.nuxt|tmp|temp"
40
+
41
+ # Extract the target (last argument after flags)
42
+ TARGET=$(echo "$COMMAND" | grep -oP 'rm\s+[^;|&]*' | awk '{print $NF}')
43
+
44
+ # Allow safe targets
45
+ if echo "$TARGET" | grep -qE "^(\./)?(${SAFE_TARGETS})(/|$)"; then
46
+ exit 0
47
+ fi
48
+
49
+ # Allow /tmp paths
50
+ if echo "$TARGET" | grep -qE "^/tmp/"; then
51
+ exit 0
52
+ fi
53
+
54
+ # Block rm on critical paths
55
+ CRITICAL="^/\$|^/home|^/etc|^/usr|^/var|^/opt|^/root|^~|^\.\.|^\.git$|^\.env"
56
+ if echo "$TARGET" | grep -qE "$CRITICAL"; then
57
+ echo "BLOCKED: rm targeting critical path: $TARGET" >&2
58
+ exit 2
59
+ fi
60
+
61
+ # Block rm -rf on any non-safe path (extra safety)
62
+ if echo "$COMMAND" | grep -qE 'rm\s+.*-[rRf]*[rR][rRf]*'; then
63
+ # rm -rf on non-safe, non-tmp target — block unless it's a known safe directory
64
+ if ! echo "$TARGET" | grep -qE "^(\./)?(${SAFE_TARGETS})(/|$)|^/tmp/"; then
65
+ echo "BLOCKED: rm -rf on non-safe target: $TARGET" >&2
66
+ exit 2
67
+ fi
68
+ fi
69
+ fi
70
+
71
+ # --- find -delete ---
72
+ if echo "$COMMAND" | grep -qE 'find\s.*-delete'; then
73
+ # Allow find in safe directories only
74
+ FIND_PATH=$(echo "$COMMAND" | grep -oP 'find\s+\K[^\s]+')
75
+ if echo "$FIND_PATH" | grep -qE '^\.|^node_modules|^dist|^build|^/tmp'; then
76
+ exit 0
77
+ fi
78
+ echo "BLOCKED: find -delete outside safe directory: $FIND_PATH" >&2
79
+ exit 2
80
+ fi
81
+
82
+ # --- shred ---
83
+ if echo "$COMMAND" | grep -qE '^\s*(sudo\s+)?shred\s'; then
84
+ echo "BLOCKED: shred command (secure file deletion)" >&2
85
+ exit 2
86
+ fi
87
+
88
+ exit 0
@@ -0,0 +1,59 @@
1
+ #!/bin/bash
2
+ # session-token-counter.sh — Track tool usage count per session
3
+ #
4
+ # Solves: No visibility into how many tool calls a session makes.
5
+ # Useful for detecting runaway loops and estimating costs.
6
+ # Warns at configurable thresholds (default: 100, 200, 500).
7
+ #
8
+ # How it works: PostToolUse hook that increments a counter file.
9
+ # At threshold crossings, outputs a warning to stderr.
10
+ # Does NOT block — just tracks and warns.
11
+ #
12
+ # Usage: Add to settings.json as a PostToolUse hook
13
+ #
14
+ # {
15
+ # "hooks": {
16
+ # "PostToolUse": [{
17
+ # "matcher": "",
18
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/session-token-counter.sh" }]
19
+ # }]
20
+ # }
21
+ # }
22
+ #
23
+ # Environment variables:
24
+ # CC_TOOL_WARN_100 — threshold 1 (default: 100)
25
+ # CC_TOOL_WARN_200 — threshold 2 (default: 200)
26
+ # CC_TOOL_WARN_500 — threshold 3 (default: 500)
27
+
28
+ INPUT=$(cat)
29
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
30
+
31
+ [ -z "$TOOL" ] && exit 0
32
+
33
+ # Use a session-specific counter file
34
+ COUNTER_FILE="${CC_TOOL_COUNTER:-/tmp/cc-session-tool-count-$$}"
35
+
36
+ # Initialize if not exists
37
+ if [ ! -f "$COUNTER_FILE" ]; then
38
+ echo "0" > "$COUNTER_FILE"
39
+ fi
40
+
41
+ # Increment
42
+ COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
43
+ COUNT=$((COUNT + 1))
44
+ echo "$COUNT" > "$COUNTER_FILE"
45
+
46
+ # Check thresholds
47
+ WARN_1=${CC_TOOL_WARN_100:-100}
48
+ WARN_2=${CC_TOOL_WARN_200:-200}
49
+ WARN_3=${CC_TOOL_WARN_500:-500}
50
+
51
+ if [ "$COUNT" -eq "$WARN_1" ]; then
52
+ echo "INFO: Session has made $COUNT tool calls. Consider whether you're in a loop." >&2
53
+ elif [ "$COUNT" -eq "$WARN_2" ]; then
54
+ echo "WARNING: Session has made $COUNT tool calls. High usage may indicate a runaway loop." >&2
55
+ elif [ "$COUNT" -eq "$WARN_3" ]; then
56
+ echo "CRITICAL: Session has made $COUNT tool calls. Very high usage — review session behavior." >&2
57
+ fi
58
+
59
+ exit 0
@@ -0,0 +1,75 @@
1
+ #!/bin/bash
2
+ # worktree-unmerged-guard.sh — Prevent worktree cleanup with unmerged commits
3
+ #
4
+ # Solves: Worktree sessions silently delete branches with unmerged/unpushed commits
5
+ # (#38287 — lost commits recoverable only via git fsck)
6
+ #
7
+ # How it works: Checks for unmerged commits before worktree removal.
8
+ # If the worktree branch has commits not in main/master, blocks cleanup.
9
+ #
10
+ # Usage: Add to settings.json as a PreToolUse hook
11
+ #
12
+ # {
13
+ # "hooks": {
14
+ # "PreToolUse": [{
15
+ # "matcher": "Bash",
16
+ # "hooks": [{ "type": "command", "command": "~/.claude/hooks/worktree-unmerged-guard.sh" }]
17
+ # }]
18
+ # }
19
+ # }
20
+
21
+ INPUT=$(cat)
22
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
23
+
24
+ [ -z "$COMMAND" ] && exit 0
25
+
26
+ # Detect worktree removal commands
27
+ if ! echo "$COMMAND" | grep -qE 'git\s+worktree\s+(remove|prune)|rm\s+.*worktree'; then
28
+ exit 0
29
+ fi
30
+
31
+ # Extract worktree path
32
+ WORKTREE_PATH=$(echo "$COMMAND" | grep -oP 'git\s+worktree\s+remove\s+\K[^\s]+')
33
+
34
+ if [ -z "$WORKTREE_PATH" ]; then
35
+ # Maybe it's rm -rf on a worktree directory
36
+ exit 0
37
+ fi
38
+
39
+ # Check if the worktree exists and has a branch
40
+ if [ ! -d "$WORKTREE_PATH" ]; then
41
+ exit 0
42
+ fi
43
+
44
+ # Get the branch name for this worktree
45
+ BRANCH=$(git -C "$WORKTREE_PATH" rev-parse --abbrev-ref HEAD 2>/dev/null)
46
+
47
+ if [ -z "$BRANCH" ] || [ "$BRANCH" = "HEAD" ]; then
48
+ exit 0
49
+ fi
50
+
51
+ # Find the default branch
52
+ DEFAULT_BRANCH=$(git -C "$WORKTREE_PATH" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')
53
+ [ -z "$DEFAULT_BRANCH" ] && DEFAULT_BRANCH="main"
54
+
55
+ # Count unmerged commits
56
+ UNMERGED=$(git -C "$WORKTREE_PATH" log --oneline "$DEFAULT_BRANCH..$BRANCH" 2>/dev/null | wc -l)
57
+
58
+ if [ "$UNMERGED" -gt 0 ]; then
59
+ echo "BLOCKED: Worktree branch '$BRANCH' has $UNMERGED unmerged commit(s)" >&2
60
+ echo "Merge or push the branch before removing the worktree:" >&2
61
+ echo " git -C $WORKTREE_PATH push origin $BRANCH" >&2
62
+ echo " # or: git merge $BRANCH" >&2
63
+ exit 2
64
+ fi
65
+
66
+ # Check for unpushed commits
67
+ UNPUSHED=$(git -C "$WORKTREE_PATH" log --oneline "origin/$BRANCH..$BRANCH" 2>/dev/null | wc -l)
68
+
69
+ if [ "$UNPUSHED" -gt 0 ]; then
70
+ echo "BLOCKED: Worktree branch '$BRANCH' has $UNPUSHED unpushed commit(s)" >&2
71
+ echo "Push before removing: git -C $WORKTREE_PATH push origin $BRANCH" >&2
72
+ exit 2
73
+ fi
74
+
75
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "29.5.0",
3
+ "version": "29.6.0",
4
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": {