cc-safe-setup 29.6.37 → 29.6.38
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/2026-03-29 +2 -0
- package/COOKBOOK.md +35 -0
- package/README.md +57 -6
- package/TROUBLESHOOTING.md +45 -0
- package/examples/cch-cache-guard.sh +25 -0
- package/examples/compact-alert-notification.sh +39 -0
- package/examples/conversation-history-guard.sh +73 -0
- package/examples/image-file-validator.sh +40 -0
- package/examples/mcp-warmup-wait.sh +39 -0
- package/examples/permission-denial-enforcer.sh +92 -0
- package/examples/pre-compact-transcript-backup.sh +71 -0
- package/examples/prompt-usage-logger.sh +53 -0
- package/examples/read-only-mode.sh +77 -0
- package/examples/replace-all-guard.sh +56 -0
- package/examples/ripgrep-permission-fix.sh +58 -0
- package/examples/session-backup-on-start.sh +72 -0
- package/examples/session-index-repair.sh +87 -0
- package/examples/subagent-error-detector.sh +73 -0
- package/examples/subagent-scope-validator.sh +53 -29
- package/examples/task-integrity-guard.sh +82 -0
- package/examples/working-directory-fence.sh +91 -0
- package/index.mjs +16 -1
- package/package.json +7 -3
- package/scripts.json +9 -9
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ================================================================
|
|
3
|
+
# working-directory-fence.sh — Block file operations outside CWD
|
|
4
|
+
# ================================================================
|
|
5
|
+
# PURPOSE:
|
|
6
|
+
# Prevents Claude Code from reading, writing, or editing files
|
|
7
|
+
# outside the current working directory tree. Catches the common
|
|
8
|
+
# problem where Claude wanders to a stale copy of the project
|
|
9
|
+
# on a different drive or directory.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse
|
|
12
|
+
# MATCHER: "Read|Edit|Write"
|
|
13
|
+
#
|
|
14
|
+
# WHY THIS MATTERS:
|
|
15
|
+
# Claude sometimes ignores explicit directory instructions and
|
|
16
|
+
# operates on files in completely different locations (e.g.,
|
|
17
|
+
# working on C:\Users\old-copy instead of D:\projects\current).
|
|
18
|
+
# This leads to edits being applied to the wrong files with
|
|
19
|
+
# no indication to the user.
|
|
20
|
+
#
|
|
21
|
+
# WHAT IT DOES:
|
|
22
|
+
# Extracts file_path from the tool input. If the resolved path
|
|
23
|
+
# is outside the current working directory, blocks with exit 2.
|
|
24
|
+
#
|
|
25
|
+
# CONFIGURATION:
|
|
26
|
+
# CC_FENCE_ALLOW — colon-separated list of additional allowed
|
|
27
|
+
# directories (e.g., "/tmp:/home/user/.config")
|
|
28
|
+
#
|
|
29
|
+
# RELATED ISSUES:
|
|
30
|
+
# https://github.com/anthropics/claude-code/issues/41850
|
|
31
|
+
# ================================================================
|
|
32
|
+
|
|
33
|
+
set -u
|
|
34
|
+
|
|
35
|
+
INPUT=$(cat)
|
|
36
|
+
|
|
37
|
+
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
38
|
+
|
|
39
|
+
if [ -z "$FILE_PATH" ]; then
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
CWD=$(pwd)
|
|
44
|
+
|
|
45
|
+
# Resolve the file path to absolute
|
|
46
|
+
# Handle ~ expansion
|
|
47
|
+
FILE_PATH=$(printf '%s' "$FILE_PATH" | sed "s|^~|$HOME|")
|
|
48
|
+
|
|
49
|
+
# Convert to absolute path if relative
|
|
50
|
+
case "$FILE_PATH" in
|
|
51
|
+
/*) ;; # already absolute
|
|
52
|
+
*) FILE_PATH="${CWD}/${FILE_PATH}" ;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
# Normalize (remove .., trailing slashes)
|
|
56
|
+
# Use realpath if available, fallback to simple check
|
|
57
|
+
if command -v realpath >/dev/null 2>&1; then
|
|
58
|
+
# realpath may fail if file doesn't exist yet (Write), so use -m
|
|
59
|
+
RESOLVED=$(realpath -m "$FILE_PATH" 2>/dev/null || printf '%s' "$FILE_PATH")
|
|
60
|
+
else
|
|
61
|
+
RESOLVED="$FILE_PATH"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Check if the path is inside CWD
|
|
65
|
+
case "$RESOLVED" in
|
|
66
|
+
"${CWD}"*) exit 0 ;; # Inside CWD — allowed
|
|
67
|
+
esac
|
|
68
|
+
|
|
69
|
+
# Check additional allowed directories
|
|
70
|
+
if [ -n "${CC_FENCE_ALLOW:-}" ]; then
|
|
71
|
+
IFS=':' read -ra ALLOWED <<< "$CC_FENCE_ALLOW"
|
|
72
|
+
for dir in "${ALLOWED[@]}"; do
|
|
73
|
+
case "$RESOLVED" in
|
|
74
|
+
"${dir}"*) exit 0 ;; # Inside allowed dir
|
|
75
|
+
esac
|
|
76
|
+
done
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Also allow /tmp (commonly used for scratch files)
|
|
80
|
+
case "$RESOLVED" in
|
|
81
|
+
/tmp*) exit 0 ;;
|
|
82
|
+
esac
|
|
83
|
+
|
|
84
|
+
printf 'BLOCKED: File operation outside working directory.\n' >&2
|
|
85
|
+
printf '\n' >&2
|
|
86
|
+
printf ' File: %s\n' "$RESOLVED" >&2
|
|
87
|
+
printf ' CWD: %s\n' "$CWD" >&2
|
|
88
|
+
printf '\n' >&2
|
|
89
|
+
printf 'Claude is trying to access a file outside the current project.\n' >&2
|
|
90
|
+
printf 'If this is intentional, set CC_FENCE_ALLOW to include the path.\n' >&2
|
|
91
|
+
exit 2
|
package/index.mjs
CHANGED
|
@@ -191,6 +191,9 @@ if (HELP) {
|
|
|
191
191
|
More: https://github.com/yurukusa/cc-safe-setup
|
|
192
192
|
Find hooks: npx cc-hook-registry search <keyword>
|
|
193
193
|
Test hooks: npx cc-hook-test <hook.sh>
|
|
194
|
+
|
|
195
|
+
Support: https://github.com/sponsors/yurukusa
|
|
196
|
+
Book: https://zenn.dev/yurukusa/books/6076c23b1cb18b
|
|
194
197
|
`);
|
|
195
198
|
process.exit(0);
|
|
196
199
|
}
|
|
@@ -3625,7 +3628,7 @@ function issues() {
|
|
|
3625
3628
|
{ hook: 'deploy-guard', issues: ['#37314 Deploy without commit'] },
|
|
3626
3629
|
{ hook: 'protect-dotfiles', issues: ['#37478 .bashrc destroyed (3r)'] },
|
|
3627
3630
|
{ hook: 'scope-guard', issues: ['#36233 Files deleted outside project (67r)'] },
|
|
3628
|
-
{ hook: 'env-source-guard', issues: ['#401 .env loaded into bash environment (54r)'] },
|
|
3631
|
+
{ hook: 'env-source-guard', issues: ['#401 .env loaded into bash environment (54r)', '#40730 Shell builtins bypass deny patterns with absolute paths'] },
|
|
3629
3632
|
{ hook: 'diff-size-guard', issues: ['Unreviable mega-commits'] },
|
|
3630
3633
|
{ hook: 'dependency-audit', issues: ['Supply chain risk from unknown packages'] },
|
|
3631
3634
|
{ hook: 'read-before-edit', issues: ['old_string mismatch from editing unread files'] },
|
|
@@ -3638,6 +3641,14 @@ function issues() {
|
|
|
3638
3641
|
{ hook: 'protect-claudemd', issues: ['AI modifying its own config files'] },
|
|
3639
3642
|
{ hook: 'git-tag-guard', issues: ['Accidental tag push'] },
|
|
3640
3643
|
{ hook: 'npm-publish-guard', issues: ['Accidental publish without version check'] },
|
|
3644
|
+
{ hook: 'read-only-mode', issues: ['#41063 Claude ignores read-only instructions during test tasks (0r)'] },
|
|
3645
|
+
{ hook: 'task-integrity-guard', issues: ['#41109 Agent deleted open tasks to hide unfinished work (0r)'] },
|
|
3646
|
+
{ hook: 'permission-denial-enforcer', issues: ['#41103 Sandbox bypass after user denied Write permission (0r)'] },
|
|
3647
|
+
{ hook: 'no-git-amend-push', issues: ['#41162 Creates new commit even when instructed to amend/squash'] },
|
|
3648
|
+
{ hook: 'auto-approve-nix', issues: ['#34007 Command parsing for permissions incorrectly handles # (6r)'] },
|
|
3649
|
+
{ hook: 'git-operations-require-approval', issues: ['#40695 CLAUDE.md git rules ignored', '#39924 Branch switches on new session'] },
|
|
3650
|
+
{ hook: 'token-budget-guard', issues: ['#41046 Account suspension from extension background activity', '#38826 Session limit tracking (2r)'] },
|
|
3651
|
+
{ hook: 'windows-path-guard', issues: ['#38890 Path rewrite to fabricated Windows path (1r)', '#40164 Worktree fails on Windows'] },
|
|
3641
3652
|
];
|
|
3642
3653
|
|
|
3643
3654
|
console.log();
|
|
@@ -5870,6 +5881,10 @@ async function main() {
|
|
|
5870
5881
|
console.log();
|
|
5871
5882
|
console.log(' ' + c.dim + '23 web tools: https://yurukusa.github.io/cc-safe-setup/hub.html' + c.reset);
|
|
5872
5883
|
console.log();
|
|
5884
|
+
console.log(' ' + c.dim + 'Like this tool? Support development:' + c.reset);
|
|
5885
|
+
console.log(' ' + c.dim + ' Sponsor: https://github.com/sponsors/yurukusa' + c.reset);
|
|
5886
|
+
console.log(' ' + c.dim + ' Book: https://zenn.dev/yurukusa/books/6076c23b1cb18b' + c.reset);
|
|
5887
|
+
console.log();
|
|
5873
5888
|
}
|
|
5874
5889
|
|
|
5875
5890
|
main().catch(e => { console.error(e); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "29.6.
|
|
4
|
-
"description": "One command to make Claude Code safe.
|
|
3
|
+
"version": "29.6.38",
|
|
4
|
+
"description": "One command to make Claude Code safe. 654 example hooks + 8 built-in. 56 CLI commands. Token consumption diagnosis. Works with Auto Mode.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cc-safe-setup": "index.mjs"
|
|
@@ -42,5 +42,9 @@
|
|
|
42
42
|
"type": "git",
|
|
43
43
|
"url": "https://github.com/yurukusa/cc-safe-setup"
|
|
44
44
|
},
|
|
45
|
-
"homepage": "https://yurukusa.github.io/cc-safe-setup/"
|
|
45
|
+
"homepage": "https://yurukusa.github.io/cc-safe-setup/",
|
|
46
|
+
"funding": {
|
|
47
|
+
"type": "github",
|
|
48
|
+
"url": "https://github.com/sponsors/yurukusa"
|
|
49
|
+
}
|
|
46
50
|
}
|
package/scripts.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"destructive-guard": "#!/bin/bash\n# ================================================================\n# destructive-guard.sh — Destructive Command Blocker\n# ================================================================\n# PURPOSE:\n# Blocks dangerous shell commands that can cause irreversible damage.\n# Catches rm -rf on sensitive paths, git reset --hard, git clean -fd,\n# and other destructive operations before they execute.\n#\n# Built after a real incident where rm -rf on a pnpm project\n# followed NTFS junctions and deleted an entire C:\\Users directory.\n# (GitHub Issue #36339)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - rm -rf / rm -r on root, home, or parent paths (/, ~, .., /home, /etc)\n# - git reset --hard\n# - git clean -fd / git clean -fdx\n# - chmod -R 777 on sensitive paths\n# - find ... -delete on broad patterns\n#\n# WHAT IT ALLOWS (exit 0):\n# - rm -rf on specific project subdirectories (node_modules, dist, build)\n# - git reset --soft, git reset HEAD\n# - All non-destructive commands\n#\n# CONFIGURATION:\n# CC_ALLOW_DESTRUCTIVE=1 — disable this guard (not recommended)\n# CC_SAFE_DELETE_DIRS — colon-separated list of safe-to-delete dirs\n# default: \"node_modules:dist:build:.cache:__pycache__:coverage\"\n#\n# NOTE: On Windows/WSL2, rm -rf can follow NTFS junctions (symlinks)\n# and delete far more than intended. This guard is especially critical\n# on WSL2 environments.\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(
|
|
3
|
-
"branch-guard": "#!/bin/bash\n# ================================================================\n# branch-guard.sh — Branch Push Protector\n# ================================================================\n# PURPOSE:\n# Prevents accidental git push to main/master branches AND\n# blocks force-push on ALL branches without explicit approval.\n#\n# Force-pushes rewrite history and can destroy teammates' work.\n# Protected branch pushes bypass code review.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - git push origin main/master (any protected branch)\n# - git push --force (any branch — history rewriting)\n# - git push -f (short flag variant)\n# - git push --force-with-lease (still destructive)\n#\n# WHAT IT ALLOWS (exit 0):\n# - git push origin feature-branch (non-force)\n# - git push -u origin feature-branch\n# - All other git commands\n# - All non-git commands\n#\n# CONFIGURATION:\n# CC_PROTECT_BRANCHES — colon-separated list of protected branches\n# default: \"main:master\"\n# CC_ALLOW_FORCE_PUSH=1 — disable force-push protection\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(
|
|
4
|
-
"syntax-check": "#!/bin/bash\n# ================================================================\n# syntax-check.sh — Automatic Syntax Validation After Edits\n# ================================================================\n# PURPOSE:\n# Runs syntax checks immediately after Claude Code edits or\n# writes a file. Catches syntax errors before they propagate\n# into downstream failures.\n#\n# SUPPORTED LANGUAGES:\n# .py — python -m py_compile\n# .sh — bash -n\n# .bash — bash -n\n# .json — jq empty\n# .yaml — python3 yaml.safe_load (if PyYAML installed)\n# .yml — python3 yaml.safe_load (if PyYAML installed)\n# .js — node --check (if node installed)\n# .ts — npx tsc --noEmit (if tsc available) [EXPERIMENTAL]\n#\n# TRIGGER: PostToolUse\n# MATCHER: \"Edit|Write\"\n#\n# DESIGN PHILOSOPHY:\n# - Never blocks (always exit 0) — reports errors but doesn't\n# prevent the edit from completing\n# - Silent on success — only speaks up when something is wrong\n# - Fails open — if a checker isn't installed, silently skips\n#\n# BORN FROM:\n# Countless sessions where Claude Code introduced a syntax error,\n# continued working for 10+ tool calls, then hit a wall when\n# trying to run the broken file. Catching it immediately saves\n# context window and frustration.\n# ================================================================\n\nINPUT=$(cat)\nFILE_PATH=$(
|
|
2
|
+
"destructive-guard": "#!/bin/bash\n# ================================================================\n# destructive-guard.sh — Destructive Command Blocker\n# ================================================================\n# PURPOSE:\n# Blocks dangerous shell commands that can cause irreversible damage.\n# Catches rm -rf on sensitive paths, git reset --hard, git clean -fd,\n# and other destructive operations before they execute.\n#\n# Built after a real incident where rm -rf on a pnpm project\n# followed NTFS junctions and deleted an entire C:\\Users directory.\n# (GitHub Issue #36339)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - rm -rf / rm -r on root, home, or parent paths (/, ~, .., /home, /etc)\n# - git reset --hard\n# - git clean -fd / git clean -fdx\n# - chmod -R 777 on sensitive paths\n# - find ... -delete on broad patterns\n#\n# WHAT IT ALLOWS (exit 0):\n# - rm -rf on specific project subdirectories (node_modules, dist, build)\n# - git reset --soft, git reset HEAD\n# - All non-destructive commands\n#\n# CONFIGURATION:\n# CC_ALLOW_DESTRUCTIVE=1 — disable this guard (not recommended)\n# CC_SAFE_DELETE_DIRS — colon-separated list of safe-to-delete dirs\n# default: \"node_modules:dist:build:.cache:__pycache__:coverage\"\n#\n# NOTE: On Windows/WSL2, rm -rf can follow NTFS junctions (symlinks)\n# and delete far more than intended. This guard is especially critical\n# on WSL2 environments.\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Allow override (not recommended)\nif [[ \"${CC_ALLOW_DESTRUCTIVE:-0}\" == \"1\" ]]; then\n exit 0\nfi\n\n# Log function — records blocked commands for audit\nlog_block() {\n local reason=\"$1\"\n local logfile=\"${CC_BLOCK_LOG:-$HOME/.claude/blocked-commands.log}\"\n mkdir -p \"$(dirname \"$logfile\")\" 2>/dev/null\n echo \"[$(date -Iseconds)] BLOCKED: $reason | cmd: $COMMAND\" >> \"$logfile\" 2>/dev/null\n}\n\n# Safe directories that can be deleted\nSAFE_DIRS=\"${CC_SAFE_DELETE_DIRS:-node_modules:dist:build:.cache:__pycache__:coverage:.next:.nuxt:tmp}\"\n\n# --- Check 0: --no-preserve-root ---\nif printf '%s' \"$COMMAND\" | grep -qE \"rm\\\\s.*\\\\-\\\\-no-preserve-root\"; then\n echo \"BLOCKED: --no-preserve-root detected.\" >&2\n exit 2\nfi\n\n# --- Check 1: rm -rf on dangerous paths ---\nif printf '%s' \"$COMMAND\" | grep -qE 'rm\\s+(-[rf]+\\s+)*(\\/$|\\/\\s|\\/[^a-z]|\\/home|\\/etc|\\/usr|\\/var|\\/mnt|~\\/|~\\s*$|\\.\\.\\/|\\.\\.\\s*$|\\.\\s*$|\\.\\/\\s*$)'; then\n # Exception: safe directories\n SAFE=0\n IFS=':' read -ra DIRS <<< \"$SAFE_DIRS\"\n for dir in \"${DIRS[@]}\"; do\n if printf '%s' \"$COMMAND\" | grep -qE \"rm\\s+.*${dir}\\s*$|rm\\s+.*${dir}/\"; then\n SAFE=1\n break\n fi\n done\n\n # Check for mounted filesystems inside the target (NFS, Docker, bind mounts)\n # Why: GitHub #36640 — rm -rf on a dir with NFS mount deleted production data\n if (( SAFE == 0 )); then\n # Extract the target path from the rm command\n TARGET_PATH=$(printf '%s' \"$COMMAND\" | grep -oP 'rm\\s+(-[rf]+\\s+)*\\K\\S+')\n if [ -n \"$TARGET_PATH\" ] && command -v findmnt &>/dev/null; then\n if findmnt -n -o TARGET --submounts \"$TARGET_PATH\" 2>/dev/null | grep -q .; then\n log_block \"rm on path with mounted filesystem\"\n echo \"BLOCKED: Target contains a mounted filesystem (NFS, Docker, bind).\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Unmount the filesystem first, then retry.\" >&2\n exit 2\n fi\n fi\n fi\n\n if (( SAFE == 0 )); then\n log_block \"rm on sensitive path\"\n echo \"BLOCKED: rm on sensitive path detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"This command targets a sensitive directory that could cause\" >&2\n echo \"irreversible data loss. On WSL2, rm -rf can follow NTFS\" >&2\n echo \"junctions and delete far beyond the target directory.\" >&2\n echo \"\" >&2\n echo \"If you need to delete a specific subdirectory, target it directly:\" >&2\n echo \" rm -rf ./specific-folder\" >&2\n exit 2\n fi\nfi\n\n# --- Check 2: git reset --hard ---\n# Only match when git is the actual command, not inside strings/arguments\nif printf '%s' \"$COMMAND\" | grep -qE '^\\s*git\\s+reset\\s+--hard|;\\s*git\\s+reset\\s+--hard|&&\\s*git\\s+reset\\s+--hard|\\|\\|\\s*git\\s+reset\\s+--hard'; then\n log_block \"git reset --hard\"\n echo \"BLOCKED: git reset --hard discards all uncommitted changes.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git stash, or git reset --soft to keep changes staged.\" >&2\n exit 2\nfi\n\n# --- Check 3: git clean -fd ---\nif printf '%s' \"$COMMAND\" | grep -qE '^\\s*git\\s+clean\\s+-[a-z]*[fd]|;\\s*git\\s+clean|&&\\s*git\\s+clean|\\|\\|\\s*git\\s+clean'; then\n log_block \"git clean\"\n echo \"BLOCKED: git clean removes untracked files permanently.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git clean -n (dry run) first to see what would be deleted.\" >&2\n exit 2\nfi\n\n# --- Check 4: chmod 777 on broad paths ---\nif printf '%s' \"$COMMAND\" | grep -qE 'chmod\\s+(-R\\s+)?777\\s+(\\/|~|\\.)'; then\n echo \"BLOCKED: chmod 777 on broad path is a security risk.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n exit 2\nfi\n\n# --- Check 5: find -delete on broad patterns ---\nif printf '%s' \"$COMMAND\" | grep -qE 'find\\s+(\\/|~|\\.\\.)\\s.*-delete'; then\n echo \"BLOCKED: find -delete on broad path risks mass deletion.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: find ... -print first to verify what matches.\" >&2\n exit 2\nfi\n\n# --- Check 6: sudo with dangerous commands ---\nif printf '%s' \"$COMMAND\" | grep -qE '^\\s*sudo\\s+(rm\\s+-[rf]|chmod\\s+(-R\\s+)?777|dd\\s+if=|mkfs)'; then\n log_block \"sudo with dangerous command\"\n echo \"BLOCKED: sudo with dangerous command detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Running destructive commands with sudo amplifies the damage.\" >&2\n echo \"Review the command carefully before proceeding.\" >&2\n exit 2\nfi\n\n\n# --- Check 7: PowerShell Remove-Item (Windows/WSL2) ---\n# Real incident: GitHub #37331 — destroyed entire repo\n# Skip if command is git commit (message text triggers false positive)\nif printf '%s' \"$COMMAND\" | grep -qE '^\\s*(git\\s+commit|echo\\s|printf\\s|cat\\s)'; then\n : # string output commands mentioning PS commands are not destructive\nelif printf '%s' \"$COMMAND\" | grep -qiE 'Remove-Item.*-Recurse.*-Force|Remove-Item.*-Force.*-Recurse|del\\s+/s\\s+/q|rd\\s+/s\\s+/q|rmdir\\s+/s\\s+/q'; then\n log_block \"PowerShell destructive command\"\n echo \"BLOCKED: Destructive PowerShell command detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Remove-Item with recursive force-delete can destroy entire directories\" >&2\n echo \"irreversibly. Target specific files instead.\" >&2\n exit 2\nfi\nif printf '%s' \"$COMMAND\" | grep -qE '(^|;|&&|\\|\\|)\\s*git\\s+(checkout|switch)\\s+.*(--force\\b|-f\\b|--discard-changes\\b)'; then\n log_block \"git checkout/switch --force\"\n echo \"BLOCKED: git checkout/switch with --force discards uncommitted changes.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git stash before switching, or use git switch without --force.\" >&2\n exit 2\nfi\n\nif printf '%s' \"$COMMAND\" | grep -qE '(sh|bash|zsh)\\s+-c\\s+'; then\n INNER=$(printf '%s' \"$COMMAND\" | sed -E \"s/.*(sh|bash|zsh)\\s+-c\\s+['\\\"]//\" | sed \"s/['\\\"]*$//\" )\n if echo \"$INNER\" | grep -qE 'rm\\s+-[rf]*\\s+[/~]|git\\s+reset\\s+--hard|git\\s+clean\\s+-[fd]+|mkfs\\.|dd\\s+if='; then\n echo \"BLOCKED: Destructive command hidden in shell wrapper\" >&2\n echo \"\" >&2\n echo \"Detected: $INNER\" >&2\n exit 2\n fi\nfi\nif printf '%s' \"$COMMAND\" | grep -qE '\\|\\s*(sh|bash)\\s*$'; then\n if printf '%s' \"$COMMAND\" | grep -qE 'rm\\s+-[rf]*\\s+[/~]|git\\s+reset\\s+--hard|git\\s+clean\\s+-[fd]+'; then\n echo \"BLOCKED: Destructive command piped to shell\" >&2\n exit 2\n fi\nfi\nexit 0",
|
|
3
|
+
"branch-guard": "#!/bin/bash\n# ================================================================\n# branch-guard.sh — Branch Push Protector\n# ================================================================\n# PURPOSE:\n# Prevents accidental git push to main/master branches AND\n# blocks force-push on ALL branches without explicit approval.\n#\n# Force-pushes rewrite history and can destroy teammates' work.\n# Protected branch pushes bypass code review.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - git push origin main/master (any protected branch)\n# - git push --force (any branch — history rewriting)\n# - git push -f (short flag variant)\n# - git push --force-with-lease (still destructive)\n#\n# WHAT IT ALLOWS (exit 0):\n# - git push origin feature-branch (non-force)\n# - git push -u origin feature-branch\n# - All other git commands\n# - All non-git commands\n#\n# CONFIGURATION:\n# CC_PROTECT_BRANCHES — colon-separated list of protected branches\n# default: \"main:master\"\n# CC_ALLOW_FORCE_PUSH=1 — disable force-push protection\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Only check git push commands\nif ! printf '%s' \"$COMMAND\" | grep -qE '^\\s*git\\s+push'; then\n exit 0\nfi\n\n# --- Check 1: Force push on ANY branch ---\nif [[ \"${CC_ALLOW_FORCE_PUSH:-0}\" != \"1\" ]]; then\n if printf '%s' \"$COMMAND\" | grep -qE 'git\\s+push\\s+.*(-f\\b|--force\\b|--force-with-lease\\b)'; then\n echo \"BLOCKED: Force push detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Force push rewrites remote history and can destroy\" >&2\n echo \"other people's work. This is almost never what you want.\" >&2\n echo \"\" >&2\n echo \"If you truly need to force push, set CC_ALLOW_FORCE_PUSH=1\" >&2\n exit 2\n fi\nfi\n\n# --- Check 2: Push to protected branches ---\nPROTECTED=\"${CC_PROTECT_BRANCHES:-main:master}\"\n\nBLOCKED=0\nIFS=':' read -ra BRANCHES <<< \"$PROTECTED\"\nfor branch in \"${BRANCHES[@]}\"; do\n if printf '%s' \"$COMMAND\" | grep -qwE \"origin\\s+${branch}|${branch}\\s|${branch}$\"; then\n BLOCKED=1\n break\n fi\ndone\n\nif (( BLOCKED == 1 )); then\n echo \"BLOCKED: Attempted push to protected branch.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Protected branches: $PROTECTED\" >&2\n echo \"\" >&2\n echo \"Push to a feature branch first, then create a pull request.\" >&2\n exit 2\nfi\n\nexit 0\n",
|
|
4
|
+
"syntax-check": "#!/bin/bash\n# ================================================================\n# syntax-check.sh — Automatic Syntax Validation After Edits\n# ================================================================\n# PURPOSE:\n# Runs syntax checks immediately after Claude Code edits or\n# writes a file. Catches syntax errors before they propagate\n# into downstream failures.\n#\n# SUPPORTED LANGUAGES:\n# .py — python -m py_compile\n# .sh — bash -n\n# .bash — bash -n\n# .json — jq empty\n# .yaml — python3 yaml.safe_load (if PyYAML installed)\n# .yml — python3 yaml.safe_load (if PyYAML installed)\n# .js — node --check (if node installed)\n# .ts — npx tsc --noEmit (if tsc available) [EXPERIMENTAL]\n#\n# TRIGGER: PostToolUse\n# MATCHER: \"Edit|Write\"\n#\n# DESIGN PHILOSOPHY:\n# - Never blocks (always exit 0) — reports errors but doesn't\n# prevent the edit from completing\n# - Silent on success — only speaks up when something is wrong\n# - Fails open — if a checker isn't installed, silently skips\n#\n# BORN FROM:\n# Countless sessions where Claude Code introduced a syntax error,\n# continued working for 10+ tool calls, then hit a wall when\n# trying to run the broken file. Catching it immediately saves\n# context window and frustration.\n# ================================================================\n\nINPUT=$(cat)\nFILE_PATH=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null)\n\n# No file path = nothing to check\nif [[ -z \"$FILE_PATH\" || ! -f \"$FILE_PATH\" ]]; then\n exit 0\nfi\n\nEXT=\"${FILE_PATH##*.}\"\n\ncase \"$EXT\" in\n py)\n if python3 -m py_compile \"$FILE_PATH\" 2>&1; then\n : # silent on success\n else\n echo \"SYNTAX ERROR (Python): $FILE_PATH\" >&2\n fi\n ;;\n sh|bash)\n if bash -n \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (Shell): $FILE_PATH\" >&2\n fi\n ;;\n json)\n if command -v jq &>/dev/null; then\n if jq empty \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (JSON): $FILE_PATH\" >&2\n fi\n fi\n ;;\n yaml|yml)\n if python3 -c \"import yaml\" 2>/dev/null; then\n if python3 -c \"\nimport yaml, sys\nwith open(sys.argv[1]) as f:\n yaml.safe_load(f)\n\" \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (YAML): $FILE_PATH\" >&2\n fi\n fi\n ;;\n js)\n if command -v node &>/dev/null; then\n if node --check \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (JavaScript): $FILE_PATH\" >&2\n fi\n fi\n ;;\n ts)\n # EXPERIMENTAL: TypeScript check requires tsc in PATH\n if command -v npx &>/dev/null; then\n if npx tsc --noEmit \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (TypeScript) [experimental]: $FILE_PATH\" >&2\n fi\n fi\n ;;\n *)\n # Unknown extension — skip silently\n ;;\nesac\n\nexit 0\n",
|
|
5
5
|
"context-monitor": "#!/bin/bash\n# ================================================================\n# context-monitor.sh — Context Window Remaining Capacity Monitor\n# ================================================================\n# PURPOSE:\n# Monitors how much context window remains during a Claude Code\n# session. Issues graduated warnings (CAUTION → WARNING → CRITICAL\n# → EMERGENCY) so you never get killed by context exhaustion.\n#\n# HOW IT WORKS:\n# 1. Reads Claude Code's debug log to extract actual token usage\n# 2. Falls back to tool-call-count estimation when debug logs\n# are unavailable\n# 3. Saves current % to /tmp/cc-context-pct (other scripts can\n# read this)\n# 4. At CRITICAL/EMERGENCY, writes an evacuation template to\n# your mission file so you can hand off state before /compact\n#\n# TRIGGER: PostToolUse (all tools)\n# MATCHER: \"\" (empty = every tool invocation)\n#\n# CONFIGURATION:\n# CC_CONTEXT_MISSION_FILE — path to your mission/state file\n# default: $HOME/mission.md\n#\n# THRESHOLDS (edit below to taste):\n# CAUTION = 40% — be mindful of consumption\n# WARNING = 25% — finish current task, save state\n# CRITICAL = 20% — run /compact immediately\n# EMERGENCY = 15% — stop everything, evacuate\n#\n# BORN FROM:\n# A session that hit 3% context remaining with no warning.\n# The agent died mid-task and all in-flight work was lost.\n# Never again.\n# ================================================================\n\nSTATE_FILE=\"/tmp/cc-context-state\"\nPCT_FILE=\"/tmp/cc-context-pct\"\nCOUNTER_FILE=\"/tmp/cc-context-monitor-count\"\nMISSION_FILE=\"${CC_CONTEXT_MISSION_FILE:-$HOME/mission.md}\"\n\n# Tool invocation counter (fallback estimator)\nCOUNT=$(cat \"$COUNTER_FILE\" 2>/dev/null || echo 0)\nCOUNT=$((COUNT + 1))\necho \"$COUNT\" > \"$COUNTER_FILE\"\n\n# Check every 3rd invocation to reduce overhead\n# (but always check in CRITICAL/EMERGENCY state)\nLAST_STATE=$(cat \"$STATE_FILE\" 2>/dev/null || echo \"normal\")\nif [ $((COUNT % 3)) -ne 0 ] && [ \"$LAST_STATE\" != \"critical\" ] && [ \"$LAST_STATE\" != \"emergency\" ]; then\n exit 0\nfi\n\n# --- Extract context % from Claude Code debug logs ---\nget_context_pct() {\n local debug_dir=\"$HOME/.claude/debug\"\n if [ ! -d \"$debug_dir\" ]; then\n echo \"\"\n return\n fi\n\n local latest\n latest=$(find \"$debug_dir\" -maxdepth 1 -name '*.txt' -printf '%T@ %p\\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2)\n if [ -z \"$latest\" ]; then\n echo \"\"\n return\n fi\n\n # Parse the last autocompact entry for token counts\n local line\n line=$(grep 'autocompact:' \"$latest\" 2>/dev/null | tail -1)\n if [ -z \"$line\" ]; then\n echo \"\"\n return\n fi\n\n local tokens window\n tokens=$(echo \"$line\" | sed 's/.*tokens=\\([0-9]*\\).*/\\1/')\n window=$(echo \"$line\" | sed 's/.*effectiveWindow=\\([0-9]*\\).*/\\1/')\n\n if [ -n \"$tokens\" ] && [ -n \"$window\" ] && [ \"$window\" -gt 0 ] 2>/dev/null; then\n local pct\n pct=$(( (window - tokens) * 100 / window ))\n echo \"$pct\"\n else\n echo \"\"\n fi\n}\n\nCONTEXT_PCT=$(get_context_pct)\n\n# Fallback: estimate from tool call count when debug logs unavailable\n# Assumes ~180 tool calls fills ~100% of context (conservative)\nif [ -z \"$CONTEXT_PCT\" ]; then\n CONTEXT_PCT=$(( 100 - (COUNT * 100 / 180) ))\n if [ \"$CONTEXT_PCT\" -lt 0 ]; then CONTEXT_PCT=0; fi\n SOURCE=\"estimate\"\nelse\n SOURCE=\"debug\"\nfi\n\necho \"$CONTEXT_PCT\" > \"$PCT_FILE\"\n\nTIMESTAMP=$(date '+%Y-%m-%d %H:%M')\n\n# --- Evacuation template (with cooldown to prevent spam) ---\nEVAC_COOLDOWN_FILE=\"/tmp/cc-context-evac-last\"\nEVAC_COOLDOWN_SEC=1800 # 30 min cooldown between template generations\n\ngenerate_evacuation_template() {\n local level=\"$1\"\n\n # Cooldown check\n if [ -f \"$EVAC_COOLDOWN_FILE\" ]; then\n local last_ts now_ts diff\n last_ts=$(cat \"$EVAC_COOLDOWN_FILE\" 2>/dev/null || echo 0)\n now_ts=$(date +%s)\n diff=$((now_ts - last_ts))\n if [ \"$diff\" -lt \"$EVAC_COOLDOWN_SEC\" ]; then\n return\n fi\n fi\n\n # Don't add a new template if there's already an unfilled one\n if [ -f \"$MISSION_FILE\" ] && grep -q '\\[TODO\\]' \"$MISSION_FILE\" 2>/dev/null; then\n return\n fi\n\n date +%s > \"$EVAC_COOLDOWN_FILE\"\n\n # Create mission file directory if needed\n mkdir -p \"$(dirname \"$MISSION_FILE\")\"\n\n cat >> \"$MISSION_FILE\" << EVAC_EOF\n\n## Context Evacuation Template (${level} - ${TIMESTAMP})\n<!-- Auto-generated by context-monitor.sh. Fill in before /compact -->\n### Current Task\n- Task: [TODO]\n- Progress: [TODO]\n- Files being edited: [TODO]\n\n### Git State\n- Branch: [TODO]\n- Uncommitted changes: [TODO]\n\n### Next Action\n- Next command/action: [TODO]\nEVAC_EOF\n}\n\n# --- Graduated warnings ---\nif [ \"$CONTEXT_PCT\" -le 15 ]; then\n # EMERGENCY\n if [ \"$LAST_STATE\" != \"emergency\" ]; then\n echo \"emergency\" > \"$STATE_FILE\"\n generate_evacuation_template \"EMERGENCY\"\n fi\n echo \"\"\n echo \"EMERGENCY: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Run /compact IMMEDIATELY. Evacuation template written to ${MISSION_FILE}.\"\n echo \"1. Fill in the [TODO] fields in the template\"\n echo \"2. Run /compact\"\n echo \"3. If needed, restart and resume from mission file\"\n echo \"No further work allowed. Evacuate only.\"\n\nelif [ \"$CONTEXT_PCT\" -le 20 ]; then\n # CRITICAL\n if [ \"$LAST_STATE\" != \"critical\" ]; then\n echo \"critical\" > \"$STATE_FILE\"\n generate_evacuation_template \"CRITICAL\"\n fi\n echo \"\"\n echo \"CRITICAL: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Run /compact IMMEDIATELY. Evacuation template written to ${MISSION_FILE}.\"\n echo \"1. Save current task state to the template\"\n echo \"2. Run /compact\"\n\nelif [ \"$CONTEXT_PCT\" -le 25 ]; then\n # WARNING\n if [ \"$LAST_STATE\" != \"warning\" ]; then\n echo \"warning\" > \"$STATE_FILE\"\n echo \"\"\n echo \"WARNING: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Do not start new large tasks. Finish current work and save state.\"\n fi\n\nelif [ \"$CONTEXT_PCT\" -le 40 ]; then\n # CAUTION\n if [ \"$LAST_STATE\" != \"caution\" ]; then\n echo \"caution\" > \"$STATE_FILE\"\n echo \"\"\n echo \"CAUTION: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Be mindful of context consumption. Keep interactions concise.\"\n fi\nfi\n\nexit 0\n",
|
|
6
|
-
"comment-strip": "#!/bin/bash\n# ================================================================\n# comment-strip.sh — Strip bash comments that break permissions\n# ================================================================\n# PURPOSE:\n# Claude Code sometimes adds comments to bash commands like:\n# # Check the diff\n# git diff HEAD~1\n# This breaks permission allowlists (e.g. Bash(git:*)) because\n# the matcher sees \"# Check the diff\" instead of \"git diff\".\n#\n# This hook strips leading comment lines and returns the clean\n# command via updatedInput, so permissions match correctly.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# INCIDENT: GitHub Issue #29582 (18 reactions)\n# Users on linux/vscode report that bash comments added by Claude\n# cause permission prompts even when the command is allowlisted.\n#\n# HOW IT WORKS:\n# - Reads the command from tool_input\n# - Strips leading lines that start with #\n# - Strips trailing comments (everything after # on command lines)\n# - Returns updatedInput with the cleaned command\n# - Uses hookSpecificOutput.permissionDecision = \"allow\" only if\n# the command was modified (so it doesn't override other hooks)\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(
|
|
7
|
-
"cd-git-allow": "#!/bin/bash\n# ================================================================\n# cd-git-allow.sh — Auto-approve cd+git compound commands\n# ================================================================\n# PURPOSE:\n# Claude Code shows \"Compound commands with cd and git require\n# approval\" for commands like: cd /path && git log\n# This is safe in trusted project directories but causes\n# constant permission prompts.\n#\n# This hook auto-approves cd+git compounds when the git operation\n# is read-only (log, diff, status, branch, show, etc.)\n# Destructive git operations (push, reset, clean) are NOT\n# auto-approved — they still require manual approval.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# INCIDENT: GitHub Issue #32985 (9 reactions)\n#\n# WHAT IT AUTO-APPROVES:\n# - cd /path && git log\n# - cd /path && git diff\n# - cd /path && git status\n# - cd /path && git branch\n# - cd /path && git show\n# - cd /path && git rev-parse\n#\n# WHAT IT DOES NOT APPROVE (still prompts):\n# - cd /path && git push\n# - cd /path && git reset --hard\n# - cd /path && git clean\n# - cd /path && git checkout (could discard changes)\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(
|
|
8
|
-
"secret-guard": "#!/bin/bash\n# ================================================================\n# secret-guard.sh — Secret/Credential Leak Prevention\n# ================================================================\n# PURPOSE:\n# Prevents accidental exposure of secrets, API keys, and\n# credentials through git commits or shell output.\n#\n# Catches the most common ways secrets leak:\n# - git add .env (committing env files)\n# - git add credentials.json / *.pem / *.key\n# - echo $API_KEY or printenv (exposing secrets in output)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - git add .env / .env.local / .env.production\n# - git add *credentials* / *secret* / *.pem / *.key\n# - git add -A or git add . when .env exists (warns)\n#\n# WHAT IT ALLOWS (exit 0):\n# - git add specific safe files\n# - Reading .env for application use (not committing)\n# - All non-git-add commands\n#\n# CONFIGURATION:\n# CC_SECRET_PATTERNS — colon-separated additional patterns to block\n# default: \".env:.env.local:.env.production:credentials:secret:*.pem:*.key:*.p12\"\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(
|
|
9
|
-
"api-error-alert": "#!/bin/bash\nINPUT=$(cat)\nREASON=$(
|
|
10
|
-
"clear-command-confirm-guard": "#!/bin/bash\n# clear-command-confirm-guard.sh — Block accidental /clear command\n#\n# Solves: /clear destroys all conversation context with zero\n# confirmation. Prefix matching means /c + Enter can\n# accidentally trigger /clear instead of /commit or /compact (#40931).\n#\n# How it works: Blocks /clear entirely. Use /compact to reduce\n# context without losing it.\n#\n# TRIGGER: UserPromptSubmit\n# MATCHER: \"^/clear$\"\n\nINPUT=$(cat)\nPROMPT=$(
|
|
6
|
+
"comment-strip": "#!/bin/bash\n# ================================================================\n# comment-strip.sh — Strip bash comments that break permissions\n# ================================================================\n# PURPOSE:\n# Claude Code sometimes adds comments to bash commands like:\n# # Check the diff\n# git diff HEAD~1\n# This breaks permission allowlists (e.g. Bash(git:*)) because\n# the matcher sees \"# Check the diff\" instead of \"git diff\".\n#\n# This hook strips leading comment lines and returns the clean\n# command via updatedInput, so permissions match correctly.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# INCIDENT: GitHub Issue #29582 (18 reactions)\n# Users on linux/vscode report that bash comments added by Claude\n# cause permission prompts even when the command is allowlisted.\n#\n# HOW IT WORKS:\n# - Reads the command from tool_input\n# - Strips leading lines that start with #\n# - Strips trailing comments (everything after # on command lines)\n# - Returns updatedInput with the cleaned command\n# - Uses hookSpecificOutput.permissionDecision = \"allow\" only if\n# the command was modified (so it doesn't override other hooks)\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Strip leading comment lines and empty lines\nCLEAN=$(printf '%s' \"$COMMAND\" | sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d')\n\n# If nothing changed, pass through\nif [[ \"$CLEAN\" == \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# If command is empty after stripping, don't modify\nif [[ -z \"$CLEAN\" ]]; then\n exit 0\nfi\n\n# Return cleaned command via hookSpecificOutput\n# permissionDecision is not set — let the normal permission flow handle it\n# We only modify the input so the permission matcher sees the real command\njq -n --arg cmd \"$CLEAN\" '{\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n updatedInput: {\n command: $cmd\n }\n }\n}'\n",
|
|
7
|
+
"cd-git-allow": "#!/bin/bash\n# ================================================================\n# cd-git-allow.sh — Auto-approve cd+git compound commands\n# ================================================================\n# PURPOSE:\n# Claude Code shows \"Compound commands with cd and git require\n# approval\" for commands like: cd /path && git log\n# This is safe in trusted project directories but causes\n# constant permission prompts.\n#\n# This hook auto-approves cd+git compounds when the git operation\n# is read-only (log, diff, status, branch, show, etc.)\n# Destructive git operations (push, reset, clean) are NOT\n# auto-approved — they still require manual approval.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# INCIDENT: GitHub Issue #32985 (9 reactions)\n#\n# WHAT IT AUTO-APPROVES:\n# - cd /path && git log\n# - cd /path && git diff\n# - cd /path && git status\n# - cd /path && git branch\n# - cd /path && git show\n# - cd /path && git rev-parse\n#\n# WHAT IT DOES NOT APPROVE (still prompts):\n# - cd /path && git push\n# - cd /path && git reset --hard\n# - cd /path && git clean\n# - cd /path && git checkout (could discard changes)\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Only handle cd + git compounds\nif ! printf '%s' \"$COMMAND\" | grep -qE '^\\s*cd\\s+.*&&\\s*git\\s'; then\n exit 0\nfi\n\n# Extract the git subcommand\nGIT_CMD=$(printf '%s' \"$COMMAND\" | grep -oP '&&\\s*git\\s+\\K\\S+')\n\n# Read-only git operations — safe to auto-approve\nSAFE_GIT=\"log diff status branch show rev-parse tag remote stash-list describe name-rev\"\n\nfor safe in $SAFE_GIT; do\n if [[ \"$GIT_CMD\" == \"$safe\" ]]; then\n jq -n '{\n hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"allow\",\n permissionDecisionReason: \"cd+git compound auto-approved (read-only git operation)\"\n }\n }'\n exit 0\n fi\ndone\n\n# Not a read-only git op — let normal permission flow handle it\nexit 0\n",
|
|
8
|
+
"secret-guard": "#!/bin/bash\n# ================================================================\n# secret-guard.sh — Secret/Credential Leak Prevention\n# ================================================================\n# PURPOSE:\n# Prevents accidental exposure of secrets, API keys, and\n# credentials through git commits or shell output.\n#\n# Catches the most common ways secrets leak:\n# - git add .env (committing env files)\n# - git add credentials.json / *.pem / *.key\n# - echo $API_KEY or printenv (exposing secrets in output)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - git add .env / .env.local / .env.production\n# - git add *credentials* / *secret* / *.pem / *.key\n# - git add -A or git add . when .env exists (warns)\n#\n# WHAT IT ALLOWS (exit 0):\n# - git add specific safe files\n# - Reading .env for application use (not committing)\n# - All non-git-add commands\n#\n# CONFIGURATION:\n# CC_SECRET_PATTERNS — colon-separated additional patterns to block\n# default: \".env:.env.local:.env.production:credentials:secret:*.pem:*.key:*.p12\"\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null)\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# --- Check 1: git add of secret files ---\nif printf '%s' \"$COMMAND\" | grep -qE '^\\s*git\\s+add'; then\n # Direct .env file staging\n if printf '%s' \"$COMMAND\" | grep -qiE 'git\\s+add\\s+.*\\.env(\\s|$|\\.|/)'; then\n echo \"BLOCKED: Attempted to stage .env file.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \".env files contain secrets and should never be committed.\" >&2\n echo \"Add .env to .gitignore instead.\" >&2\n exit 2\n fi\n\n # Credential/key files\n if printf '%s' \"$COMMAND\" | grep -qiE 'git\\s+add\\s+.*(credentials|\\.pem|\\.key|\\.p12|\\.pfx|id_rsa|id_ed25519)'; then\n echo \"BLOCKED: Attempted to stage credential/key file.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Key and credential files should never be committed to git.\" >&2\n echo \"Add them to .gitignore instead.\" >&2\n exit 2\n fi\n\n # git add -A or git add . when .env exists — warn but check\n if printf '%s' \"$COMMAND\" | grep -qE 'git\\s+add\\s+(-A|--all|\\.)(\\s|$)'; then\n # Check if .env exists in the current or project directory\n if [ -f \".env\" ] || [ -f \".env.local\" ] || [ -f \".env.production\" ]; then\n echo \"BLOCKED: 'git add .' with .env file present.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"An .env file exists in this directory. 'git add .' would stage it.\" >&2\n echo \"Add specific files instead: git add src/ lib/ package.json\" >&2\n echo \"Or add .env to .gitignore first.\" >&2\n exit 2\n fi\n fi\nfi\n\nexit 0\n",
|
|
9
|
+
"api-error-alert": "#!/bin/bash\nINPUT=$(cat)\nREASON=$(printf '%s' \"$INPUT\" | jq -r '.stop_reason // \"unknown\"' 2>/dev/null)\nHOOK_EVENT=$(printf '%s' \"$INPUT\" | jq -r '.hook_event_name // \"\"' 2>/dev/null)\nif [[ \"$REASON\" == \"user\" || \"$REASON\" == \"normal\" || -z \"$REASON\" ]]; then\n exit 0\nfi\nLOG=\"${CC_ERROR_ALERT_LOG:-$HOME/.claude/session-errors.log}\"\nMISSION=\"${CC_CONTEXT_MISSION_FILE:-$HOME/mission.md}\"\nTS=$(date -Iseconds)\nmkdir -p \"$(dirname \"$LOG\")\" 2>/dev/null\necho \"[$TS] Session stopped: reason=$REASON event=$HOOK_EVENT\" >> \"$LOG\"\nif [ -z \"$WSL_DISTRO_NAME\" ]; then\n notify-send \"Claude Code\" \"Session stopped: $REASON\" 2>/dev/null || true\n osascript -e \"display notification \\\"Session stopped: $REASON\\\" with title \\\"Claude Code\\\"\" 2>/dev/null || true\nelse\n powershell.exe -Command \"Write-Host 'Claude Code: Session stopped - $REASON'\" 2>/dev/null || true\nfi\nexit 0\n",
|
|
10
|
+
"clear-command-confirm-guard": "#!/bin/bash\n# clear-command-confirm-guard.sh — Block accidental /clear command\n#\n# Solves: /clear destroys all conversation context with zero\n# confirmation. Prefix matching means /c + Enter can\n# accidentally trigger /clear instead of /commit or /compact (#40931).\n#\n# How it works: Blocks /clear entirely. Use /compact to reduce\n# context without losing it.\n#\n# TRIGGER: UserPromptSubmit\n# MATCHER: \"^/clear$\"\n\nINPUT=$(cat)\nPROMPT=$(printf '%s' \"$INPUT\" | jq -r '.prompt // empty' 2>/dev/null)\n\nif printf '%s' \"$PROMPT\" | grep -qE '^/clear$'; then\n echo \"BLOCKED: /clear permanently destroys all context. Use /compact instead to reduce context safely.\" >&2\n exit 2\nfi\nexit 0\n",
|
|
11
11
|
"claudemd-violation-detector": "#!/bin/bash\n# claudemd-violation-detector.sh — Remind critical CLAUDE.md rules after tool use\n#\n# Solves: Claude ignores CLAUDE.md instructions, especially after\n# context compaction or in long sessions (#40930).\n#\n# How it works: After each tool use, extracts and prints\n# critical rules (ABSOLUTE/MUST NEVER/NEVER/禁止) from CLAUDE.md\n# as a reminder. Runs every N tool calls to avoid noise.\n#\n# TRIGGER: PostToolUse\n# MATCHER: \"\"\n\nset -euo pipefail\n\n# Rate limit: only remind every 20 tool calls\nCOUNTER_FILE=\"/tmp/claudemd-reminder-counter\"\nCOUNT=$(cat \"$COUNTER_FILE\" 2>/dev/null || echo \"0\")\nCOUNT=$((COUNT + 1))\necho \"$COUNT\" > \"$COUNTER_FILE\"\n[ $((COUNT % 20)) -ne 0 ] && exit 0\n\n# Find CLAUDE.md\nCLAUDEMD=\"\"\nfor candidate in \"CLAUDE.md\" \".claude/CLAUDE.md\" \"../CLAUDE.md\"; do\n [ -f \"$candidate\" ] && CLAUDEMD=\"$candidate\" && break\ndone\n[ -z \"$CLAUDEMD\" ] && exit 0\n\n# Extract critical rules\nRULES=$(grep -iE '(ABSOLUTE|MUST NEVER|NEVER DO|禁止|絶対)' \"$CLAUDEMD\" 2>/dev/null | head -5 || true)\n[ -z \"$RULES\" ] && exit 0\n\necho \"📋 CLAUDE.md critical rules reminder:\" >&2\necho \"$RULES\" >&2\nexit 0\n",
|
|
12
|
-
"subagent-context-size-guard": "#!/bin/bash\n# subagent-context-size-guard.sh — Warn on thin subagent prompts\n#\n# Solves: Subagents get spawned with minimal context, leading to\n# poor results because they lack necessary background (#40929).\n# The parent agent assumes shared context, but each subagent\n# starts fresh.\n#\n# How it works: Checks Agent tool's prompt parameter length.\n# If under 100 characters, warns that the prompt may be too thin\n# for a standalone agent to work effectively.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Agent\"\n\nset -euo pipefail\nINPUT=$(cat)\nPROMPT=$(
|
|
12
|
+
"subagent-context-size-guard": "#!/bin/bash\n# subagent-context-size-guard.sh — Warn on thin subagent prompts\n#\n# Solves: Subagents get spawned with minimal context, leading to\n# poor results because they lack necessary background (#40929).\n# The parent agent assumes shared context, but each subagent\n# starts fresh.\n#\n# How it works: Checks Agent tool's prompt parameter length.\n# If under 100 characters, warns that the prompt may be too thin\n# for a standalone agent to work effectively.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Agent\"\n\nset -euo pipefail\nINPUT=$(cat)\nPROMPT=$(printf '%s' \"$INPUT\" | jq -r '.tool_input.prompt // empty' 2>/dev/null)\n\n[ -z \"$PROMPT\" ] && exit 0\n\nLEN=${#PROMPT}\nif [ \"$LEN\" -lt 100 ]; then\n echo \"WARNING: Agent prompt is only ${LEN} chars. Subagents start with zero context — include enough background for them to work independently.\" >&2\nfi\nexit 0\n"
|
|
13
13
|
}
|