cc-safe-setup 29.6.0 → 29.6.2
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/COOKBOOK.md +70 -0
- package/README.md +43 -4
- package/TROUBLESHOOTING.md +30 -0
- package/examples/api-rate-limit-tracker.sh +51 -0
- package/examples/auto-answer-question.sh +67 -0
- package/examples/auto-approve-readonly-tools.sh +10 -0
- package/examples/aws-production-guard.sh +40 -0
- package/examples/banned-command-guard.sh +48 -0
- package/examples/bash-heuristic-approver.sh +59 -0
- package/examples/bash-trace-guard.sh +48 -0
- package/examples/block-database-wipe.sh +1 -1
- package/examples/classifier-fallback-allow.sh +70 -0
- package/examples/commit-message-check.sh +8 -1
- package/examples/commit-message-quality.sh +35 -0
- package/examples/credential-exfil-guard.sh +12 -0
- package/examples/cwd-reminder.sh +37 -0
- package/examples/dependency-install-guard.sh +84 -0
- package/examples/deploy-guard.sh +1 -1
- package/examples/detect-mixed-indentation.sh +33 -0
- package/examples/disk-space-check.sh +42 -0
- package/examples/docker-dangerous-guard.sh +47 -0
- package/examples/dockerfile-lint.sh +58 -0
- package/examples/edit-always-allow.sh +53 -0
- package/examples/env-file-gitignore-check.sh +39 -0
- package/examples/env-source-guard.sh +1 -1
- package/examples/git-stash-before-danger.sh +58 -0
- package/examples/github-actions-guard.sh +49 -0
- package/examples/gitignore-auto-add.sh +30 -0
- package/examples/go-vet-after-edit.sh +33 -0
- package/examples/hook-tamper-guard.sh +67 -0
- package/examples/kubernetes-guard.sh +2 -1
- package/examples/large-file-write-guard.sh +40 -0
- package/examples/main-branch-warn.sh +40 -0
- package/examples/max-edit-size-guard.sh +9 -15
- package/examples/mcp-server-guard.sh +70 -0
- package/examples/multiline-command-approver.sh +89 -0
- package/examples/no-base64-exfil.sh +27 -0
- package/examples/no-debug-commit.sh +60 -0
- package/examples/no-exposed-port-in-dockerfile.sh +32 -0
- package/examples/no-fixme-ship.sh +41 -0
- package/examples/no-hardcoded-ip.sh +26 -0
- package/examples/no-http-in-code.sh +19 -0
- package/examples/no-push-without-tests.sh +33 -0
- package/examples/no-self-signed-cert.sh +19 -0
- package/examples/no-star-import-python.sh +28 -0
- package/examples/no-wget-piped-bash.sh +22 -0
- package/examples/node-version-check.sh +40 -0
- package/examples/npm-publish-guard.sh +5 -2
- package/examples/output-token-env-check.sh +44 -0
- package/examples/package-lock-frozen.sh +25 -0
- package/examples/pip-venv-required.sh +40 -0
- package/examples/port-conflict-check.sh +62 -0
- package/examples/post-compact-safety.sh +61 -0
- package/examples/prefer-builtin-tools.sh +33 -0
- package/examples/python-import-check.sh +52 -0
- package/examples/python-ruff-on-edit.sh +51 -0
- package/examples/quoted-flag-approver.sh +51 -0
- package/examples/react-key-warn.sh +32 -0
- package/examples/read-budget-guard.sh +63 -0
- package/examples/rm-safety-net.sh +9 -0
- package/examples/rust-clippy-after-edit.sh +37 -0
- package/examples/session-drift-guard.sh +73 -0
- package/examples/session-quota-tracker.sh +44 -0
- package/examples/session-start-safety-check.sh +60 -0
- package/examples/session-summary-stop.sh +49 -0
- package/examples/session-time-limit.sh +34 -0
- package/examples/strip-coauthored-by.sh +46 -0
- package/examples/temp-file-cleanup.sh +41 -0
- package/examples/test-before-push.sh +8 -1
- package/examples/test-coverage-reminder.sh +49 -0
- package/examples/test-exit-code-verify.sh +60 -0
- package/examples/tool-file-logger.sh +46 -0
- package/examples/typescript-lint-on-edit.sh +61 -0
- package/examples/typescript-strict-check.sh +35 -0
- package/examples/uncommitted-changes-stop.sh +16 -0
- package/examples/uncommitted-discard-guard.sh +72 -0
- package/examples/variable-expansion-guard.sh +51 -0
- package/examples/worktree-unmerged-guard.sh +13 -3
- package/examples/yaml-syntax-check.sh +50 -0
- package/index.mjs +9 -0
- package/package.json +2 -2
|
@@ -70,4 +70,16 @@ if echo "$COMMAND" | grep -qE '^\s*(env|printenv|set)\s*$'; then
|
|
|
70
70
|
exit 0
|
|
71
71
|
fi
|
|
72
72
|
|
|
73
|
+
# Pattern 8: curl/wget posting credential files
|
|
74
|
+
if echo "$COMMAND" | grep -qiP 'curl\s.*-d\s+@[^\s]*(\.env|\.pem|\.key|credentials|\.ssh/id_)|wget\s.*--post-file[= ][^\s]*(\.env|\.pem|\.key|credentials|\.ssh/id_)'; then
|
|
75
|
+
echo "BLOCKED: Credential file exfiltration via HTTP upload" >&2
|
|
76
|
+
exit 2
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Pattern 9: Piping credential files to curl/wget
|
|
80
|
+
if echo "$COMMAND" | grep -qiP 'cat\s+[^\s]*(\.env|\.pem|\.key|credentials|\.ssh/id_)\S*\s*\|.*curl|cat\s+[^\s]*(\.env|\.pem|\.key|credentials|\.ssh/id_)\S*\s*\|.*wget'; then
|
|
81
|
+
echo "BLOCKED: Credential file piped to HTTP client" >&2
|
|
82
|
+
exit 2
|
|
83
|
+
fi
|
|
84
|
+
|
|
73
85
|
exit 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# cwd-reminder.sh — Remind Claude of the current working directory
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude loses track of which directory it's in (#1669 — 71 reactions)
|
|
5
|
+
# Can lead to commands running in wrong directory, including
|
|
6
|
+
# destructive operations like git reset in the wrong repo.
|
|
7
|
+
#
|
|
8
|
+
# Emits the current working directory to stderr before every Bash command,
|
|
9
|
+
# making it visible in the tool output so Claude always knows where it is.
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: PreToolUse
|
|
12
|
+
# MATCHER: "Bash"
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# {
|
|
16
|
+
# "hooks": {
|
|
17
|
+
# "PreToolUse": [{
|
|
18
|
+
# "matcher": "Bash",
|
|
19
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/cwd-reminder.sh" }]
|
|
20
|
+
# }]
|
|
21
|
+
# }
|
|
22
|
+
# }
|
|
23
|
+
|
|
24
|
+
INPUT=$(cat)
|
|
25
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
26
|
+
|
|
27
|
+
# Don't add noise to empty commands
|
|
28
|
+
[ -z "$COMMAND" ] && exit 0
|
|
29
|
+
|
|
30
|
+
# Get the working directory from the tool input if available,
|
|
31
|
+
# otherwise use the process's cwd
|
|
32
|
+
CWD=$(echo "$INPUT" | jq -r '.tool_input.working_directory // empty' 2>/dev/null)
|
|
33
|
+
[ -z "$CWD" ] && CWD=$(pwd)
|
|
34
|
+
|
|
35
|
+
echo "[cwd: $CWD]" >&2
|
|
36
|
+
|
|
37
|
+
exit 0
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# dependency-install-guard.sh — PreToolUse hook
|
|
3
|
+
# Trigger: PreToolUse
|
|
4
|
+
# Matcher: Bash
|
|
5
|
+
#
|
|
6
|
+
# Blocks unintended dependency installations (npm install, pip install,
|
|
7
|
+
# gem install, cargo add, go get). Prevents:
|
|
8
|
+
# - Supply chain attacks from unknown packages
|
|
9
|
+
# - Dependency bloat from unnecessary installations
|
|
10
|
+
# - Breaking lockfiles with unplanned additions
|
|
11
|
+
#
|
|
12
|
+
# Allowed:
|
|
13
|
+
# - npm install (no args) — installs from existing lockfile
|
|
14
|
+
# - npm ci — clean install from lockfile
|
|
15
|
+
# - pip install -r requirements.txt — from requirements file
|
|
16
|
+
# - Packages in ALLOWLIST (customize below)
|
|
17
|
+
#
|
|
18
|
+
# Usage: Add to settings.json as a PreToolUse hook on "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
|
+
# Normalize: collapse whitespace, extract first logical command
|
|
25
|
+
CMD=$(echo "$COMMAND" | tr '\n' ' ' | sed 's/ */ /g')
|
|
26
|
+
|
|
27
|
+
# --- Allowlist: packages you trust ---
|
|
28
|
+
# Customize this list for your project
|
|
29
|
+
ALLOWLIST="typescript|eslint|prettier|jest|vitest|@types/"
|
|
30
|
+
|
|
31
|
+
# npm install <package> — block unless allowlisted
|
|
32
|
+
if echo "$CMD" | grep -qiE 'npm\s+(install|i|add)\s+[a-z@]'; then
|
|
33
|
+
PKG=$(echo "$CMD" | grep -oiE 'npm\s+(install|i|add)\s+\S+' | awk '{print $NF}')
|
|
34
|
+
if echo "$PKG" | grep -qiE "^($ALLOWLIST)"; then
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
echo "🚫 Blocked: npm install $PKG (not in allowlist)" >&2
|
|
38
|
+
echo "Add to ALLOWLIST in dependency-install-guard.sh if intended." >&2
|
|
39
|
+
exit 2
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# npm install (no args) / npm ci — allowed (uses lockfile)
|
|
43
|
+
if echo "$CMD" | grep -qiE 'npm\s+(install|i|ci)\s*($|[&|;])'; then
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# pip install <package> — block unless from requirements
|
|
48
|
+
if echo "$CMD" | grep -qiE 'pip3?\s+install\s+'; then
|
|
49
|
+
# Allow: pip install -r requirements.txt
|
|
50
|
+
if echo "$CMD" | grep -qiE 'pip3?\s+install\s+(-r|--requirement)\s+'; then
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
# Allow: pip install -e . (editable install)
|
|
54
|
+
if echo "$CMD" | grep -qiE 'pip3?\s+install\s+(-e|--editable)\s+'; then
|
|
55
|
+
exit 0
|
|
56
|
+
fi
|
|
57
|
+
PKG=$(echo "$CMD" | grep -oiE 'pip3?\s+install\s+\S+' | awk '{print $NF}')
|
|
58
|
+
echo "🚫 Blocked: pip install $PKG" >&2
|
|
59
|
+
echo "Use 'pip install -r requirements.txt' or add to allowlist." >&2
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# gem install — block
|
|
64
|
+
if echo "$CMD" | grep -qiE 'gem\s+install\s+[a-z]'; then
|
|
65
|
+
PKG=$(echo "$CMD" | grep -oiE 'gem\s+install\s+\S+' | awk '{print $NF}')
|
|
66
|
+
echo "🚫 Blocked: gem install $PKG" >&2
|
|
67
|
+
exit 2
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# cargo add — block
|
|
71
|
+
if echo "$CMD" | grep -qiE 'cargo\s+add\s+[a-z]'; then
|
|
72
|
+
PKG=$(echo "$CMD" | grep -oiE 'cargo\s+add\s+\S+' | awk '{print $NF}')
|
|
73
|
+
echo "🚫 Blocked: cargo add $PKG" >&2
|
|
74
|
+
exit 2
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# go get — block
|
|
78
|
+
if echo "$CMD" | grep -qiE 'go\s+get\s+[a-z]'; then
|
|
79
|
+
PKG=$(echo "$CMD" | grep -oiE 'go\s+get\s+\S+' | awk '{print $NF}')
|
|
80
|
+
echo "🚫 Blocked: go get $PKG" >&2
|
|
81
|
+
exit 2
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
exit 0
|
package/examples/deploy-guard.sh
CHANGED
|
@@ -24,7 +24,7 @@ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
|
24
24
|
[[ -z "$COMMAND" ]] && exit 0
|
|
25
25
|
|
|
26
26
|
# Detect deploy commands
|
|
27
|
-
if ! echo "$COMMAND" | grep -qiE '(rsync|scp|deploy|firebase\s+deploy|vercel|netlify\s+deploy|fly\s+deploy|railway\s+up|git\s+push\s+heroku)'; then
|
|
27
|
+
if ! echo "$COMMAND" | grep -qiE '(rsync|scp|deploy|firebase\s+deploy|vercel|netlify\s+deploy|fly\s+deploy|railway\s+up|git\s+push\s+heroku|kubectl\s+(apply|create|delete|rollout)|terraform\s+(apply|destroy))'; then
|
|
28
28
|
exit 0
|
|
29
29
|
fi
|
|
30
30
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# detect-mixed-indentation.sh — Warn about mixed tabs/spaces
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Indentation errors from mixing tabs and spaces.
|
|
5
|
+
# Common when Claude pastes code from different sources.
|
|
6
|
+
#
|
|
7
|
+
# TRIGGER: PostToolUse
|
|
8
|
+
# MATCHER: "Write|Edit"
|
|
9
|
+
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
12
|
+
[ -z "$FILE" ] && exit 0
|
|
13
|
+
[ ! -f "$FILE" ] && exit 0
|
|
14
|
+
|
|
15
|
+
# Skip binary files and makefiles (which require tabs)
|
|
16
|
+
case "$(basename "$FILE")" in
|
|
17
|
+
Makefile|makefile|GNUmakefile) exit 0 ;;
|
|
18
|
+
esac
|
|
19
|
+
|
|
20
|
+
case "$FILE" in
|
|
21
|
+
*.py|*.js|*.ts|*.tsx|*.jsx|*.yaml|*.yml|*.rb|*.go) ;;
|
|
22
|
+
*) exit 0 ;;
|
|
23
|
+
esac
|
|
24
|
+
|
|
25
|
+
HAS_TABS=$(grep -cP '^\t' "$FILE" 2>/dev/null || echo 0)
|
|
26
|
+
HAS_SPACES=$(grep -cP '^ {2,}' "$FILE" 2>/dev/null || echo 0)
|
|
27
|
+
|
|
28
|
+
if [ "$HAS_TABS" -gt 0 ] && [ "$HAS_SPACES" -gt 0 ]; then
|
|
29
|
+
echo "WARNING: Mixed tabs and spaces in $FILE ($HAS_TABS tab-lines, $HAS_SPACES space-lines)." >&2
|
|
30
|
+
echo " Standardize to one indentation style." >&2
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
exit 0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# disk-space-check.sh — Warn if disk space is low at session start
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Autonomous sessions crashing due to disk full
|
|
5
|
+
# npm install, git operations, and file writes fail silently
|
|
6
|
+
# when disk space runs out during long-running sessions.
|
|
7
|
+
#
|
|
8
|
+
# Checks: root filesystem usage percentage
|
|
9
|
+
# Warns at: 80% (yellow), 90% (red), 95% (critical)
|
|
10
|
+
#
|
|
11
|
+
# TRIGGER: Notification
|
|
12
|
+
# MATCHER: ""
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# {
|
|
16
|
+
# "hooks": {
|
|
17
|
+
# "Notification": [{
|
|
18
|
+
# "matcher": "",
|
|
19
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/disk-space-check.sh" }]
|
|
20
|
+
# }]
|
|
21
|
+
# }
|
|
22
|
+
# }
|
|
23
|
+
|
|
24
|
+
# Only run once per session
|
|
25
|
+
MARKER="/tmp/cc-disk-check-$$"
|
|
26
|
+
[ -f "$MARKER" ] && exit 0
|
|
27
|
+
|
|
28
|
+
# Get disk usage percentage for root filesystem
|
|
29
|
+
USAGE=$(df / 2>/dev/null | awk 'NR==2 {gsub(/%/,""); print $5}')
|
|
30
|
+
[ -z "$USAGE" ] && exit 0
|
|
31
|
+
|
|
32
|
+
if [ "$USAGE" -ge 95 ]; then
|
|
33
|
+
echo "CRITICAL: Disk usage at ${USAGE}%. Operations may fail." >&2
|
|
34
|
+
echo " Free space immediately: docker system prune, rm tmp files" >&2
|
|
35
|
+
elif [ "$USAGE" -ge 90 ]; then
|
|
36
|
+
echo "WARNING: Disk usage at ${USAGE}%. Consider freeing space." >&2
|
|
37
|
+
elif [ "$USAGE" -ge 80 ]; then
|
|
38
|
+
echo "NOTE: Disk usage at ${USAGE}%." >&2
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
touch "$MARKER"
|
|
42
|
+
exit 0
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# docker-dangerous-guard.sh — Block dangerous Docker operations
|
|
3
|
+
#
|
|
4
|
+
# Prevents: docker system prune -a, docker rm -f on running containers,
|
|
5
|
+
# docker run --privileged, docker exec as root on production containers.
|
|
6
|
+
#
|
|
7
|
+
# TRIGGER: PreToolUse
|
|
8
|
+
# MATCHER: "Bash"
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# {
|
|
12
|
+
# "hooks": {
|
|
13
|
+
# "PreToolUse": [{
|
|
14
|
+
# "matcher": "Bash",
|
|
15
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/docker-dangerous-guard.sh" }]
|
|
16
|
+
# }]
|
|
17
|
+
# }
|
|
18
|
+
# }
|
|
19
|
+
|
|
20
|
+
INPUT=$(cat)
|
|
21
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
22
|
+
[ -z "$COMMAND" ] && exit 0
|
|
23
|
+
|
|
24
|
+
# Block docker system prune -a (removes all images)
|
|
25
|
+
if echo "$COMMAND" | grep -qE 'docker\s+system\s+prune\s+.*-a'; then
|
|
26
|
+
echo "BLOCKED: docker system prune -a removes all unused images." >&2
|
|
27
|
+
echo " Use 'docker system prune' (without -a) to keep tagged images." >&2
|
|
28
|
+
exit 2
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Block docker run --privileged
|
|
32
|
+
if echo "$COMMAND" | grep -qE 'docker\s+run\s+.*--privileged'; then
|
|
33
|
+
echo "BLOCKED: --privileged gives full host access to the container." >&2
|
|
34
|
+
exit 2
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Warn on docker rm -f (force remove)
|
|
38
|
+
if echo "$COMMAND" | grep -qE 'docker\s+(rm|container\s+rm)\s+.*-f'; then
|
|
39
|
+
echo "WARNING: Force-removing container. Data in the container will be lost." >&2
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Block docker run with host network and port 22/80/443
|
|
43
|
+
if echo "$COMMAND" | grep -qE 'docker\s+run.*--network\s+host'; then
|
|
44
|
+
echo "WARNING: --network host exposes all container ports on the host." >&2
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
exit 0
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# dockerfile-lint.sh — Basic Dockerfile validation after editing
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Common Dockerfile mistakes:
|
|
5
|
+
# - Missing FROM instruction
|
|
6
|
+
# - Using latest tag (non-reproducible builds)
|
|
7
|
+
# - Running as root without explicit USER
|
|
8
|
+
# - COPY/ADD before dependency install (cache invalidation)
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PostToolUse
|
|
11
|
+
# MATCHER: "Write|Edit"
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# {
|
|
15
|
+
# "hooks": {
|
|
16
|
+
# "PostToolUse": [{
|
|
17
|
+
# "matcher": "Write|Edit",
|
|
18
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/dockerfile-lint.sh" }]
|
|
19
|
+
# }]
|
|
20
|
+
# }
|
|
21
|
+
# }
|
|
22
|
+
|
|
23
|
+
INPUT=$(cat)
|
|
24
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
25
|
+
[ -z "$FILE" ] && exit 0
|
|
26
|
+
|
|
27
|
+
# Only check Dockerfiles
|
|
28
|
+
BASENAME=$(basename "$FILE")
|
|
29
|
+
case "$BASENAME" in
|
|
30
|
+
Dockerfile|Dockerfile.*|*.dockerfile) ;;
|
|
31
|
+
*) exit 0 ;;
|
|
32
|
+
esac
|
|
33
|
+
|
|
34
|
+
[ ! -f "$FILE" ] && exit 0
|
|
35
|
+
|
|
36
|
+
WARNINGS=""
|
|
37
|
+
|
|
38
|
+
# Check for FROM instruction
|
|
39
|
+
if ! grep -qE '^FROM\s' "$FILE"; then
|
|
40
|
+
WARNINGS="${WARNINGS}\n Missing FROM instruction"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Check for :latest tag
|
|
44
|
+
if grep -qE '^FROM\s+\S+:latest' "$FILE"; then
|
|
45
|
+
WARNINGS="${WARNINGS}\n Using :latest tag (non-reproducible)"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Check for no USER instruction (running as root)
|
|
49
|
+
if ! grep -qE '^USER\s' "$FILE"; then
|
|
50
|
+
WARNINGS="${WARNINGS}\n No USER instruction (container runs as root)"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if [ -n "$WARNINGS" ]; then
|
|
54
|
+
echo "Dockerfile warnings in $FILE:" >&2
|
|
55
|
+
echo -e "$WARNINGS" >&2
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
exit 0
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# edit-always-allow.sh — Auto-approve all Edit prompts in configured directories
|
|
3
|
+
#
|
|
4
|
+
# Solves: --dangerously-skip-permissions doesn't bypass Edit prompts
|
|
5
|
+
# (#36192, #36168 — bypass permissions broken since v2.1.78)
|
|
6
|
+
#
|
|
7
|
+
# Claude Code v2.1.78+ prompts for Edit in .claude/, .git/, .vscode/
|
|
8
|
+
# even with bypassPermissions enabled. This PermissionRequest hook
|
|
9
|
+
# restores the pre-v2.1.78 behavior for specified directories.
|
|
10
|
+
#
|
|
11
|
+
# Configure allowed directories via CC_EDIT_ALLOW_DIRS env var:
|
|
12
|
+
# export CC_EDIT_ALLOW_DIRS=".claude/skills:.claude/commands"
|
|
13
|
+
# Default: .claude/skills
|
|
14
|
+
#
|
|
15
|
+
# TRIGGER: PermissionRequest
|
|
16
|
+
# MATCHER: "Edit|Write"
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# {
|
|
20
|
+
# "hooks": {
|
|
21
|
+
# "PermissionRequest": [{
|
|
22
|
+
# "matcher": "Edit|Write",
|
|
23
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/edit-always-allow.sh" }]
|
|
24
|
+
# }]
|
|
25
|
+
# }
|
|
26
|
+
# }
|
|
27
|
+
|
|
28
|
+
INPUT=$(cat)
|
|
29
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
30
|
+
|
|
31
|
+
# Only handle Edit/Write
|
|
32
|
+
case "$TOOL" in
|
|
33
|
+
Edit|Write) ;;
|
|
34
|
+
*) exit 0 ;;
|
|
35
|
+
esac
|
|
36
|
+
|
|
37
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty' 2>/dev/null)
|
|
38
|
+
[ -z "$FILE" ] && exit 0
|
|
39
|
+
|
|
40
|
+
# Configurable allowed directories (colon-separated)
|
|
41
|
+
ALLOW_DIRS="${CC_EDIT_ALLOW_DIRS:-.claude/skills}"
|
|
42
|
+
|
|
43
|
+
# Check if file is in an allowed directory
|
|
44
|
+
IFS=':' read -ra DIRS <<< "$ALLOW_DIRS"
|
|
45
|
+
for dir in "${DIRS[@]}"; do
|
|
46
|
+
if echo "$FILE" | grep -q "$dir"; then
|
|
47
|
+
echo '{"permissionDecision":"allow"}'
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
# Not in allowed directories — let the prompt through
|
|
53
|
+
exit 0
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# env-file-gitignore-check.sh — Warn if .env is not in .gitignore
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Accidental commit of .env files containing secrets.
|
|
5
|
+
# Checks on session start if .env exists but .gitignore
|
|
6
|
+
# doesn't exclude it.
|
|
7
|
+
#
|
|
8
|
+
# TRIGGER: Notification
|
|
9
|
+
# MATCHER: ""
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# {
|
|
13
|
+
# "hooks": {
|
|
14
|
+
# "Notification": [{
|
|
15
|
+
# "matcher": "",
|
|
16
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/env-file-gitignore-check.sh" }]
|
|
17
|
+
# }]
|
|
18
|
+
# }
|
|
19
|
+
# }
|
|
20
|
+
|
|
21
|
+
# Only run once per session
|
|
22
|
+
MARKER="/tmp/cc-env-gitignore-$$"
|
|
23
|
+
[ -f "$MARKER" ] && exit 0
|
|
24
|
+
|
|
25
|
+
# Check if we're in a git repo
|
|
26
|
+
git rev-parse --git-dir >/dev/null 2>&1 || { touch "$MARKER"; exit 0; }
|
|
27
|
+
|
|
28
|
+
# Check if .env exists
|
|
29
|
+
[ -f ".env" ] || { touch "$MARKER"; exit 0; }
|
|
30
|
+
|
|
31
|
+
# Check if .env is in .gitignore
|
|
32
|
+
if ! git check-ignore -q .env 2>/dev/null; then
|
|
33
|
+
echo "WARNING: .env file exists but is not in .gitignore!" >&2
|
|
34
|
+
echo " Add '.env' to .gitignore to prevent accidental commit." >&2
|
|
35
|
+
echo " echo '.env' >> .gitignore" >&2
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
touch "$MARKER"
|
|
39
|
+
exit 0
|
|
@@ -32,7 +32,7 @@ if [[ -z "$COMMAND" ]]; then
|
|
|
32
32
|
fi
|
|
33
33
|
|
|
34
34
|
# Block direct sourcing of .env files
|
|
35
|
-
if echo "$COMMAND" | grep -qE '(source
|
|
35
|
+
if echo "$COMMAND" | grep -qE '(source|\.)\s+\.env'; then
|
|
36
36
|
echo "BLOCKED: Sourcing .env into shell environment." >&2
|
|
37
37
|
echo "Command: $COMMAND" >&2
|
|
38
38
|
echo "" >&2
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# git-stash-before-danger.sh — Auto-stash before risky git operations
|
|
3
|
+
#
|
|
4
|
+
# Solves: Losing uncommitted work when Claude runs git checkout, git reset, or git pull
|
|
5
|
+
# Related: data loss incidents reported in #36339, #37331
|
|
6
|
+
#
|
|
7
|
+
# How it works: PreToolUse hook that auto-runs `git stash push -m "cc-auto-stash"`
|
|
8
|
+
# before destructive git operations. The stash can be recovered with
|
|
9
|
+
# `git stash pop`.
|
|
10
|
+
#
|
|
11
|
+
# Usage: Add to settings.json as a PreToolUse hook
|
|
12
|
+
#
|
|
13
|
+
# {
|
|
14
|
+
# "hooks": {
|
|
15
|
+
# "PreToolUse": [{
|
|
16
|
+
# "matcher": "Bash",
|
|
17
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/git-stash-before-danger.sh" }]
|
|
18
|
+
# }]
|
|
19
|
+
# }
|
|
20
|
+
# }
|
|
21
|
+
|
|
22
|
+
INPUT=$(cat)
|
|
23
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
24
|
+
|
|
25
|
+
[ -z "$COMMAND" ] && exit 0
|
|
26
|
+
|
|
27
|
+
# Only act on risky git operations
|
|
28
|
+
RISKY=false
|
|
29
|
+
if echo "$COMMAND" | grep -qE 'git\s+(checkout|reset|pull|merge|rebase|cherry-pick)\s'; then
|
|
30
|
+
RISKY=true
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
if [ "$RISKY" = false ]; then
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Check if we're in a git repo with uncommitted changes
|
|
38
|
+
if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Check for uncommitted changes
|
|
43
|
+
if git diff --quiet HEAD 2>/dev/null && git diff --cached --quiet 2>/dev/null; then
|
|
44
|
+
# No changes — nothing to stash
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Auto-stash
|
|
49
|
+
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
|
50
|
+
git stash push -m "cc-auto-stash-$TIMESTAMP (before: $COMMAND)" > /dev/null 2>&1
|
|
51
|
+
|
|
52
|
+
if [ $? -eq 0 ]; then
|
|
53
|
+
echo "INFO: Auto-stashed uncommitted changes before risky operation" >&2
|
|
54
|
+
echo "Recovery: git stash pop" >&2
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Don't block — just stash and let the command proceed
|
|
58
|
+
exit 0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# github-actions-guard.sh — Validate GitHub Actions workflow changes
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Broken CI/CD pipelines from workflow syntax errors.
|
|
5
|
+
# Claude sometimes generates invalid workflow YAML.
|
|
6
|
+
#
|
|
7
|
+
# Checks:
|
|
8
|
+
# - Workflow must have 'on' trigger
|
|
9
|
+
# - Job names must exist
|
|
10
|
+
# - 'uses' actions should have version pins
|
|
11
|
+
#
|
|
12
|
+
# TRIGGER: PostToolUse
|
|
13
|
+
# MATCHER: "Write|Edit"
|
|
14
|
+
|
|
15
|
+
INPUT=$(cat)
|
|
16
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
17
|
+
[ -z "$FILE" ] && exit 0
|
|
18
|
+
|
|
19
|
+
# Only check GitHub Actions workflow files
|
|
20
|
+
echo "$FILE" | grep -qE '\.github/workflows/.*\.ya?ml$' || exit 0
|
|
21
|
+
[ ! -f "$FILE" ] && exit 0
|
|
22
|
+
|
|
23
|
+
WARNINGS=""
|
|
24
|
+
|
|
25
|
+
# Check for 'on' trigger
|
|
26
|
+
if ! grep -qE '^on:' "$FILE"; then
|
|
27
|
+
WARNINGS="${WARNINGS}\n Missing 'on:' trigger definition"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Check for unpinned actions (uses: without @sha or @v)
|
|
31
|
+
UNPINNED=$(grep -E '^\s*uses:\s+\S+' "$FILE" | grep -v '@' | head -3)
|
|
32
|
+
if [ -n "$UNPINNED" ]; then
|
|
33
|
+
WARNINGS="${WARNINGS}\n Unpinned action versions (use @v or @sha):"
|
|
34
|
+
echo "$UNPINNED" | while read -r line; do
|
|
35
|
+
WARNINGS="${WARNINGS}\n $line"
|
|
36
|
+
done
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Check for 'runs-on' in jobs
|
|
40
|
+
if grep -qE '^\s+jobs:' "$FILE" && ! grep -qE 'runs-on:' "$FILE"; then
|
|
41
|
+
WARNINGS="${WARNINGS}\n Jobs missing 'runs-on' runner specification"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
if [ -n "$WARNINGS" ]; then
|
|
45
|
+
echo "GitHub Actions warnings in $FILE:" >&2
|
|
46
|
+
echo -e "$WARNINGS" >&2
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
exit 0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# gitignore-auto-add.sh — Suggest .gitignore entries for common patterns
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Committing build artifacts, cache dirs, env files.
|
|
5
|
+
# When Claude creates new directories or files that should
|
|
6
|
+
# be gitignored, this hook warns.
|
|
7
|
+
#
|
|
8
|
+
# TRIGGER: PostToolUse
|
|
9
|
+
# MATCHER: "Bash"
|
|
10
|
+
|
|
11
|
+
INPUT=$(cat)
|
|
12
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
13
|
+
[ -z "$COMMAND" ] && exit 0
|
|
14
|
+
|
|
15
|
+
# Only check mkdir and touch commands
|
|
16
|
+
echo "$COMMAND" | grep -qE '^\s*(mkdir|touch)\s' || exit 0
|
|
17
|
+
|
|
18
|
+
# Patterns that should typically be gitignored
|
|
19
|
+
GITIGNORE_PATTERNS="node_modules|__pycache__|\.cache|dist/|build/|\.next|\.nuxt|\.env\.|coverage|\.pytest_cache|\.mypy_cache|\.tox|\.venv|venv|\.eggs"
|
|
20
|
+
|
|
21
|
+
# Extract the target path
|
|
22
|
+
TARGET=$(echo "$COMMAND" | awk '{print $NF}')
|
|
23
|
+
|
|
24
|
+
if echo "$TARGET" | grep -qiE "$GITIGNORE_PATTERNS"; then
|
|
25
|
+
if ! git check-ignore -q "$TARGET" 2>/dev/null; then
|
|
26
|
+
echo "TIP: '$TARGET' should probably be in .gitignore." >&2
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
exit 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# go-vet-after-edit.sh — Run go vet after editing Go files
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Common Go mistakes that compile but fail at runtime.
|
|
5
|
+
# go vet catches: printf format mismatches, unreachable code,
|
|
6
|
+
# struct tag errors, and more.
|
|
7
|
+
#
|
|
8
|
+
# TRIGGER: PostToolUse
|
|
9
|
+
# MATCHER: "Write|Edit"
|
|
10
|
+
|
|
11
|
+
INPUT=$(cat)
|
|
12
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
13
|
+
[ -z "$FILE" ] && exit 0
|
|
14
|
+
|
|
15
|
+
case "$FILE" in
|
|
16
|
+
*.go) ;;
|
|
17
|
+
*) exit 0 ;;
|
|
18
|
+
esac
|
|
19
|
+
|
|
20
|
+
[ ! -f "$FILE" ] && exit 0
|
|
21
|
+
|
|
22
|
+
# Run go vet on the package containing the file
|
|
23
|
+
DIR=$(dirname "$FILE")
|
|
24
|
+
if command -v go >/dev/null 2>&1; then
|
|
25
|
+
ERRORS=$(cd "$DIR" && go vet ./... 2>&1)
|
|
26
|
+
if [ $? -ne 0 ]; then
|
|
27
|
+
echo "go vet found issues:" >&2
|
|
28
|
+
echo "$ERRORS" | head -5 | sed 's/^/ /' >&2
|
|
29
|
+
exit 2
|
|
30
|
+
fi
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
exit 0
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# hook-tamper-guard.sh — Prevent Claude from modifying its own hooks
|
|
3
|
+
#
|
|
4
|
+
# Solves: Claude can rewrite its own hooks to weaken enforcement
|
|
5
|
+
# (#32376 — "Who watches the watchmen?")
|
|
6
|
+
#
|
|
7
|
+
# Blocks Edit/Write to:
|
|
8
|
+
# ~/.claude/hooks/ (hook scripts)
|
|
9
|
+
# ~/.claude/settings.json (hook registration)
|
|
10
|
+
# .claude/hooks/ (project-level hooks)
|
|
11
|
+
#
|
|
12
|
+
# Also blocks Bash commands that modify these paths:
|
|
13
|
+
# mv/cp/rm on hook files
|
|
14
|
+
# sed/awk that edit hook files
|
|
15
|
+
# echo/cat/tee that overwrite hook files
|
|
16
|
+
#
|
|
17
|
+
# TRIGGER: PreToolUse
|
|
18
|
+
# MATCHER: "Edit|Write|Bash"
|
|
19
|
+
#
|
|
20
|
+
# Usage:
|
|
21
|
+
# {
|
|
22
|
+
# "hooks": {
|
|
23
|
+
# "PreToolUse": [{
|
|
24
|
+
# "matcher": "Edit|Write|Bash",
|
|
25
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/hook-tamper-guard.sh" }]
|
|
26
|
+
# }]
|
|
27
|
+
# }
|
|
28
|
+
# }
|
|
29
|
+
|
|
30
|
+
INPUT=$(cat)
|
|
31
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
32
|
+
|
|
33
|
+
# --- Check Edit/Write tools ---
|
|
34
|
+
if [ "$TOOL" = "Edit" ] || [ "$TOOL" = "Write" ]; then
|
|
35
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty' 2>/dev/null)
|
|
36
|
+
[ -z "$FILE" ] && exit 0
|
|
37
|
+
|
|
38
|
+
# Expand ~ to $HOME
|
|
39
|
+
FILE=$(echo "$FILE" | sed "s|^~|$HOME|")
|
|
40
|
+
|
|
41
|
+
# Block writes to hook directories and settings
|
|
42
|
+
if echo "$FILE" | grep -qE '\.claude/hooks/|\.claude/settings\.json|\.claude/settings\.local\.json'; then
|
|
43
|
+
echo "BLOCKED: Cannot modify hook files or settings. This protects the integrity of your safety hooks." >&2
|
|
44
|
+
echo "If you need to modify hooks, do it manually outside Claude Code." >&2
|
|
45
|
+
exit 2
|
|
46
|
+
fi
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# --- Check Bash commands ---
|
|
50
|
+
if [ "$TOOL" = "Bash" ]; then
|
|
51
|
+
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
52
|
+
[ -z "$CMD" ] && exit 0
|
|
53
|
+
|
|
54
|
+
# Block commands that modify hook files
|
|
55
|
+
if echo "$CMD" | grep -qE '(mv|cp|rm|sed|awk|tee|cat\s*>)\s.*\.claude/(hooks/|settings\.json|settings\.local\.json)'; then
|
|
56
|
+
echo "BLOCKED: Cannot modify hook files via shell commands." >&2
|
|
57
|
+
exit 2
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Block chmod on hook files (could remove execute permission)
|
|
61
|
+
if echo "$CMD" | grep -qE 'chmod\s.*\.claude/hooks/'; then
|
|
62
|
+
echo "BLOCKED: Cannot change hook file permissions." >&2
|
|
63
|
+
exit 2
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
exit 0
|