/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 directoryhistory— 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"
# Verify we are in a git repository
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
# Determine scope from arguments
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
# For staged scope: check that there are staged files
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)
# Scan staged content (what would be committed)
# git diff --cached shows the actual content being staged
git diff --cached -U0 2>/dev/null > "$SCAN_TARGET_FILE"
echo "Collected staged diff for scanning ($(wc -l < "$SCAN_TARGET_FILE") lines)"
;;
all)
# Scan all tracked and untracked files in working directory
# Exclude common binary and generated directories
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"
# Filter out binary-likely extensions and large generated files
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"
# Read all filtered files into scan target
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)
# Scan git log content (recent commits — last 50 commits or since last tag)
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"
# Pattern 1: AWS Access Keys (AKIA... format — 20 char alphanumeric after AKIA)
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
# Pattern 2: AWS Secret Access Keys (40-char base64-like strings after known label)
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
# Pattern 3: GitHub Personal Access Tokens (ghp_, gho_, ghu_, ghs_, ghr_ prefixes)
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
# Pattern 4: Private key headers (RSA, EC, OpenSSH, DSA)
echo "--- Private Key Material ---" >> "$FINDINGS_FILE"
grep -n -E '-----BEGIN (RSA |EC |DSA |OPENSSH |PRIVATE )PRIVATE KEY-----' "$SCAN_TARGET_FILE" >> "$FINDINGS_FILE" || true
# Pattern 5: Generic high-confidence password patterns (labeled assignments)
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
# Pattern 6: Database connection strings with embedded credentials
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
# Pattern 7: JWT tokens (3-part base64 separated by dots, starting with eyJ)
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
# Pattern 8: Slack tokens (xox[bpaso]-...)
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
# Pattern 9: Google API keys (AIza...)
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
# Pattern 10: Stripe API keys (sk_live_, pk_live_, sk_test_, rk_live_)
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/' >>