| name | secrets-scan |
| description | Pre-commit secrets detection with pattern-based scanning for API keys, tokens, passwords, private keys, and connection strings. Self-contained — no external tools required. |
| model | claude-sonnet-4-6 |
| version | 1.0.0 |
/secrets-scan Workflow
Role
This skill is a pipeline coordinator. It orchestrates a sequential secrets detection workflow using pattern-based scanning. It delegates grep/regex scanning to Bash and synthesis to analysis tasks. It does NOT require external tools like trufflehog or gitleaks — all scanning uses built-in grep patterns, making it self-contained and deployable anywhere Claude Code runs.
Zero tolerance policy: Any confirmed secret detected results in a BLOCKED verdict. There is no passing threshold — secrets in code are a critical finding.
Report redaction rule: This skill NEVER includes actual secret values in reports. Reports show secret type, file path, and line number only. Pattern matches are redacted to show type and location: e.g., "AWS Access Key at src/config.js:42".
Inputs
- Scan scope: $ARGUMENTS
staged (default) — scan git staged files only (pre-commit gate)
all — scan entire working directory
history — scan git commit history (use for post-incident review)
Step 0 — Pre-flight checks
Tool: Bash (direct — coordinator does this)
TIMESTAMP=$(date -u +"%Y%m%dT%H%M%SZ")
echo "Secrets scan run: $TIMESTAMP"
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "ERROR: Not inside a git repository. /secrets-scan requires git."
exit 1
fi
SCOPE="${1:-staged}"
if [ "$SCOPE" != "staged" ] && [ "$SCOPE" != "all" ] && [ "$SCOPE" != "history" ]; then
echo "ERROR: Invalid scope '$SCOPE'. Valid options: staged, all, history"
echo "Usage: /secrets-scan [staged|all|history]"
exit 1
fi
if [ "$SCOPE" = "staged" ]; then
STAGED_FILES=$(git diff --cached --name-only)
if [ -z "$STAGED_FILES" ]; then
echo "No staged files found. Stage files with 'git add' before running /secrets-scan staged."
echo "VERDICT: PASS (no staged files to scan)"
exit 0
fi
echo "Staged files: $(echo "$STAGED_FILES" | wc -l | tr -d ' ') file(s)"
fi
echo "SCOPE=$SCOPE"
echo "TIMESTAMP=$TIMESTAMP"
Pre-flight failures:
- Not a git repo: Stop with error message.
- Invalid scope argument: Stop with usage message.
- No staged files (staged scope): Output PASS with explanation and stop.
Step 1 — Determine scan scope and collect target content
Tool: Bash (direct — coordinator does this)
Based on the scope from Step 0, collect the content to scan:
SCAN_TARGET_FILE=$(mktemp /tmp/secrets-scan-XXXXXXXX.tmp)
case "$SCOPE" in
staged)
git diff --cached -U0 2>/dev/null > "$SCAN_TARGET_FILE"
echo "Collected staged diff for scanning ($(wc -l < "$SCAN_TARGET_FILE") lines)"
;;
all)
FILELIST_TMP=$(mktemp /tmp/secrets-scan-filelist-XXXXXXXX.tmp)
FILELIST_FILTERED_TMP=$(mktemp /tmp/secrets-scan-filelist-XXXXXXXX.tmp)
git ls-files 2>/dev/null > "$FILELIST_TMP"
git ls-files --others --exclude-standard 2>/dev/null >> "$FILELIST_TMP"
grep -v -E '\.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|zip|tar|gz|bin|exe|dll|so|dylib|pyc|class)$' \
"$FILELIST_TMP" | \
grep -v -E '(node_modules/|\.git/|vendor/|dist/|build/|__pycache__/)' > "$FILELIST_FILTERED_TMP"
while IFS= read -r f; do
[ -f "$f" ] && echo "=== FILE: $f ===" >> "$SCAN_TARGET_FILE" && cat "$f" >> "$SCAN_TARGET_FILE"
done < "$FILELIST_FILTERED_TMP"
rm -f "$FILELIST_TMP" "$FILELIST_FILTERED_TMP" 2>/dev/null
echo "Collected working directory content for scanning"
;;
history)
git log --oneline -50 --format="%H" 2>/dev/null | while read -r commit; do
git show "$commit" 2>/dev/null >> "$SCAN_TARGET_FILE"
done
echo "Collected git history (last 50 commits) for scanning"
;;
esac
Step 2 — Pattern-based secret detection
Tool: Bash (direct — coordinator does this)
Run pattern-based detection across the collected scan target. Each pattern targets a distinct secret type.
Note: No entropy analysis in v1.0.0. Pattern-based detection only. Entropy analysis deferred to v1.1.0 after false-positive calibration against real codebases.
FINDINGS_FILE="./plans/secrets-scan-${TIMESTAMP}.raw-findings.txt"
touch "$FINDINGS_FILE"
echo "=== PATTERN SCAN RESULTS ===" >> "$FINDINGS_FILE"
echo "Timestamp: $TIMESTAMP" >> "$FINDINGS_FILE"
echo "Scope: $SCOPE" >> "$FINDINGS_FILE"
echo "" >> "$FINDINGS_FILE"
echo "--- AWS Access Keys (AKIA...) ---" >> "$FINDINGS_FILE"
grep -n -E 'AKIA[0-9A-Z]{16}' "$SCAN_TARGET_FILE" | \
sed 's/\(AKIA[0-9A-Z]\{4\}\)[0-9A-Z]*/\1****REDACTED/' >> "$FINDINGS_FILE" || true
echo "--- AWS Secret Access Keys ---" >> "$FINDINGS_FILE"
grep -n -E -i '(aws_secret_access_key|aws_secret_key)\s*[=:]\s*[A-Za-z0-9/+=]{40}' "$SCAN_TARGET_FILE" | \
sed 's/\([A-Za-z0-9\/+=]\{4\}\)[A-Za-z0-9\/+=]\{32\}\([A-Za-z0-9\/+=]\{4\}\)/\1****REDACTED****\2/' >> "$FINDINGS_FILE" || true
echo "--- GitHub Tokens (ghp_/gho_/ghu_/ghs_/ghr_) ---" >> "$FINDINGS_FILE"
grep -n -E '(ghp_|gho_|ghu_|ghs_|ghr_)[A-Za-z0-9_]{36}' "$SCAN_TARGET_FILE" | \
sed 's/\(gh[a-z]_[A-Za-z0-9_]\{4\}\)[A-Za-z0-9_]*/\1****REDACTED/' >> "$FINDINGS_FILE" || true
echo "--- Private Key Material ---" >> "$FINDINGS_FILE"
grep -n -E '-----BEGIN (RSA |EC |DSA |OPENSSH |PRIVATE )PRIVATE KEY-----' "$SCAN_TARGET_FILE" >> "$FINDINGS_FILE" || true
echo "--- Generic Passwords (labeled) ---" >> "$FINDINGS_FILE"
grep -n -E -i '(password|passwd|pwd|secret|api_key|apikey|api_secret|client_secret|auth_token|access_token)\s*[=:]\s*['\''"][^'\''"]{8,}['\''"]' "$SCAN_TARGET_FILE" | \
sed "s/\(=\s*['\"][^'\"]\{4\}\)[^'\"]*\([^'\"]\{4\}['\"]\)/\1****REDACTED****\2/" >> "$FINDINGS_FILE" || true
echo "--- Database Connection Strings ---" >> "$FINDINGS_FILE"
grep -n -E '(mysql|postgresql|postgres|mongodb|redis|amqp|jdbc)://[^@\s]+:[^@\s]+@' "$SCAN_TARGET_FILE" | \
sed 's|://\([^:]*\):[^@]*@|://\1:****REDACTED****@|' >> "$FINDINGS_FILE" || true
echo "--- JWT Tokens ---" >> "$FINDINGS_FILE"
grep -n -E 'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}' "$SCAN_TARGET_FILE" | \
sed 's/\(eyJ[A-Za-z0-9_-]\{6\}\)[A-Za-z0-9_-]*\(\.[A-Za-z0-9_-]\{4\}\).*/\1****REDACTED***\2.../' >> "$FINDINGS_FILE" || true
echo "--- Slack Tokens ---" >> "$FINDINGS_FILE"
grep -n -E 'xox[bpaso]-[A-Za-z0-9-]{10,}' "$SCAN_TARGET_FILE" | \
sed 's/\(xox[bpaso]-[A-Za-z0-9-]\{6\}\)[A-Za-z0-9-]*/\1****REDACTED/' >> "$FINDINGS_FILE" || true
echo "--- Google API Keys ---" >> "$FINDINGS_FILE"
grep -n -E 'AIza[0-9A-Za-z_-]{35}' "$SCAN_TARGET_FILE" | \
sed 's/\(AIza[0-9A-Za-z_-]\{4\}\)[0-9A-Za-z_-]*/\1****REDACTED/' >> "$FINDINGS_FILE" || true
echo "--- Stripe API Keys ---" >> "$FINDINGS_FILE"
grep -n -E '(sk_live_|pk_live_|rk_live_|sk_test_)[0-9a-zA-Z]{24,}' "$SCAN_TARGET_FILE" | \
sed 's/\(\(sk\|pk\|rk\)_\(live\|test\)_[0-9a-zA-Z]\{6\}\)[0-9a-zA-Z]*/\1****REDACTED/' >> "$FINDINGS_FILE" || true
echo "" >> "$FINDINGS_FILE"
echo "=== RAW SCAN COMPLETE ===" >> "$FINDINGS_FILE"
FINDING_LINES=$(grep -v -E '^(---|===|Timestamp|Scope|$)' "$FINDINGS_FILE" | grep -c '[0-9]' || true)
echo "Raw matches found: $FINDING_LINES"
rm -f "$SCAN_TARGET_FILE"
Step 3 — False positive filtering
Tool: Task, subagent_type=general-purpose, model=claude-sonnet-4-6
Prompt:
"You are a security analyst reviewing raw pattern-match output for false positive elimination.
Read the raw findings file at ./plans/secrets-scan-[TIMESTAMP].raw-findings.txt.
Your task:
Review each finding and classify as CONFIRMED or FALSE_POSITIVE.
False positive indicators — classify as FALSE_POSITIVE if:
- The match is in a test fixture file (e.g., path contains:
test/, tests/, spec/, fixtures/, __tests__/, testdata/)
- The match is in a documentation file (
.md, .rst, .txt, .adoc) showing an example format
- The match is in a comment explaining what a secret LOOKS LIKE (e.g.,
# example: AKIA...)
- The match is an obvious placeholder (e.g.,
AKIAIOSFODNN7EXAMPLE, your-secret-here, <YOUR_API_KEY>, xxx...xxx)
- The match is in a
.env.example, .env.sample, or .env.template file
- The match is in a README or CONTRIBUTING file describing configuration
- The value is clearly a test/dummy value (e.g.,
password: 'test', password: 'secret' in test files)
- The match is in a mock or stub (file path contains
mock, stub, fake)
Confirmed secret indicators — classify as CONFIRMED if:
- The match appears in a source code file that would be executed
- The match appears in a configuration file that is NOT an example/template
- The match appears in a script file
- The match appears in a Dockerfile or docker-compose file
- The value appears to be a real credential format (not a placeholder)
- The match appears in a git diff as an added line (lines starting with '+' in staged scope)
CRITICAL REDACTION RULE: Your output must NEVER include actual secret values. The raw findings file already has patterns redacted. Do NOT attempt to reconstruct or show the original value. Report type, file path, and line number only.
Write your analysis to ./plans/secrets-scan-[TIMESTAMP].filtered-findings.md:
## Secrets Scan Filtered Findings
**Scope:** [staged/all/history]
**Timestamp:** [TIMESTAMP]
### Confirmed Secrets
| # | Type | File | Line | Severity | Notes |
|---|------|------|------|----------|-------|
[rows for each confirmed finding]
[Or: 'No confirmed secrets detected.']
### False Positives Excluded
| Type | File | Reason |
|------|------|--------|
[rows for each excluded finding]
[Or: 'No false positives to exclude.']
### Summary
- Total pattern matches: [N]
- Confirmed secrets: [N]
- False positives excluded: [N]
```"
## Step 4 — Verdict gate
Tool: `Read` (direct — coordinator does this)
Read `./plans/secrets-scan-[TIMESTAMP].filtered-findings.md` and count confirmed secrets.
**Verdict rules — zero tolerance:**
- **BLOCKED:** Any confirmed secret detected (count > 0). No exceptions. No thresholds.
- **PASS:** Zero confirmed secrets detected.
There is no PASS_WITH_NOTES for secrets — a secret is either present or it is not.
**If BLOCKED:**
Output immediately:
BLOCKED — Secrets detected in [scope] scan.
Confirmed secrets found: [count]
[List each: type + file:line — NO actual secret values]
Remove all confirmed secrets before committing or shipping.
Options:
- Remove the secret and rotate the credential (strongly recommended)
- Add the file to .gitignore if it must not be committed
- Use environment variables or a secrets manager instead
- If this is a test fixture or example, rename the file to include 'example' or 'fixture' and use obviously fake values
Run /secrets-scan again after remediation to verify.
Stop workflow. Do not proceed to Step 5 if BLOCKED.
**If PASS:**
Proceed to Step 5.
## Step 5 — Report generation and archive
Tool: `Task` (report) + `Bash` (archive)
**Generate final report:**
Tool: `Task`, `subagent_type=general-purpose`, `model=claude-sonnet-4-6`
Prompt:
"Generate a final secrets scan report.
Read `./plans/secrets-scan-[TIMESTAMP].filtered-findings.md`.
Write the final report to `./plans/secrets-scan-[TIMESTAMP].report.md`:
Secrets Scan Report
Date: [TIMESTAMP]
Scope: [staged/all/history]
Verdict: PASS
Summary
No secrets detected in [scope] scan.
Pattern categories checked:
- AWS Access Keys (AKIA... format)
- AWS Secret Access Keys (labeled assignments)
- GitHub Personal Access Tokens (ghp_, gho_, ghu_, ghs_, ghr_)
- Private Key Material (RSA, EC, DSA, OpenSSH)
- Generic Passwords (labeled high-confidence patterns)
- Database Connection Strings (with embedded credentials)
- JWT Tokens
- Slack Tokens (xox[bpaso]-)
- Google API Keys (AIza...)
- Stripe API Keys (sk_live_, pk_live_, rk_live_)
False Positives Excluded
[List from filtered findings, or 'None']
Recommendations
For production use, consider also deploying:
Note: v1.0.0 uses pattern-based detection. Entropy-based detection (for unstructured secrets) is planned for v1.1.0.
**Archive artifacts:**
Tool: `Bash`
```bash
mkdir -p ./plans/archive/secrets-scan/${TIMESTAMP}
mv ./plans/secrets-scan-${TIMESTAMP}.raw-findings.txt \
./plans/secrets-scan-${TIMESTAMP}.filtered-findings.md \
./plans/archive/secrets-scan/${TIMESTAMP}/ 2>/dev/null || true
echo "Archived intermediate artifacts to ./plans/archive/secrets-scan/${TIMESTAMP}/"
echo "Final report: ./plans/secrets-scan-${TIMESTAMP}.report.md"
Final output:
"Secrets scan PASS. No secrets detected in [scope] scan.
Report: ./plans/secrets-scan-[TIMESTAMP].report.md
Archived artifacts: ./plans/archive/secrets-scan/[TIMESTAMP]/"