cc-safe-setup 29.6.30 → 29.6.32

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,46 @@
1
+ #!/bin/bash
2
+ # max-concurrent-agents.sh — Limit number of simultaneous subagents
3
+ #
4
+ # Solves: Uncontrolled agent spawning burns through rate limits and tokens.
5
+ # A single prompt like "research 10 topics" can spawn 10 agents,
6
+ # each consuming context and API calls simultaneously.
7
+ #
8
+ # How it works: PreToolUse hook on "Agent" that tracks active agents
9
+ # via a counter file. Blocks new agents when the limit is reached.
10
+ # Counter is decremented by a companion PostToolUse hook or timeout.
11
+ #
12
+ # CONFIG:
13
+ # CC_MAX_AGENTS=3 (default: 3 concurrent agents)
14
+ #
15
+ # TRIGGER: PreToolUse
16
+ # MATCHER: "Agent"
17
+
18
+ INPUT=$(cat)
19
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
20
+ [ "$TOOL" != "Agent" ] && exit 0
21
+
22
+ MAX_AGENTS=${CC_MAX_AGENTS:-3}
23
+ COUNTER_FILE="/tmp/cc-agent-count-${PPID}"
24
+
25
+ # Initialize counter
26
+ [ -f "$COUNTER_FILE" ] || echo "0" > "$COUNTER_FILE"
27
+
28
+ # Read current count
29
+ CURRENT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
30
+
31
+ # Clean up stale counts (reset if file is older than 10 minutes)
32
+ if [ -f "$COUNTER_FILE" ]; then
33
+ AGE=$(( $(date +%s) - $(stat -c %Y "$COUNTER_FILE" 2>/dev/null || echo 0) ))
34
+ [ "$AGE" -gt 600 ] && echo "0" > "$COUNTER_FILE" && CURRENT=0
35
+ fi
36
+
37
+ if [ "$CURRENT" -ge "$MAX_AGENTS" ]; then
38
+ echo "BLOCKED: Maximum concurrent agents reached (${CURRENT}/${MAX_AGENTS})" >&2
39
+ echo " Wait for existing agents to complete before spawning new ones." >&2
40
+ echo " Set CC_MAX_AGENTS to increase the limit." >&2
41
+ exit 2
42
+ fi
43
+
44
+ # Increment counter
45
+ echo $((CURRENT + 1)) > "$COUNTER_FILE"
46
+ exit 0
@@ -0,0 +1,48 @@
1
+ #!/bin/bash
2
+ # mcp-config-freeze.sh — Prevent MCP configuration changes during session
3
+ #
4
+ # Solves: Shadow MCP servers added mid-session (OWASP MCP09).
5
+ # An agent or prompt injection could modify .mcp.json or
6
+ # settings.json to add unauthorized MCP servers.
7
+ #
8
+ # How it works: On SessionStart, snapshots the current MCP config.
9
+ # On subsequent Edit/Write to config files, compares against snapshot.
10
+ # Blocks changes that add new MCP servers.
11
+ #
12
+ # Complements mcp-server-guard.sh (which blocks Bash-based server launches)
13
+ # by also covering config file modification.
14
+ #
15
+ # TRIGGER: PreToolUse
16
+ # MATCHER: "Edit|Write"
17
+
18
+ INPUT=$(cat)
19
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
20
+ [ -z "$FILE" ] && exit 0
21
+
22
+ # Only check MCP-related config files
23
+ FILENAME=$(basename "$FILE")
24
+ case "$FILENAME" in
25
+ .mcp.json|mcp.json|mcp-config.json)
26
+ ;;
27
+ settings.json|settings.local.json)
28
+ # Check if the edit adds mcpServers
29
+ CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
30
+ if echo "$CONTENT" | grep -qiE 'mcpServers|mcp_servers'; then
31
+ echo "BLOCKED: MCP server configuration change detected" >&2
32
+ echo " File: $FILE" >&2
33
+ echo " MCP server additions require manual approval." >&2
34
+ echo " Edit the file manually or remove this hook temporarily." >&2
35
+ exit 2
36
+ fi
37
+ exit 0
38
+ ;;
39
+ *)
40
+ exit 0
41
+ ;;
42
+ esac
43
+
44
+ # For .mcp.json files, block all modifications
45
+ echo "BLOCKED: MCP configuration file is frozen during this session" >&2
46
+ echo " File: $FILE" >&2
47
+ echo " To modify MCP config, edit the file manually outside Claude Code." >&2
48
+ exit 2
@@ -0,0 +1,43 @@
1
+ #!/bin/bash
2
+ # mcp-data-boundary.sh — Prevent MCP tools from accessing sensitive paths
3
+ #
4
+ # Solves: MCP tools can read/write files outside the intended scope.
5
+ # A rogue or misconfigured MCP server could exfiltrate credentials
6
+ # or modify system files. (OWASP MCP01 + MCP10)
7
+ #
8
+ # How it works: PostToolUse hook that checks MCP tool results for
9
+ # sensitive file path references. Warns if an MCP tool accessed
10
+ # paths outside the project directory.
11
+ #
12
+ # CONFIG:
13
+ # CC_MCP_ALLOWED_PATHS="/home/user/project" (colon-separated)
14
+ #
15
+ # TRIGGER: PostToolUse
16
+ # MATCHER: "" (monitors all tools, focuses on MCP tool outputs)
17
+
18
+ INPUT=$(cat)
19
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
20
+ OUTPUT=$(echo "$INPUT" | jq -r '.tool_output // empty' 2>/dev/null | head -c 2000)
21
+ [ -z "$OUTPUT" ] && exit 0
22
+
23
+ # Only check MCP tool outputs (tool names starting with mcp__)
24
+ echo "$TOOL" | grep -q '^mcp__' || exit 0
25
+
26
+ # Check for sensitive path patterns in output
27
+ SENSITIVE_PATHS='/etc/passwd|/etc/shadow|\.ssh/|\.aws/|\.env|credentials|\.npmrc|\.pypirc|\.netrc|\.gnupg|\.kube/config'
28
+
29
+ if echo "$OUTPUT" | grep -qiE "$SENSITIVE_PATHS"; then
30
+ echo "⚠ MCP DATA BOUNDARY: MCP tool accessed sensitive path" >&2
31
+ echo " Tool: $TOOL" >&2
32
+ echo " Detected sensitive path reference in output." >&2
33
+ echo " Review the MCP server's file access scope." >&2
34
+ fi
35
+
36
+ # Check for data that looks like secrets in output
37
+ if echo "$OUTPUT" | grep -qE 'sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{30,}|-----BEGIN.*KEY|AKIA[A-Z0-9]{16}'; then
38
+ echo "⚠ MCP DATA BOUNDARY: MCP tool output contains potential secrets" >&2
39
+ echo " Tool: $TOOL" >&2
40
+ echo " Review output for leaked credentials." >&2
41
+ fi
42
+
43
+ exit 0
@@ -0,0 +1,27 @@
1
+ #!/bin/bash
2
+ # no-git-amend.sh — Block git commit --amend to prevent overwriting previous commits
3
+ #
4
+ # Solves: Claude Code amending previous commits instead of creating new ones.
5
+ # When a pre-commit hook fails, the commit doesn't happen. If Claude
6
+ # then runs --amend to "fix" it, it modifies the PREVIOUS commit
7
+ # instead of creating a new one — potentially destroying prior work.
8
+ #
9
+ # This is explicitly recommended in Claude Code's own system prompt:
10
+ # "Always create NEW commits rather than amending"
11
+ #
12
+ # TRIGGER: PreToolUse
13
+ # MATCHER: "Bash"
14
+
15
+ INPUT=$(cat)
16
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
17
+ [ -z "$COMMAND" ] && exit 0
18
+
19
+ # Block git commit --amend
20
+ if echo "$COMMAND" | grep -qE 'git\s+commit\s+.*--amend|git\s+commit\s+--amend'; then
21
+ echo "BLOCKED: git commit --amend is not allowed" >&2
22
+ echo " Create a new commit instead: git commit -m 'fix: ...'" >&2
23
+ echo " Amending can overwrite the previous commit's changes." >&2
24
+ exit 2
25
+ fi
26
+
27
+ exit 0
@@ -0,0 +1,66 @@
1
+ #!/bin/bash
2
+ # path-deny-bash-guard.sh — Enforce path deny rules on Bash commands
3
+ #
4
+ # Solves: Bash tool bypasses settings.json path deny rules (#39987).
5
+ # Read/Glob/Grep respect deny rules, but Bash commands like
6
+ # `cat /denied/path/file.txt` or `grep pattern /denied/path/`
7
+ # bypass the restriction entirely.
8
+ #
9
+ # How it works: Reads denied paths from CC_DENIED_PATHS env var or
10
+ # a config file, then checks if any Bash command argument contains
11
+ # a denied path.
12
+ #
13
+ # CONFIG:
14
+ # CC_DENIED_PATHS="/path/one:/path/two:/path/three"
15
+ # Or create ~/.claude/denied-paths.txt (one path per line)
16
+ #
17
+ # TRIGGER: PreToolUse
18
+ # MATCHER: "Bash"
19
+
20
+ INPUT=$(cat)
21
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
22
+ [ -z "$COMMAND" ] && exit 0
23
+
24
+ # Load denied paths
25
+ DENIED_PATHS=""
26
+
27
+ # Source 1: Environment variable (colon-separated)
28
+ if [ -n "${CC_DENIED_PATHS:-}" ]; then
29
+ DENIED_PATHS="$CC_DENIED_PATHS"
30
+ fi
31
+
32
+ # Source 2: Config file (one path per line)
33
+ DENY_FILE="${HOME}/.claude/denied-paths.txt"
34
+ if [ -f "$DENY_FILE" ]; then
35
+ while IFS= read -r line; do
36
+ [ -z "$line" ] && continue
37
+ [[ "$line" =~ ^# ]] && continue
38
+ if [ -n "$DENIED_PATHS" ]; then
39
+ DENIED_PATHS="${DENIED_PATHS}:${line}"
40
+ else
41
+ DENIED_PATHS="$line"
42
+ fi
43
+ done < "$DENY_FILE"
44
+ fi
45
+
46
+ [ -z "$DENIED_PATHS" ] && exit 0
47
+
48
+ # Check command against denied paths
49
+ IFS=':'
50
+ for denied_path in $DENIED_PATHS; do
51
+ [ -z "$denied_path" ] && continue
52
+ # Normalize: remove trailing slash
53
+ denied_path="${denied_path%/}"
54
+
55
+ if echo "$COMMAND" | grep -qF "$denied_path"; then
56
+ echo "BLOCKED: Bash command accesses denied path" >&2
57
+ echo " Denied: $denied_path" >&2
58
+ echo " Command: $(echo "$COMMAND" | head -c 100)" >&2
59
+ echo " Note: This path is restricted in your deny rules." >&2
60
+ echo " Use Read/Glob/Grep tools which respect deny rules," >&2
61
+ echo " or remove the path from denied-paths.txt." >&2
62
+ exit 2
63
+ fi
64
+ done
65
+
66
+ exit 0
@@ -0,0 +1,47 @@
1
+ #!/bin/bash
2
+ # permission-mode-drift-guard.sh — Detect permission mode changes mid-session
3
+ #
4
+ # Solves: Permission mode resets from 'Bypass permissions' to 'Edit automatically'
5
+ # mid-session without user interaction (#39057, 3 reactions).
6
+ #
7
+ # How it works: On SessionStart, records the initial permission mode.
8
+ # On each PermissionRequest, compares current behavior against expected.
9
+ # If permissions are being requested when bypass mode was set,
10
+ # warns the user that the mode may have drifted.
11
+ #
12
+ # Uses ConfigChange hook event (v2.1.83+) when available, falls back
13
+ # to heuristic detection via unexpected permission prompts.
14
+ #
15
+ # TRIGGER: PermissionRequest (fallback detection)
16
+ # MATCHER: "" (all permission requests)
17
+
18
+ INPUT=$(cat)
19
+ MESSAGE=$(echo "$INPUT" | jq -r '.message // empty' 2>/dev/null)
20
+
21
+ STATE_FILE="/tmp/cc-permission-mode-${PPID}"
22
+
23
+ # On first call, record that we're getting permission prompts
24
+ if [ ! -f "$STATE_FILE" ]; then
25
+ echo "prompt_count=1" > "$STATE_FILE"
26
+ echo "first_prompt=$(date +%s)" >> "$STATE_FILE"
27
+ exit 0
28
+ fi
29
+
30
+ # Track prompt count
31
+ . "$STATE_FILE"
32
+ prompt_count=$((prompt_count + 1))
33
+ echo "prompt_count=$prompt_count" > "$STATE_FILE"
34
+ echo "first_prompt=$first_prompt" >> "$STATE_FILE"
35
+
36
+ # If we're getting many permission prompts, something may be wrong
37
+ if [ "$prompt_count" -eq 5 ]; then
38
+ echo "⚠ Permission mode drift detected: ${prompt_count} permission prompts this session" >&2
39
+ echo " If you set 'Bypass permissions', it may have reset to 'Edit automatically'" >&2
40
+ echo " Check: Ctrl+Shift+P → Claude Code: Set Permission Mode" >&2
41
+ fi
42
+
43
+ if [ "$prompt_count" -eq 20 ]; then
44
+ echo "⚠ ${prompt_count} permission prompts — consider re-enabling bypass mode" >&2
45
+ fi
46
+
47
+ exit 0
@@ -0,0 +1,75 @@
1
+ #!/bin/bash
2
+ # plan-mode-strict-guard.sh — Hard-block all write operations during plan mode
3
+ #
4
+ # Solves: Plan mode doesn't hard-block write tools (#40324, 0 reactions).
5
+ # When Claude is in plan mode, it should only read and analyze.
6
+ # But the model can propose Edit/Write operations, and if the user
7
+ # clicks "approve", they execute — defeating the purpose of plan mode.
8
+ #
9
+ # How it works: Checks for plan mode indicators:
10
+ # 1. CC_PLAN_MODE env var (set by some configurations)
11
+ # 2. .claude/plan-mode.lock file (created by plan-mode-enforcer.sh)
12
+ # 3. Plan-related keywords in the session context
13
+ #
14
+ # When plan mode is active, blocks Edit, Write, and dangerous Bash commands.
15
+ # Read, Glob, Grep, and safe Bash commands are allowed.
16
+ #
17
+ # TRIGGER: PreToolUse
18
+ # MATCHER: "Edit|Write|Bash"
19
+
20
+ INPUT=$(cat)
21
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
22
+ [ -z "$TOOL" ] && exit 0
23
+
24
+ # Check if plan mode is active
25
+ PLAN_MODE=false
26
+
27
+ # Method 1: Environment variable
28
+ [ "${CC_PLAN_MODE:-}" = "true" ] && PLAN_MODE=true
29
+
30
+ # Method 2: Lock file
31
+ [ -f "${HOME}/.claude/plan-mode.lock" ] && PLAN_MODE=true
32
+
33
+ # Method 3: Project-level plan lock
34
+ [ -f ".claude/plan-mode.lock" ] && PLAN_MODE=true
35
+
36
+ [ "$PLAN_MODE" = "false" ] && exit 0
37
+
38
+ # Plan mode is active — enforce read-only
39
+ case "$TOOL" in
40
+ Edit|Write)
41
+ echo "BLOCKED: Plan mode is active — write operations are not allowed" >&2
42
+ echo " Exit plan mode first, then make changes" >&2
43
+ echo " Remove ~/.claude/plan-mode.lock or unset CC_PLAN_MODE" >&2
44
+ exit 2
45
+ ;;
46
+ Bash)
47
+ CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
48
+ [ -z "$CMD" ] && exit 0
49
+
50
+ # Allow read-only commands in plan mode
51
+ # Strip compound operators to get the first command
52
+ FIRST_PART=$(echo "$CMD" | sed 's/[;&|].*//' | sed 's/^\s*//')
53
+
54
+ # Single-word safe commands
55
+ FIRST_WORD=$(echo "$FIRST_PART" | awk '{print $1}')
56
+ SAFE_SINGLE="ls|cat|head|tail|grep|find|wc|diff|pwd|echo|date|which|type|file|tree|du|df|env|printenv"
57
+ if echo "$FIRST_WORD" | grep -qxE "$SAFE_SINGLE"; then
58
+ exit 0
59
+ fi
60
+
61
+ # Two-word safe commands (git, npm, etc.)
62
+ FIRST_TWO=$(echo "$FIRST_PART" | awk '{print $1, $2}')
63
+ SAFE_TWO="git status|git log|git diff|git branch|git show|git rev-parse|git tag|node -v|python3 -V|npm list|npm outdated|pip list|pip show"
64
+ if echo "$FIRST_TWO" | grep -qxE "$SAFE_TWO"; then
65
+ exit 0
66
+ fi
67
+
68
+ # Block write commands in plan mode
69
+ echo "BLOCKED: Plan mode is active — only read-only Bash commands allowed" >&2
70
+ echo " Allowed: ls, cat, grep, git status/log/diff, etc." >&2
71
+ exit 2
72
+ ;;
73
+ esac
74
+
75
+ exit 0
@@ -0,0 +1,39 @@
1
+ #!/bin/bash
2
+ # pre-compact-checkpoint.sh — Auto-save before context compaction
3
+ #
4
+ # Uses the PreCompact hook event to create a git checkpoint before
5
+ # Claude Code compresses the conversation context. This ensures
6
+ # uncommitted edits are preserved even if compaction loses track
7
+ # of recent changes.
8
+ #
9
+ # Solves: Context compaction can cause Claude to lose awareness of
10
+ # recent file edits (#34674). A pre-compaction checkpoint
11
+ # makes recovery trivial: just run `git log --oneline -5`.
12
+ #
13
+ # TRIGGER: PreCompact (fires right before context compression)
14
+ # MATCHER: No matcher support — always fires
15
+ #
16
+ # DECISION CONTROL: None (notification only)
17
+ #
18
+ # Compared to auto-compact-prep.sh (which uses tool call counting
19
+ # on PreToolUse), this hook fires at the exact right moment —
20
+ # when compaction actually happens, not on an estimated threshold.
21
+
22
+ # Check if we're in a git repo
23
+ git rev-parse --is-inside-work-tree &>/dev/null || exit 0
24
+
25
+ # Check for uncommitted changes
26
+ CHANGES=$(git status --porcelain 2>/dev/null | wc -l)
27
+ [ "$CHANGES" -eq 0 ] && exit 0
28
+
29
+ # Create checkpoint commit
30
+ BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
31
+ TIMESTAMP=$(date -u '+%Y%m%d-%H%M%S')
32
+
33
+ git add -A 2>/dev/null
34
+ git commit -m "checkpoint: pre-compact auto-save (${CHANGES} files, ${TIMESTAMP})" --no-verify 2>/dev/null
35
+
36
+ echo "📸 Pre-compact checkpoint: ${CHANGES} file(s) saved on ${BRANCH}" >&2
37
+ echo " Recover with: git log --oneline -5" >&2
38
+
39
+ exit 0
@@ -0,0 +1,51 @@
1
+ #!/bin/bash
2
+ # sandbox-write-verify.sh — Verify file existence before overwrite in sandbox mode
3
+ #
4
+ # Solves: Sandbox half-broken — writes hit real filesystem (#40321).
5
+ # When sandbox reads are isolated but writes pass through,
6
+ # Claude overwrites real files it can't see, destroying projects.
7
+ #
8
+ # How it works: Before Edit/Write operations, checks if the target file
9
+ # exists on the REAL filesystem (not sandboxed). If Claude is about to
10
+ # overwrite an existing file and can't read it (sandbox read isolation),
11
+ # blocks the write.
12
+ #
13
+ # Also detects bulk writes (>10 files in quick succession) which is
14
+ # a sign of runaway overwrite behavior.
15
+ #
16
+ # TRIGGER: PreToolUse
17
+ # MATCHER: "Edit|Write"
18
+
19
+ INPUT=$(cat)
20
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
21
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
22
+ [ -z "$FILE" ] && exit 0
23
+
24
+ # Track writes per session to detect bulk overwrite
25
+ WRITE_LOG="/tmp/cc-sandbox-writes-${PPID}"
26
+ echo "$(date +%s) $FILE" >> "$WRITE_LOG" 2>/dev/null
27
+
28
+ # Check for bulk writes (>10 in last 60 seconds)
29
+ if [ -f "$WRITE_LOG" ]; then
30
+ NOW=$(date +%s)
31
+ RECENT=$(awk -v now="$NOW" '$1 > now - 60 {count++} END {print count+0}' "$WRITE_LOG")
32
+ if [ "$RECENT" -gt 10 ]; then
33
+ echo "BLOCKED: Bulk write detected (${RECENT} files in 60s)" >&2
34
+ echo " This may indicate a sandbox read/write mismatch." >&2
35
+ echo " Verify sandbox state before continuing." >&2
36
+ exit 2
37
+ fi
38
+ fi
39
+
40
+ # Check if target file exists and is non-empty (potential overwrite)
41
+ if [ -f "$FILE" ] && [ -s "$FILE" ]; then
42
+ # File exists. Check if it's in a project directory
43
+ DIR=$(dirname "$FILE")
44
+ # Count files in the same directory that were recently written
45
+ DIR_WRITES=$(grep -c "$DIR" "$WRITE_LOG" 2>/dev/null || echo 0)
46
+ if [ "$DIR_WRITES" -gt 5 ]; then
47
+ echo "WARNING: ${DIR_WRITES} writes to $(basename "$DIR")/ — verify sandbox state" >&2
48
+ fi
49
+ fi
50
+
51
+ exit 0
@@ -0,0 +1,70 @@
1
+ #!/bin/bash
2
+ # session-resume-guard.sh — Verify context is loaded after session resume
3
+ #
4
+ # Solves: Session resume loads zero conversation history (#40319).
5
+ # When --continue resumes a long session, cache_read_input_tokens
6
+ # can drop from 434k to 0, silently losing all context.
7
+ #
8
+ # How it works: On SessionStart, checks if this is a resumed session
9
+ # (via CC_RESUME or --continue flag indicators). If so, verifies that
10
+ # key context files exist and warns if they might be stale.
11
+ #
12
+ # Also saves a "session handoff" file on Stop, so the next session
13
+ # can detect if context was properly transferred.
14
+ #
15
+ # TRIGGER: Notification (SessionStart)
16
+ # MATCHER: "" (fires on all notifications, filters for SessionStart internally)
17
+
18
+ INPUT=$(cat)
19
+ EVENT=$(echo "$INPUT" | jq -r '.event // empty' 2>/dev/null)
20
+
21
+ HANDOFF_DIR="${HOME}/.claude/handoff"
22
+ mkdir -p "$HANDOFF_DIR"
23
+ HANDOFF_FILE="${HANDOFF_DIR}/last-session.md"
24
+
25
+ case "$EVENT" in
26
+ session_start|SessionStart)
27
+ # Check if this is a resume (handoff file exists and is recent)
28
+ if [ -f "$HANDOFF_FILE" ]; then
29
+ AGE_SECONDS=$(( $(date +%s) - $(stat -c %Y "$HANDOFF_FILE" 2>/dev/null || echo 0) ))
30
+
31
+ if [ "$AGE_SECONDS" -lt 3600 ]; then
32
+ echo "📋 Resuming from previous session (handoff ${AGE_SECONDS}s ago)" >&2
33
+ echo " Last session state:" >&2
34
+ head -5 "$HANDOFF_FILE" >&2
35
+ else
36
+ AGE_HOURS=$((AGE_SECONDS / 3600))
37
+ echo "⚠ Previous session handoff is ${AGE_HOURS}h old" >&2
38
+ echo " Context may be stale. Consider starting fresh." >&2
39
+ fi
40
+ fi
41
+
42
+ # Check for recovery snapshots (from compaction-transcript-guard)
43
+ RECOVERY_DIR="${HOME}/.claude/recovery"
44
+ if [ -d "$RECOVERY_DIR" ]; then
45
+ LATEST=$(ls -t "$RECOVERY_DIR"/pre-compact-*.md 2>/dev/null | head -1)
46
+ if [ -n "$LATEST" ]; then
47
+ AGE=$(( $(date +%s) - $(stat -c %Y "$LATEST" 2>/dev/null || echo 0) ))
48
+ if [ "$AGE" -lt 7200 ]; then
49
+ echo "📸 Recent compaction recovery snapshot found ($(( AGE / 60 ))m ago)" >&2
50
+ echo " Path: $LATEST" >&2
51
+ fi
52
+ fi
53
+ fi
54
+ ;;
55
+
56
+ session_end|Stop)
57
+ # Save handoff for next session
58
+ cat > "$HANDOFF_FILE" << EOF
59
+ # Session Handoff
60
+ Timestamp: $(date -u '+%Y-%m-%dT%H:%M:%SZ')
61
+ Working directory: $(pwd)
62
+ Branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'N/A')
63
+ Uncommitted: $(git status --porcelain 2>/dev/null | wc -l) files
64
+ Last commit: $(git log --oneline -1 2>/dev/null || echo 'N/A')
65
+ EOF
66
+ echo "📋 Session handoff saved for next resume" >&2
67
+ ;;
68
+ esac
69
+
70
+ exit 0
@@ -0,0 +1,49 @@
1
+ #!/bin/bash
2
+ # subagent-scope-validator.sh — Validate subagent task scope before launch
3
+ #
4
+ # Solves: Main agent's subagent delegation produces poor scoping (#40339).
5
+ # Subagents are launched with vague prompts, missing context,
6
+ # and no result verification criteria.
7
+ #
8
+ # How it works: PreToolUse hook on "Agent" that checks the prompt
9
+ # for minimum scope requirements:
10
+ # 1. Prompt must be longer than 50 characters (not just "do X")
11
+ # 2. Must contain file paths or specific identifiers
12
+ # 3. Warns if no success criteria are mentioned
13
+ #
14
+ # TRIGGER: PreToolUse
15
+ # MATCHER: "Agent"
16
+
17
+ INPUT=$(cat)
18
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
19
+ [ "$TOOL" != "Agent" ] && exit 0
20
+
21
+ PROMPT=$(echo "$INPUT" | jq -r '.tool_input.prompt // empty' 2>/dev/null)
22
+ [ -z "$PROMPT" ] && exit 0
23
+
24
+ PROMPT_LEN=${#PROMPT}
25
+ WARNINGS=""
26
+
27
+ # Check 1: Minimum prompt length
28
+ if [ "$PROMPT_LEN" -lt 50 ]; then
29
+ WARNINGS="${WARNINGS}\n - Prompt is only ${PROMPT_LEN} chars. Subagents need detailed context (50+ chars recommended)"
30
+ fi
31
+
32
+ # Check 2: Contains specific identifiers (files, functions, paths)
33
+ if ! echo "$PROMPT" | grep -qE '/[a-zA-Z]|\.ts|\.py|\.js|\.md|\.json|\.sh|function |class |def |const |let |var '; then
34
+ WARNINGS="${WARNINGS}\n - No file paths or code identifiers found. Subagent may lack context"
35
+ fi
36
+
37
+ # Check 3: Success criteria
38
+ if ! echo "$PROMPT" | grep -qiE 'verify|confirm|test|check|ensure|must|should|expect|return|report'; then
39
+ WARNINGS="${WARNINGS}\n - No success criteria detected. Consider adding verification steps"
40
+ fi
41
+
42
+ # Output warnings (don't block — just inform)
43
+ if [ -n "$WARNINGS" ]; then
44
+ echo "⚠ Subagent scope review:" >&2
45
+ echo -e "$WARNINGS" >&2
46
+ echo " Prompt preview: $(echo "$PROMPT" | head -c 100)..." >&2
47
+ fi
48
+
49
+ exit 0
@@ -0,0 +1,45 @@
1
+ #!/bin/bash
2
+ # windows-path-guard.sh — Prevent NTFS junction/symlink traversal destruction
3
+ #
4
+ # Solves: rm -rf following NTFS junctions to delete user directories (#36339).
5
+ # On Windows (WSL/Git Bash), `rm -rf` can traverse NTFS junctions
6
+ # and delete system directories like C:\Users.
7
+ #
8
+ # How it works: Before rm operations, checks if the target path is
9
+ # a symlink or junction that points outside the project directory.
10
+ # Blocks rm if it would traverse to a system-critical location.
11
+ #
12
+ # TRIGGER: PreToolUse
13
+ # MATCHER: "Bash"
14
+
15
+ INPUT=$(cat)
16
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
17
+ [ -z "$COMMAND" ] && exit 0
18
+
19
+ # Only check rm commands
20
+ echo "$COMMAND" | grep -qE '^\s*rm\s|;\s*rm\s|&&\s*rm\s' || exit 0
21
+
22
+ # Check the entire command for Windows system paths
23
+ # This catches both direct paths and quoted paths with spaces
24
+ WINDOWS_SYSTEM='/mnt/[a-z]/(Users|Windows|Program Files|Program)'
25
+ if echo "$COMMAND" | grep -qiE "$WINDOWS_SYSTEM"; then
26
+ echo "BLOCKED: rm targets Windows system directory" >&2
27
+ echo " Command contains a Windows system path reference." >&2
28
+ echo " This could destroy system files via NTFS junction traversal." >&2
29
+ echo " Reference: GitHub Issue #36339" >&2
30
+ exit 2
31
+ fi
32
+
33
+ # Check if any rm target is a symlink pointing to system directories
34
+ for target in $(echo "$COMMAND" | grep -oE '/[^ ";\|&]+' | head -10); do
35
+ [ -L "$target" ] || [ -L "$(dirname "$target" 2>/dev/null)" ] || continue
36
+ LINK_TARGET=$(readlink -f "$target" 2>/dev/null)
37
+ [ -z "$LINK_TARGET" ] && continue
38
+ if echo "$LINK_TARGET" | grep -qiE "^/(mnt/[a-z]/(Users|Windows|Program)|home$|etc$|usr$|var$)"; then
39
+ echo "BLOCKED: rm would traverse symlink/junction to system directory" >&2
40
+ echo " Target: $target → $LINK_TARGET" >&2
41
+ exit 2
42
+ fi
43
+ done
44
+
45
+ exit 0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "29.6.30",
4
- "description": "One command to make Claude Code safe. 516 example hooks + 8 built-in. 56 CLI commands. 7582 tests. Works with Auto Mode.",
3
+ "version": "29.6.32",
4
+ "description": "One command to make Claude Code safe. 539 example hooks + 8 built-in. 56 CLI commands. 7789 tests. Works with Auto Mode.",
5
5
  "main": "index.mjs",
6
6
  "bin": {
7
7
  "cc-safe-setup": "index.mjs"
package/test.sh.tmp ADDED
@@ -0,0 +1,12 @@
1
+ echo "path-deny-bash-guard.sh:"
2
+ test_ex path-deny-bash-guard.sh '{"tool_input":{"command":"cat /etc/passwd"}}' 0 "path-deny: no deny config"
3
+ test_ex path-deny-bash-guard.sh '{}' 0 "path-deny: empty input"
4
+ export CC_DENIED_PATHS="/secret/data:/private/keys"
5
+ test_ex path-deny-bash-guard.sh '{"tool_input":{"command":"cat /secret/data/file.txt"}}' 2 "path-deny: cat denied path BLOCKED"
6
+ test_ex path-deny-bash-guard.sh '{"tool_input":{"command":"grep pattern /secret/data/"}}' 2 "path-deny: grep denied path BLOCKED"
7
+ test_ex path-deny-bash-guard.sh '{"tool_input":{"command":"head /private/keys/id_rsa"}}' 2 "path-deny: head denied path BLOCKED"
8
+ test_ex path-deny-bash-guard.sh '{"tool_input":{"command":"ls /home/user/projects"}}' 0 "path-deny: safe path allowed"
9
+ test_ex path-deny-bash-guard.sh '{"tool_input":{"command":"echo hello"}}' 0 "path-deny: no path in command"
10
+ test_ex path-deny-bash-guard.sh '{"tool_input":{"command":"cat /secret/data/../../../etc/passwd"}}' 2 "path-deny: traversal still matches denied prefix"
11
+ unset CC_DENIED_PATHS
12
+ echo ""