cc-safe-setup 29.6.33 → 29.6.37
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 +1 -1
- package/examples/claudemd-violation-detector.sh +36 -0
- package/examples/clear-command-confirm-guard.sh +21 -0
- package/examples/core-file-protect-guard.sh +91 -0
- package/examples/cwd-drift-detector.sh +47 -0
- package/examples/deployment-verify-guard.sh +81 -0
- package/examples/edit-old-string-validator.sh +37 -0
- package/examples/encoding-preserve-guard.sh +34 -0
- package/examples/git-crypt-worktree-guard.sh +36 -0
- package/examples/git-operations-require-approval.sh +99 -0
- package/examples/line-ending-guard.sh +30 -0
- package/examples/permission-pattern-auto-allow.sh +50 -0
- package/examples/read-audit-log.sh +34 -0
- package/examples/session-duration-guard.sh +51 -0
- package/examples/settings-auto-backup.sh +53 -0
- package/examples/settings-mutation-detector.sh +45 -0
- package/examples/subagent-context-size-guard.sh +26 -0
- package/examples/symlink-protect.sh +12 -0
- package/examples/temp-file-cleanup-stop.sh +28 -0
- package/examples/test-before-commit.sh +13 -16
- package/examples/token-spike-alert.sh +51 -0
- package/examples/virtual-cwd-helper.sh +40 -0
- package/examples/worktree-delete-guard.sh +43 -0
- package/examples/worktree-path-validator.sh +42 -0
- package/examples/write-shrink-guard.sh +46 -0
- package/index.mjs +631 -138
- package/package.json +2 -2
- package/scripts/generate-categories.mjs +206 -0
- package/scripts.json +4 -1
- package/test.sh.new_tests +0 -0
- package/test.sh.patch +0 -0
- package/tests/test-core-file-protect-guard.sh +73 -0
- package/tests/test-deployment-verify-guard.sh +74 -0
- package/tests/test-git-operations-require-approval.sh +65 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/cc-safe-setup)
|
|
5
5
|
[](https://github.com/yurukusa/cc-safe-setup/actions/workflows/test.yml)
|
|
6
6
|
|
|
7
|
-
**One command to make Claude Code safe for autonomous operation.**
|
|
7
|
+
**One command to make Claude Code safe for autonomous operation.** 634 example hooks · 13,835 tests · 1,000+ installs/day · [日本語](docs/README.ja.md)
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npx cc-safe-setup
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# claudemd-violation-detector.sh — Remind critical CLAUDE.md rules after tool use
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude ignores CLAUDE.md instructions, especially after
|
|
5
|
+
# context compaction or in long sessions (#40930).
|
|
6
|
+
#
|
|
7
|
+
# How it works: After each tool use, extracts and prints
|
|
8
|
+
# critical rules (ABSOLUTE/MUST NEVER/NEVER/禁止) from CLAUDE.md
|
|
9
|
+
# as a reminder. Runs every N tool calls to avoid noise.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PostToolUse
|
|
12
|
+
# MATCHER: ""
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
# Rate limit: only remind every 20 tool calls
|
|
17
|
+
COUNTER_FILE="/tmp/claudemd-reminder-counter"
|
|
18
|
+
COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
|
|
19
|
+
COUNT=$((COUNT + 1))
|
|
20
|
+
echo "$COUNT" > "$COUNTER_FILE"
|
|
21
|
+
[ $((COUNT % 20)) -ne 0 ] && exit 0
|
|
22
|
+
|
|
23
|
+
# Find CLAUDE.md
|
|
24
|
+
CLAUDEMD=""
|
|
25
|
+
for candidate in "CLAUDE.md" ".claude/CLAUDE.md" "../CLAUDE.md"; do
|
|
26
|
+
[ -f "$candidate" ] && CLAUDEMD="$candidate" && break
|
|
27
|
+
done
|
|
28
|
+
[ -z "$CLAUDEMD" ] && exit 0
|
|
29
|
+
|
|
30
|
+
# Extract critical rules
|
|
31
|
+
RULES=$(grep -iE '(ABSOLUTE|MUST NEVER|NEVER DO|禁止|絶対)' "$CLAUDEMD" 2>/dev/null | head -5 || true)
|
|
32
|
+
[ -z "$RULES" ] && exit 0
|
|
33
|
+
|
|
34
|
+
echo "📋 CLAUDE.md critical rules reminder:" >&2
|
|
35
|
+
echo "$RULES" >&2
|
|
36
|
+
exit 0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# clear-command-confirm-guard.sh — Block accidental /clear command
|
|
3
|
+
#
|
|
4
|
+
# Solves: /clear destroys all conversation context with zero
|
|
5
|
+
# confirmation. Prefix matching means /c + Enter can
|
|
6
|
+
# accidentally trigger /clear instead of /commit or /compact (#40931).
|
|
7
|
+
#
|
|
8
|
+
# How it works: Blocks /clear entirely. Use /compact to reduce
|
|
9
|
+
# context without losing it.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: UserPromptSubmit
|
|
12
|
+
# MATCHER: "^/clear$"
|
|
13
|
+
|
|
14
|
+
INPUT=$(cat)
|
|
15
|
+
PROMPT=$(echo "$INPUT" | jq -r '.prompt // empty' 2>/dev/null)
|
|
16
|
+
|
|
17
|
+
if echo "$PROMPT" | grep -qE '^/clear$'; then
|
|
18
|
+
echo "BLOCKED: /clear permanently destroys all context. Use /compact instead to reduce context safely." >&2
|
|
19
|
+
exit 2
|
|
20
|
+
fi
|
|
21
|
+
exit 0
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# core-file-protect-guard.sh — Block edits to core/config/rules files
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code sometimes makes unprompted architectural changes to
|
|
7
|
+
# game rules, configuration files, and core logic files. This hook
|
|
8
|
+
# blocks modifications to files matching configurable glob patterns.
|
|
9
|
+
#
|
|
10
|
+
# Protects files matching CC_PROTECTED_FILES patterns (colon-separated).
|
|
11
|
+
# Default: "*rules*:*config*:*core*"
|
|
12
|
+
#
|
|
13
|
+
# Catches:
|
|
14
|
+
# - Edit/Write tool targeting protected files
|
|
15
|
+
# - Bash commands using sed -i or awk -i on protected files
|
|
16
|
+
#
|
|
17
|
+
# See: https://github.com/anthropics/claude-code/issues/40788
|
|
18
|
+
#
|
|
19
|
+
# TRIGGER: PreToolUse MATCHER: "Edit|Write|Bash"
|
|
20
|
+
#
|
|
21
|
+
# Configuration:
|
|
22
|
+
# CC_PROTECTED_FILES — colon-separated glob patterns
|
|
23
|
+
# Default: "*rules*:*config*:*core*"
|
|
24
|
+
# ================================================================
|
|
25
|
+
|
|
26
|
+
INPUT=$(cat)
|
|
27
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
28
|
+
|
|
29
|
+
# Configurable protected file patterns (colon-separated globs)
|
|
30
|
+
PROTECTED="${CC_PROTECTED_FILES:-*rules*:*config*:*core*}"
|
|
31
|
+
|
|
32
|
+
# Convert colon-separated globs to a function that checks a filename
|
|
33
|
+
matches_protected() {
|
|
34
|
+
local filepath="$1"
|
|
35
|
+
local basename
|
|
36
|
+
basename=$(basename "$filepath")
|
|
37
|
+
|
|
38
|
+
IFS=':' read -ra PATTERNS <<< "$PROTECTED"
|
|
39
|
+
for pattern in "${PATTERNS[@]}"; do
|
|
40
|
+
# Use bash glob matching (case-insensitive via shopt)
|
|
41
|
+
if [[ "$basename" == $pattern ]] || [[ "$filepath" == *$pattern* ]]; then
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
done
|
|
45
|
+
return 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Handle Edit/Write tools
|
|
49
|
+
if [[ "$TOOL" == "Edit" || "$TOOL" == "Write" ]]; then
|
|
50
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
51
|
+
[ -z "$FILE" ] && exit 0
|
|
52
|
+
|
|
53
|
+
if matches_protected "$FILE"; then
|
|
54
|
+
echo "BLOCKED: Cannot modify protected file: $FILE" >&2
|
|
55
|
+
echo "" >&2
|
|
56
|
+
echo "Protected patterns: $PROTECTED" >&2
|
|
57
|
+
echo "Configure with CC_PROTECTED_FILES env var." >&2
|
|
58
|
+
echo "" >&2
|
|
59
|
+
echo "See: https://github.com/anthropics/claude-code/issues/40788" >&2
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Handle Bash tool — check for sed -i / awk -i targeting protected files
|
|
66
|
+
if [[ "$TOOL" == "Bash" ]]; then
|
|
67
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
68
|
+
[ -z "$COMMAND" ] && exit 0
|
|
69
|
+
|
|
70
|
+
# Skip echo/printf
|
|
71
|
+
echo "$COMMAND" | grep -qE '^\s*(echo|printf)\s' && exit 0
|
|
72
|
+
|
|
73
|
+
# Check for sed -i or awk -i inplace targeting protected files
|
|
74
|
+
if echo "$COMMAND" | grep -qE '(sed\s+-i|awk\s+-i\s+inplace)'; then
|
|
75
|
+
# Extract potential file arguments from the command
|
|
76
|
+
IFS=':' read -ra PATTERNS <<< "$PROTECTED"
|
|
77
|
+
for pattern in "${PATTERNS[@]}"; do
|
|
78
|
+
if echo "$COMMAND" | grep -qE "$pattern"; then
|
|
79
|
+
echo "BLOCKED: In-place edit targets protected file pattern: $pattern" >&2
|
|
80
|
+
echo "" >&2
|
|
81
|
+
echo "Command: $COMMAND" >&2
|
|
82
|
+
echo "Protected patterns: $PROTECTED" >&2
|
|
83
|
+
echo "" >&2
|
|
84
|
+
echo "See: https://github.com/anthropics/claude-code/issues/40788" >&2
|
|
85
|
+
exit 2
|
|
86
|
+
fi
|
|
87
|
+
done
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
exit 0
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# cwd-drift-detector.sh — Warn when destructive commands run outside project root
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude frequently loses track of which directory
|
|
5
|
+
# it is in, risking destructive commands in the wrong
|
|
6
|
+
# location (#1669). git reset --hard in the wrong
|
|
7
|
+
# directory can destroy unrelated work.
|
|
8
|
+
#
|
|
9
|
+
# How it works: For destructive commands (git reset, rm -rf,
|
|
10
|
+
# git clean, git checkout -- .), checks if the current
|
|
11
|
+
# directory looks like a project root (has .git, package.json,
|
|
12
|
+
# etc). Warns if it doesn't.
|
|
13
|
+
#
|
|
14
|
+
# TRIGGER: PreToolUse
|
|
15
|
+
# MATCHER: "Bash"
|
|
16
|
+
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
INPUT=$(cat)
|
|
19
|
+
|
|
20
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
21
|
+
[ -z "$COMMAND" ] && exit 0
|
|
22
|
+
|
|
23
|
+
# Only check destructive commands
|
|
24
|
+
if ! echo "$COMMAND" | grep -qE '(git\s+(reset|clean|checkout\s+--|push\s+--force)|rm\s+-rf|DROP\s+TABLE|DROP\s+DATABASE)'; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Check if we're in a project root
|
|
29
|
+
CWD=$(pwd)
|
|
30
|
+
IS_PROJECT=false
|
|
31
|
+
|
|
32
|
+
for marker in .git package.json Cargo.toml go.mod pyproject.toml Makefile; do
|
|
33
|
+
if [ -e "$CWD/$marker" ]; then
|
|
34
|
+
IS_PROJECT=true
|
|
35
|
+
break
|
|
36
|
+
fi
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
if [ "$IS_PROJECT" = false ]; then
|
|
40
|
+
echo "WARNING: Destructive command detected outside project root." >&2
|
|
41
|
+
echo " CWD: $CWD" >&2
|
|
42
|
+
echo " Command: $(echo "$COMMAND" | head -c 100)" >&2
|
|
43
|
+
echo " No project markers (.git, package.json, etc) found." >&2
|
|
44
|
+
echo " Verify you are in the correct directory." >&2
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
exit 0
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# deployment-verify-guard.sh — Warn if committing without post-deploy verification
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code sometimes reports "deployment successful" without
|
|
7
|
+
# actually verifying the deployment works. This hook tracks deploy
|
|
8
|
+
# commands and checks that functional verification was performed
|
|
9
|
+
# before the next git commit.
|
|
10
|
+
#
|
|
11
|
+
# How it works:
|
|
12
|
+
# 1. When a deploy command is detected, logs timestamp to a marker file
|
|
13
|
+
# 2. When verification commands (test, curl, log grep) run, clears the marker
|
|
14
|
+
# 3. When git commit is attempted after a deploy without verification,
|
|
15
|
+
# emits a warning (non-blocking, exit 0)
|
|
16
|
+
#
|
|
17
|
+
# See: https://github.com/anthropics/claude-code/issues/40861
|
|
18
|
+
#
|
|
19
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
20
|
+
#
|
|
21
|
+
# Configuration:
|
|
22
|
+
# CC_DEPLOY_COMMANDS — regex pattern for deploy commands
|
|
23
|
+
# Default: "systemctl restart|docker restart|docker-compose up|deploy|kubectl apply|terraform apply|heroku push"
|
|
24
|
+
#
|
|
25
|
+
# CC_VERIFY_COMMANDS — regex pattern for verification commands
|
|
26
|
+
# Default: "curl|wget|test |pytest|npm test|jest|mocha|rspec|go test|cargo test|make test|grep.*log|tail.*log|journalctl|docker logs|health"
|
|
27
|
+
# ================================================================
|
|
28
|
+
|
|
29
|
+
INPUT=$(cat)
|
|
30
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
31
|
+
|
|
32
|
+
[ -z "$COMMAND" ] && exit 0
|
|
33
|
+
|
|
34
|
+
MARKER="/tmp/cc-deploy-pending-$$"
|
|
35
|
+
|
|
36
|
+
# Configurable deploy command patterns
|
|
37
|
+
DEPLOY_PATTERN="${CC_DEPLOY_COMMANDS:-systemctl\s+restart|docker\s+restart|docker-compose\s+up|docker\s+compose\s+up|\bdeploy\b|kubectl\s+apply|terraform\s+apply|heroku\s+.*push|fly\s+deploy}"
|
|
38
|
+
|
|
39
|
+
# Configurable verify command patterns
|
|
40
|
+
VERIFY_PATTERN="${CC_VERIFY_COMMANDS:-\bcurl\b|\bwget\b|\btest\s|\bpytest\b|npm\s+test|\bjest\b|\bmocha\b|\brspec\b|go\s+test|cargo\s+test|make\s+test|grep.*log|tail.*log|\bjournalctl\b|docker\s+logs|\bhealth}"
|
|
41
|
+
|
|
42
|
+
# Skip echo/printf
|
|
43
|
+
echo "$COMMAND" | grep -qE '^\s*(echo|printf)\s' && exit 0
|
|
44
|
+
|
|
45
|
+
# Check if this is a deploy command
|
|
46
|
+
if echo "$COMMAND" | grep -qiE "$DEPLOY_PATTERN"; then
|
|
47
|
+
date +%s > "$MARKER"
|
|
48
|
+
echo "Deploy detected. Verification will be required before commit." >&2
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Check if this is a verification command — clear the deploy marker
|
|
53
|
+
if echo "$COMMAND" | grep -qiE "$VERIFY_PATTERN"; then
|
|
54
|
+
if [ -f "$MARKER" ]; then
|
|
55
|
+
rm -f "$MARKER"
|
|
56
|
+
fi
|
|
57
|
+
exit 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Check if this is a git commit after an unverified deploy
|
|
61
|
+
if echo "$COMMAND" | grep -qE '\bgit\s+commit\b'; then
|
|
62
|
+
if [ -f "$MARKER" ]; then
|
|
63
|
+
DEPLOY_TIME=$(cat "$MARKER" 2>/dev/null || echo "unknown")
|
|
64
|
+
echo "WARNING: Committing after deployment without verification." >&2
|
|
65
|
+
echo "" >&2
|
|
66
|
+
echo "A deploy command was run (at timestamp $DEPLOY_TIME) but no" >&2
|
|
67
|
+
echo "verification command was detected since then." >&2
|
|
68
|
+
echo "" >&2
|
|
69
|
+
echo "Recommended verifications:" >&2
|
|
70
|
+
echo " curl http://localhost:<port>/health" >&2
|
|
71
|
+
echo " npm test / pytest / go test" >&2
|
|
72
|
+
echo " docker logs <container> | tail" >&2
|
|
73
|
+
echo " journalctl -u <service> --since '5 min ago'" >&2
|
|
74
|
+
echo "" >&2
|
|
75
|
+
echo "See: https://github.com/anthropics/claude-code/issues/40861" >&2
|
|
76
|
+
# Non-blocking — just warn
|
|
77
|
+
rm -f "$MARKER"
|
|
78
|
+
fi
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
exit 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# edit-old-string-validator.sh — Pre-validate Edit tool old_string exists
|
|
3
|
+
#
|
|
4
|
+
# Solves: Parallel Edit tool calls cascade-fail when one Edit's
|
|
5
|
+
# old_string doesn't match the file content (#22264).
|
|
6
|
+
# By catching mismatches before execution, sibling
|
|
7
|
+
# edits in the same batch can proceed normally.
|
|
8
|
+
#
|
|
9
|
+
# How it works: Reads the Edit tool input, checks if old_string
|
|
10
|
+
# exists in the target file. If not found, blocks with exit 2
|
|
11
|
+
# and a descriptive error message.
|
|
12
|
+
#
|
|
13
|
+
# TRIGGER: PreToolUse
|
|
14
|
+
# MATCHER: "Edit"
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
INPUT=$(cat)
|
|
18
|
+
|
|
19
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
20
|
+
OLD_STRING=$(echo "$INPUT" | jq -r '.tool_input.old_string // empty' 2>/dev/null)
|
|
21
|
+
|
|
22
|
+
# Skip if no file or old_string
|
|
23
|
+
[ -z "$FILE" ] && exit 0
|
|
24
|
+
[ -z "$OLD_STRING" ] && exit 0
|
|
25
|
+
|
|
26
|
+
# Skip if file doesn't exist (Edit tool will handle that error)
|
|
27
|
+
[ ! -f "$FILE" ] && exit 0
|
|
28
|
+
|
|
29
|
+
# Check if old_string exists in the file
|
|
30
|
+
if ! grep -qF "$OLD_STRING" "$FILE" 2>/dev/null; then
|
|
31
|
+
echo "BLOCKED: old_string not found in $FILE." >&2
|
|
32
|
+
echo "The file may have been modified by a prior edit in this batch." >&2
|
|
33
|
+
echo "Re-read the file to get the current content before retrying." >&2
|
|
34
|
+
exit 2
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
exit 0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# encoding-preserve-guard.sh — Warn when file encoding changes
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude's Write tool always outputs UTF-8. When editing files
|
|
7
|
+
# that use different encodings (UTF-8 BOM, Latin-1, Shift-JIS),
|
|
8
|
+
# the encoding silently changes, potentially corrupting content.
|
|
9
|
+
# Common in legacy codebases, .csv exports, Windows batch files.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse
|
|
12
|
+
# MATCHER: "Write|Edit"
|
|
13
|
+
# ================================================================
|
|
14
|
+
|
|
15
|
+
INPUT=$(cat)
|
|
16
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
17
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
18
|
+
|
|
19
|
+
[[ "$TOOL" != "Write" && "$TOOL" != "Edit" ]] && exit 0
|
|
20
|
+
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
|
|
21
|
+
|
|
22
|
+
# Check for BOM (Byte Order Mark)
|
|
23
|
+
if head -c 3 "$FILE" 2>/dev/null | od -An -tx1 | grep -q "ef bb bf"; then
|
|
24
|
+
echo "WARNING: $FILE has UTF-8 BOM. Write tool may strip the BOM." >&2
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Check for non-UTF-8 encoding using file command
|
|
28
|
+
ENCODING=$(file -bi "$FILE" 2>/dev/null | grep -oP 'charset=\K\S+')
|
|
29
|
+
if [ -n "$ENCODING" ] && [ "$ENCODING" != "utf-8" ] && [ "$ENCODING" != "us-ascii" ]; then
|
|
30
|
+
echo "WARNING: $FILE uses $ENCODING encoding. Write tool outputs UTF-8." >&2
|
|
31
|
+
echo " This may corrupt non-ASCII characters in the file." >&2
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
exit 0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# git-crypt-worktree-guard.sh — Block worktree creation in git-crypt repos
|
|
3
|
+
#
|
|
4
|
+
# Solves: When Claude creates a worktree in a git-crypt repo,
|
|
5
|
+
# the smudge filter fails because git-crypt hasn't been
|
|
6
|
+
# unlocked in the new worktree. This produces destructive
|
|
7
|
+
# commits that delete all encrypted files (#38538).
|
|
8
|
+
#
|
|
9
|
+
# How it works: Before git worktree add, checks if the repo
|
|
10
|
+
# uses git-crypt (.gitattributes contains filter=git-crypt).
|
|
11
|
+
# If yes, blocks the worktree creation with a warning.
|
|
12
|
+
#
|
|
13
|
+
# TRIGGER: PreToolUse
|
|
14
|
+
# MATCHER: "Bash"
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
INPUT=$(cat)
|
|
18
|
+
|
|
19
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
20
|
+
[ -z "$COMMAND" ] && exit 0
|
|
21
|
+
|
|
22
|
+
# Only check git worktree add
|
|
23
|
+
if ! echo "$COMMAND" | grep -qE 'git\s+worktree\s+add'; then
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Check if repo uses git-crypt
|
|
28
|
+
if [ -f ".gitattributes" ] && grep -q "filter=git-crypt" .gitattributes 2>/dev/null; then
|
|
29
|
+
echo "BLOCKED: Cannot create worktree in a git-crypt repo." >&2
|
|
30
|
+
echo "git-crypt is not automatically unlocked in new worktrees." >&2
|
|
31
|
+
echo "This would produce destructive commits that delete all encrypted files." >&2
|
|
32
|
+
echo "Work in the main repo instead, or manually run 'git-crypt unlock' in the worktree first." >&2
|
|
33
|
+
exit 2
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
exit 0
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# git-operations-require-approval.sh — Block git write operations
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code sometimes ignores CLAUDE.md rules about git commit,
|
|
7
|
+
# push, and branch creation — performing these operations without
|
|
8
|
+
# user approval. This hook enforces the restriction at process level.
|
|
9
|
+
#
|
|
10
|
+
# Blocks:
|
|
11
|
+
# git commit, git push (including --force), git checkout -b,
|
|
12
|
+
# git switch -c, git branch <name>
|
|
13
|
+
#
|
|
14
|
+
# Does NOT block:
|
|
15
|
+
# git status, git log, git diff, git show, git branch (list),
|
|
16
|
+
# git fetch, git stash, git add
|
|
17
|
+
#
|
|
18
|
+
# Handles compound commands (&&, ;, ||) by checking each segment.
|
|
19
|
+
#
|
|
20
|
+
# See: https://github.com/anthropics/claude-code/issues/40695
|
|
21
|
+
#
|
|
22
|
+
# TRIGGER: PreToolUse MATCHER: "Bash"
|
|
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
|
+
# Skip if the command is inside echo/printf (not actual execution)
|
|
31
|
+
echo "$COMMAND" | grep -qE '^\s*(echo|printf)\s' && exit 0
|
|
32
|
+
|
|
33
|
+
# Check each segment of compound commands
|
|
34
|
+
check_segment() {
|
|
35
|
+
local seg="$1"
|
|
36
|
+
# Trim whitespace
|
|
37
|
+
seg=$(echo "$seg" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
38
|
+
[ -z "$seg" ] && return 0
|
|
39
|
+
|
|
40
|
+
# git commit
|
|
41
|
+
if echo "$seg" | grep -qE '\bgit\s+commit\b'; then
|
|
42
|
+
echo "BLOCKED: git commit requires explicit user approval." >&2
|
|
43
|
+
echo "Command: $seg" >&2
|
|
44
|
+
echo "" >&2
|
|
45
|
+
echo "See: https://github.com/anthropics/claude-code/issues/40695" >&2
|
|
46
|
+
return 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# git push (including force variants)
|
|
50
|
+
if echo "$seg" | grep -qE '\bgit\s+push\b'; then
|
|
51
|
+
echo "BLOCKED: git push requires explicit user approval." >&2
|
|
52
|
+
echo "Command: $seg" >&2
|
|
53
|
+
echo "" >&2
|
|
54
|
+
echo "See: https://github.com/anthropics/claude-code/issues/40695" >&2
|
|
55
|
+
return 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# git checkout -b (branch creation)
|
|
59
|
+
if echo "$seg" | grep -qE '\bgit\s+checkout\s+(-b|--branch)\b'; then
|
|
60
|
+
echo "BLOCKED: git branch creation requires explicit user approval." >&2
|
|
61
|
+
echo "Command: $seg" >&2
|
|
62
|
+
return 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# git switch -c / --create (branch creation)
|
|
66
|
+
if echo "$seg" | grep -qE '\bgit\s+switch\s+(-c|--create)\b'; then
|
|
67
|
+
echo "BLOCKED: git branch creation requires explicit user approval." >&2
|
|
68
|
+
echo "Command: $seg" >&2
|
|
69
|
+
return 1
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# git branch <name> (creation, not listing)
|
|
73
|
+
# git branch without flags or with only -a/-r/-l/--list is listing
|
|
74
|
+
if echo "$seg" | grep -qE '\bgit\s+branch\s'; then
|
|
75
|
+
# Allow listing flags
|
|
76
|
+
if echo "$seg" | grep -qE '\bgit\s+branch\s+(-[arl]|--list|--merged|--no-merged|--contains|-v|--verbose|-d|--delete|-D)\b'; then
|
|
77
|
+
return 0
|
|
78
|
+
fi
|
|
79
|
+
# If it has a name argument after "git branch", it's creation
|
|
80
|
+
local args
|
|
81
|
+
args=$(echo "$seg" | sed 's/.*\bgit\s\+branch\s\+//')
|
|
82
|
+
if [ -n "$args" ] && ! echo "$args" | grep -qE '^\s*$'; then
|
|
83
|
+
echo "BLOCKED: git branch creation requires explicit user approval." >&2
|
|
84
|
+
echo "Command: $seg" >&2
|
|
85
|
+
return 1
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
return 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Split on && ; || and check each part
|
|
93
|
+
while IFS= read -r segment; do
|
|
94
|
+
if ! check_segment "$segment"; then
|
|
95
|
+
exit 2
|
|
96
|
+
fi
|
|
97
|
+
done < <(echo "$COMMAND" | sed 's/&&/\n/g; s/;/\n/g; s/||/\n/g')
|
|
98
|
+
|
|
99
|
+
exit 0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# line-ending-guard.sh — Warn on CRLF/LF mismatch
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Claude Code outputs LF line endings. On Windows/WSL, files may
|
|
7
|
+
# use CRLF. Editing a CRLF file with LF content creates mixed
|
|
8
|
+
# line endings, causing test failures, script errors, and git
|
|
9
|
+
# noise. Especially common in .bat, .cmd, .ps1 files.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse
|
|
12
|
+
# MATCHER: "Write|Edit"
|
|
13
|
+
# ================================================================
|
|
14
|
+
|
|
15
|
+
INPUT=$(cat)
|
|
16
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
17
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
18
|
+
|
|
19
|
+
[[ "$TOOL" != "Write" && "$TOOL" != "Edit" ]] && exit 0
|
|
20
|
+
[ -z "$FILE" ] || [ ! -f "$FILE" ] && exit 0
|
|
21
|
+
|
|
22
|
+
# Check if existing file uses CRLF
|
|
23
|
+
if head -c 1000 "$FILE" 2>/dev/null | od -c | grep -q '\\r\\n'; then
|
|
24
|
+
echo "WARNING: $FILE uses CRLF line endings. Claude outputs LF." >&2
|
|
25
|
+
echo " This may create mixed line endings. Consider:" >&2
|
|
26
|
+
echo " - Setting .gitattributes: *.bat text eol=crlf" >&2
|
|
27
|
+
echo " - Running: unix2dos $FILE (after edit)" >&2
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
exit 0
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# permission-pattern-auto-allow.sh — Auto-allow commands matching user-defined patterns
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude repeatedly asks for permission to run commands
|
|
5
|
+
# even after "Always Allow" — because the settings use
|
|
6
|
+
# exact argument matching, not pattern matching (#819).
|
|
7
|
+
#
|
|
8
|
+
# How it works: Maintains a list of regex patterns in an env var
|
|
9
|
+
# or config file. If the Bash command matches any pattern,
|
|
10
|
+
# returns allow decision. Bypasses the broken exact-match
|
|
11
|
+
# permission system entirely.
|
|
12
|
+
#
|
|
13
|
+
# Config: Set ALLOWED_PATTERNS env var or create ~/.claude/allowed-patterns.txt
|
|
14
|
+
# Example patterns (one per line):
|
|
15
|
+
# ^npm (test|run|install|ci)
|
|
16
|
+
# ^git (status|log|diff|add|commit|push|pull|fetch|branch|checkout)
|
|
17
|
+
# ^(ls|cat|pwd|echo|head|tail|wc|grep|find|which|env)
|
|
18
|
+
# ^python[23]?\s
|
|
19
|
+
# ^cargo (build|test|run|check)
|
|
20
|
+
#
|
|
21
|
+
# TRIGGER: PreToolUse
|
|
22
|
+
# MATCHER: "Bash"
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
INPUT=$(cat)
|
|
26
|
+
|
|
27
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
28
|
+
[ -z "$COMMAND" ] && exit 0
|
|
29
|
+
|
|
30
|
+
# Load patterns from file or env
|
|
31
|
+
PATTERN_FILE="${HOME}/.claude/allowed-patterns.txt"
|
|
32
|
+
if [ -f "$PATTERN_FILE" ]; then
|
|
33
|
+
while IFS= read -r pattern || [ -n "$pattern" ]; do
|
|
34
|
+
# Skip empty lines and comments
|
|
35
|
+
[[ -z "$pattern" || "$pattern" == \#* ]] && continue
|
|
36
|
+
if echo "$COMMAND" | grep -qE "$pattern" 2>/dev/null; then
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
done < "$PATTERN_FILE"
|
|
40
|
+
elif [ -n "${ALLOWED_PATTERNS:-}" ]; then
|
|
41
|
+
# Fallback: pipe-separated patterns in env var
|
|
42
|
+
echo "$ALLOWED_PATTERNS" | tr '|' '\n' | while IFS= read -r pattern; do
|
|
43
|
+
[ -z "$pattern" ] && continue
|
|
44
|
+
if echo "$COMMAND" | grep -qE "$pattern" 2>/dev/null; then
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
done
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
exit 0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# read-audit-log.sh — Log all file read operations for forensics
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Creates a searchable audit trail of every file Claude reads.
|
|
7
|
+
# Useful for:
|
|
8
|
+
# - Post-incident forensics ("what did Claude access?")
|
|
9
|
+
# - Detecting prompt injection (tracking reads of untrusted files)
|
|
10
|
+
# - Understanding context consumption (which files cost tokens)
|
|
11
|
+
#
|
|
12
|
+
# TRIGGER: PostToolUse
|
|
13
|
+
# MATCHER: "Read"
|
|
14
|
+
#
|
|
15
|
+
# OUTPUT: ~/.claude/read-audit.jsonl (append-only)
|
|
16
|
+
# ================================================================
|
|
17
|
+
|
|
18
|
+
INPUT=$(cat)
|
|
19
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
20
|
+
[ "$TOOL" != "Read" ] && exit 0
|
|
21
|
+
|
|
22
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
23
|
+
[ -z "$FILE" ] && exit 0
|
|
24
|
+
|
|
25
|
+
AUDIT_FILE="${CC_READ_AUDIT:-$HOME/.claude/read-audit.jsonl}"
|
|
26
|
+
mkdir -p "$(dirname "$AUDIT_FILE")"
|
|
27
|
+
|
|
28
|
+
# Get file metadata
|
|
29
|
+
SIZE=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE" 2>/dev/null || echo 0)
|
|
30
|
+
LINES=$(wc -l < "$FILE" 2>/dev/null || echo 0)
|
|
31
|
+
|
|
32
|
+
echo "{\"time\":\"$(date -Iseconds)\",\"file\":\"$FILE\",\"size\":$SIZE,\"lines\":$LINES,\"cwd\":\"$(pwd)\"}" >> "$AUDIT_FILE"
|
|
33
|
+
|
|
34
|
+
exit 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# session-duration-guard.sh — Warn on long-running sessions
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Model quality degrades in very long sessions due to context
|
|
7
|
+
# accumulation, compaction artifacts, and attention dilution.
|
|
8
|
+
# This hook warns at configurable thresholds and suggests
|
|
9
|
+
# saving state + starting fresh.
|
|
10
|
+
#
|
|
11
|
+
# Based on 700+ hours of autonomous operation experience.
|
|
12
|
+
#
|
|
13
|
+
# TRIGGER: PostToolUse
|
|
14
|
+
# MATCHER: "" (all tools)
|
|
15
|
+
#
|
|
16
|
+
# CONFIG:
|
|
17
|
+
# CC_SESSION_WARN_HOURS=2 (warn after 2 hours, default)
|
|
18
|
+
# CC_SESSION_CRITICAL_HOURS=4 (critical after 4 hours, default)
|
|
19
|
+
# ================================================================
|
|
20
|
+
|
|
21
|
+
MARKER="/tmp/cc-session-start-$$"
|
|
22
|
+
WARN_HOURS="${CC_SESSION_WARN_HOURS:-2}"
|
|
23
|
+
CRITICAL_HOURS="${CC_SESSION_CRITICAL_HOURS:-4}"
|
|
24
|
+
|
|
25
|
+
# Create marker on first run
|
|
26
|
+
if [ ! -f "$MARKER" ]; then
|
|
27
|
+
date +%s > "$MARKER"
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Check every 50 tool calls (not every call)
|
|
32
|
+
COUNTER="/tmp/cc-duration-counter-$$"
|
|
33
|
+
COUNT=$(cat "$COUNTER" 2>/dev/null || echo 0)
|
|
34
|
+
COUNT=$((COUNT + 1))
|
|
35
|
+
echo "$COUNT" > "$COUNTER"
|
|
36
|
+
[ $((COUNT % 50)) -ne 0 ] && exit 0
|
|
37
|
+
|
|
38
|
+
START=$(cat "$MARKER" 2>/dev/null || echo 0)
|
|
39
|
+
NOW=$(date +%s)
|
|
40
|
+
ELAPSED=$(( (NOW - START) / 3600 ))
|
|
41
|
+
ELAPSED_MIN=$(( (NOW - START) / 60 ))
|
|
42
|
+
|
|
43
|
+
if [ "$ELAPSED" -ge "$CRITICAL_HOURS" ]; then
|
|
44
|
+
echo "⚠ CRITICAL: Session running for ${ELAPSED_MIN} minutes (${ELAPSED}+ hours)." >&2
|
|
45
|
+
echo " Model quality typically degrades after ${CRITICAL_HOURS} hours." >&2
|
|
46
|
+
echo " Save your state and start a new session: /compact then resume later." >&2
|
|
47
|
+
elif [ "$ELAPSED" -ge "$WARN_HOURS" ]; then
|
|
48
|
+
echo "NOTE: Session running for ${ELAPSED_MIN} minutes. Consider saving state." >&2
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
exit 0
|