cc-safe-setup 29.5.0 → 29.6.1
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/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 +85 -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/file-change-tracker.sh +49 -0
- 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-secret-mask.sh +49 -0
- package/examples/output-token-env-check.sh +44 -0
- package/examples/package-lock-frozen.sh +25 -0
- package/examples/permission-audit-log.sh +77 -0
- package/examples/pip-venv-required.sh +40 -0
- package/examples/port-conflict-check.sh +62 -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/rm-safety-net.sh +97 -0
- package/examples/rust-clippy-after-edit.sh +37 -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/session-token-counter.sh +59 -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/worktree-unmerged-guard.sh +85 -0
- package/examples/yaml-syntax-check.sh +50 -0
- package/index.mjs +3 -0
- package/package.json +2 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# credential-exfil-guard.sh — Block credential hunting commands
|
|
3
|
+
#
|
|
4
|
+
# Solves: Agents scanning for tokens, secrets, and credentials without permission
|
|
5
|
+
# (#37845 — 48 bash commands auto-executed to exfiltrate credentials)
|
|
6
|
+
#
|
|
7
|
+
# Detects patterns like:
|
|
8
|
+
# env | grep -i token
|
|
9
|
+
# find / -name "*.token" -o -name "*credentials*"
|
|
10
|
+
# cat ~/.ssh/id_rsa
|
|
11
|
+
# printenv | grep SECRET
|
|
12
|
+
# cat /etc/shadow
|
|
13
|
+
#
|
|
14
|
+
# Usage: Add to settings.json as a PreToolUse hook
|
|
15
|
+
#
|
|
16
|
+
# {
|
|
17
|
+
# "hooks": {
|
|
18
|
+
# "PreToolUse": [{
|
|
19
|
+
# "matcher": "Bash",
|
|
20
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/credential-exfil-guard.sh" }]
|
|
21
|
+
# }]
|
|
22
|
+
# }
|
|
23
|
+
# }
|
|
24
|
+
|
|
25
|
+
INPUT=$(cat)
|
|
26
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
27
|
+
|
|
28
|
+
[ -z "$COMMAND" ] && exit 0
|
|
29
|
+
|
|
30
|
+
# Pattern 1: env/printenv piped to grep for secrets
|
|
31
|
+
if echo "$COMMAND" | grep -qiE '(env|printenv|set)\s*\|.*grep.*\b(token|secret|key|password|credential|auth|oauth|cookie|session|api.key)\b'; then
|
|
32
|
+
echo "BLOCKED: Credential hunting via environment variable scanning" >&2
|
|
33
|
+
exit 2
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Pattern 2: find searching for credential files
|
|
37
|
+
if echo "$COMMAND" | grep -qiE 'find\s.*-name\s.*\*?(token|secret|credential|password|\.key|\.pem|\.p12|\.pfx|\.keystore|\.jks|\.env)'; then
|
|
38
|
+
echo "BLOCKED: Credential hunting via file system search" >&2
|
|
39
|
+
exit 2
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Pattern 3: Direct access to known credential locations
|
|
43
|
+
if echo "$COMMAND" | grep -qE 'cat\s+(~|/home|/root)/.ssh/(id_|authorized_keys|known_hosts|config)'; then
|
|
44
|
+
echo "BLOCKED: Direct SSH credential access" >&2
|
|
45
|
+
exit 2
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Pattern 4: Reading system credential files
|
|
49
|
+
if echo "$COMMAND" | grep -qE 'cat\s+(/etc/shadow|/etc/gshadow|/etc/passwd)'; then
|
|
50
|
+
echo "BLOCKED: System credential file access" >&2
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Pattern 5: AWS/cloud credential files
|
|
55
|
+
if echo "$COMMAND" | grep -qE 'cat\s+(~|/home|/root)/\.(aws|gcloud|azure|kube)/(credentials|config|token)'; then
|
|
56
|
+
echo "BLOCKED: Cloud provider credential access" >&2
|
|
57
|
+
exit 2
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Pattern 6: Browser credential stores
|
|
61
|
+
if echo "$COMMAND" | grep -qiE 'find\s.*\.(chrome|firefox|mozilla|safari).*\b(login|password|cookie|token)\b'; then
|
|
62
|
+
echo "BLOCKED: Browser credential hunting" >&2
|
|
63
|
+
exit 2
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Pattern 7: Dumping all environment variables (without filtering)
|
|
67
|
+
if echo "$COMMAND" | grep -qE '^\s*(env|printenv|set)\s*$'; then
|
|
68
|
+
echo "WARNING: Dumping all environment variables may expose secrets" >&2
|
|
69
|
+
# Don't block, just warn — some legitimate uses exist
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# 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
|
+
|
|
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,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# file-change-tracker.sh — Track all file modifications in a session
|
|
3
|
+
#
|
|
4
|
+
# Solves: Hard to know which files Claude modified during a session.
|
|
5
|
+
# Git diff shows the final state but not the order of changes.
|
|
6
|
+
# This log shows every Write/Edit in chronological order.
|
|
7
|
+
#
|
|
8
|
+
# How it works: PostToolUse hook for Write/Edit that logs each change.
|
|
9
|
+
# Creates a timestamped changelog at ~/.claude/session-changes.log
|
|
10
|
+
#
|
|
11
|
+
# Usage: Add to settings.json as a PostToolUse hook
|
|
12
|
+
#
|
|
13
|
+
# {
|
|
14
|
+
# "hooks": {
|
|
15
|
+
# "PostToolUse": [{
|
|
16
|
+
# "matcher": "Write",
|
|
17
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/file-change-tracker.sh" }]
|
|
18
|
+
# }, {
|
|
19
|
+
# "matcher": "Edit",
|
|
20
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/file-change-tracker.sh" }]
|
|
21
|
+
# }]
|
|
22
|
+
# }
|
|
23
|
+
# }
|
|
24
|
+
#
|
|
25
|
+
# View changes: cat ~/.claude/session-changes.log
|
|
26
|
+
|
|
27
|
+
INPUT=$(cat)
|
|
28
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
29
|
+
|
|
30
|
+
[ -z "$TOOL" ] && exit 0
|
|
31
|
+
|
|
32
|
+
LOG_FILE="${CC_CHANGE_LOG:-$HOME/.claude/session-changes.log}"
|
|
33
|
+
TIMESTAMP=$(date '+%H:%M:%S')
|
|
34
|
+
|
|
35
|
+
case "$TOOL" in
|
|
36
|
+
Write)
|
|
37
|
+
FILEPATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
38
|
+
CONTENT_LEN=$(echo "$INPUT" | jq -r '.tool_input.content // empty' 2>/dev/null | wc -c)
|
|
39
|
+
echo "$TIMESTAMP WRITE $FILEPATH (${CONTENT_LEN}B)" >> "$LOG_FILE" 2>/dev/null
|
|
40
|
+
;;
|
|
41
|
+
Edit)
|
|
42
|
+
FILEPATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
43
|
+
OLD_LEN=$(echo "$INPUT" | jq -r '.tool_input.old_string // empty' 2>/dev/null | wc -c)
|
|
44
|
+
NEW_LEN=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null | wc -c)
|
|
45
|
+
echo "$TIMESTAMP EDIT $FILEPATH (${OLD_LEN}B → ${NEW_LEN}B)" >> "$LOG_FILE" 2>/dev/null
|
|
46
|
+
;;
|
|
47
|
+
esac
|
|
48
|
+
|
|
49
|
+
exit 0
|
|
@@ -0,0 +1,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
|