-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathpre-commit-secrets.sh
More file actions
executable file
·199 lines (178 loc) · 6.07 KB
/
Copy pathpre-commit-secrets.sh
File metadata and controls
executable file
·199 lines (178 loc) · 6.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#!/bin/bash
# pre-commit-secrets.sh - Pre-commit hook to detect secrets in staged files
# Version: 1.0.0
# Purpose: Prevent accidental commits of API keys, tokens, and credentials
#
# Installation:
# cp examples/hooks/bash/pre-commit-secrets.sh .git/hooks/pre-commit
# chmod +x .git/hooks/pre-commit
#
# Test:
# echo "GITHUB_TOKEN=ghp_test" > test.txt
# git add test.txt
# git commit -m "Test"
# # Should fail with secret detection error
set -euo pipefail
# Context: this is a pre-commit hook (uses git diff --cached); outside a git
# repo (e.g. running as PostToolUse:Edit under ~/.claude) there are no staged
# files to check.
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
exit 0
fi
# Colors
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Secret patterns (extended regex)
# Compat: bash 3.2 (macOS default) does not support 'declare -A'
# (associative arrays only exist from bash 4 onward). So names and
# regexes live in two PARALLEL indexed arrays, in the same order,
# instead of a single name->regex map.
PATTERN_NAMES=(
"OpenAI API Key"
"GitHub Token (ghp)"
"GitHub Token (gho)"
"GitHub Token (ghu)"
"GitHub Token (ghs)"
"GitHub Token (ghr)"
"AWS Access Key"
"AWS Secret Key"
"Anthropic API Key"
"Generic API Key"
"Generic Secret"
"Generic Token"
"Database URL with Password"
"Private Key"
"JWT Token"
)
PATTERN_REGEXES=(
"sk-[A-Za-z0-9]{48}"
"ghp_[A-Za-z0-9]{36}"
"gho_[A-Za-z0-9]{36}"
"ghu_[A-Za-z0-9]{36}"
"ghs_[A-Za-z0-9]{36}"
"ghr_[A-Za-z0-9]{36}"
"AKIA[A-Z0-9]{16}"
"[A-Za-z0-9/+=]{40}"
"sk-ant-[A-Za-z0-9-]{95,}"
"api[_-]?key[\"']?\s*[:=]\s*[\"']?[A-Za-z0-9]{20,}"
"secret[\"']?\s*[:=]\s*[\"']?[A-Za-z0-9]{20,}"
"token[\"']?\s*[:=]\s*[\"']?[A-Za-z0-9]{20,}"
"(postgres|mysql|mongodb)://[^:]+:[^@]+@"
"-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----"
"eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}"
)
# Whitelisted patterns (safe to ignore)
WHITELIST=(
"your_token_here"
"your_key_here"
"example.com"
"localhost"
"placeholder"
"XXXXXX"
"\${env:" # Template variable syntax
"sk-ant-example" # Example in documentation
)
# Files to always skip (even if staged)
SKIP_FILES=(
"*.md" # Documentation often contains example secrets
"*.txt" # Same for text files
"*example*"
"*template*"
"*.sample"
)
# Check if a file should be skipped
should_skip_file() {
local file=$1
for pattern in "${SKIP_FILES[@]}"; do
# Convert glob to regex
local regex="${pattern//\*/.*}"
if [[ $file =~ $regex ]]; then
return 0 # Skip this file
fi
done
return 1 # Don't skip
}
# Check if a match is whitelisted
is_whitelisted() {
local match=$1
for whitelist in "${WHITELIST[@]}"; do
if [[ $match == *"$whitelist"* ]]; then
return 0 # Whitelisted
fi
done
return 1 # Not whitelisted
}
# Main secret detection logic
detect_secrets() {
local files
files=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$files" ]; then
exit 0 # No staged files
fi
local found_secrets=0
local secrets_report=""
# Iterate through staged files
while IFS= read -r file; do
# Skip if file should be ignored
if should_skip_file "$file"; then
continue
fi
# Skip if file doesn't exist (deleted)
if [ ! -f "$file" ]; then
continue
fi
# Get staged content
local content
content=$(git show ":$file" 2>/dev/null || continue)
# Check each pattern (walks both arrays by the same index)
local pidx=0
while [ "$pidx" -lt "${#PATTERN_NAMES[@]}" ]; do
local pattern_name="${PATTERN_NAMES[$pidx]}"
local pattern="${PATTERN_REGEXES[$pidx]}"
pidx=$((pidx + 1))
local matches
# -e isolates the pattern from option parsing: some patterns
# (e.g. "Private Key") start with "-----", and without -e
# grep treats them as an unknown flag instead of a regex,
# failing with "unrecognized option" and never matching.
matches=$(echo "$content" | grep -noE -e "$pattern" || true)
if [ -n "$matches" ]; then
# Check each match against whitelist
while IFS= read -r match; do
local line_num="${match%%:*}"
local matched_text="${match#*:}"
if ! is_whitelisted "$matched_text"; then
found_secrets=1
secrets_report+=" ${file}:${line_num} - ${pattern_name}\n"
secrets_report+=" Content: ${matched_text:0:50}...\n"
fi
done <<< "$matches"
fi
done
done <<< "$files"
# Report findings
if [ $found_secrets -eq 1 ]; then
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${RED}✗ COMMIT BLOCKED: Secrets detected in staged files${NC}"
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e "${YELLOW}Found potential secrets:${NC}"
echo -e "$secrets_report"
echo ""
echo -e "${YELLOW}Remediation steps:${NC}"
echo " 1. Remove secrets from files"
echo " 2. Use environment variables: \${env:VAR_NAME}"
echo " 3. Store secrets in ~/.claude/.env (gitignored)"
echo " 4. See: guide/ultimate-guide.md Section 8.3.1"
echo ""
echo -e "${YELLOW}If this is a false positive:${NC}"
echo " - Edit .git/hooks/pre-commit and add to WHITELIST array"
echo " - Or skip hook: git commit --no-verify (USE WITH CAUTION)"
echo ""
exit 1
fi
exit 0
}
# Run detection
detect_secrets