Hook Source Code
Full source code for all built-in hooks included in Claude Code Kit.
Hook Source Code
All hooks live in .claude/hooks/ and receive tool input as JSON via stdin. Exit 0 to allow, exit 2 to block (PreToolUse only).
PreToolUse Hooks
protect-files.sh
Blocks edits to sensitive files — .env, credentials, private keys, lock files.
#!/usr/bin/env bash
#
# protect-files.sh — PreToolUse hook
# Blocks edits to sensitive files (secrets, credentials, lock files)
#
# Reads tool input from stdin (JSON with tool_name and tool_input)
#
set -euo pipefail
INPUT=$(cat)
parse_json_field() {
local field="$1"
if command -v jq &>/dev/null; then
echo "$INPUT" | jq -r "(.tool_input.${field} // .${field}) // empty" 2>/dev/null || true
elif command -v python3 &>/dev/null; then
echo "$INPUT" | python3 -c "import sys,json;d=json.load(sys.stdin);v=d.get('tool_input',d);print(v.get('${field}',d.get('${field}','')))" 2>/dev/null || true
else
echo "$INPUT" | grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:[[:space:]]*"//;s/"$//' || true
fi
}
TOOL_NAME=$(parse_json_field "tool_name")
# Only check file-writing tools
case "$TOOL_NAME" in
Edit|Write|NotebookEdit) ;;
*) exit 0 ;;
esac
# Extract file path from tool input
FILE_PATH=$(parse_json_field "file_path")
[ -z "$FILE_PATH" ] && exit 0
BASENAME=$(basename "$FILE_PATH")
DIRNAME=$(dirname "$FILE_PATH")
# Protected file patterns
BLOCKED=false
REASON=""
case "$BASENAME" in
.env|.env.local|.env.production|.env.staging|.env.development)
BLOCKED=true
REASON="Environment file with secrets"
;;
.env.example|.env.template|.env.sample|.env.test)
;; # Allow these — they don't contain real secrets
.env.*)
BLOCKED=true
REASON="Environment file with secrets"
;;
credentials.json|service-account.json|serviceAccountKey.json)
BLOCKED=true
REASON="Credential file"
;;
id_rsa|id_ed25519|id_ecdsa|*.pem|*.key)
BLOCKED=true
REASON="Private key file"
;;
*.p12|*.pfx)
BLOCKED=true
REASON="Certificate file"
;;
*.jks)
BLOCKED=true
REASON="Java keystore file"
;;
firebase-adminsdk*.json)
BLOCKED=true
REASON="Firebase Admin SDK credential file"
;;
google-services.json)
BLOCKED=true
REASON="Android Google services config"
;;
GoogleService-Info.plist)
BLOCKED=true
REASON="iOS Google services config"
;;
package-lock.json|yarn.lock|pnpm-lock.yaml|Gemfile.lock|poetry.lock|Cargo.lock)
BLOCKED=true
REASON="Lock file (should be auto-generated)"
;;
esac
# Check directory patterns
case "$DIRNAME" in
*/.ssh|*/.ssh/*|*/.gnupg|*/.gnupg/*|*/.aws|*/.aws/*)
BLOCKED=true
REASON="Sensitive config directory"
;;
esac
if [ "$BLOCKED" = true ]; then
echo "BLOCKED: $REASON — $FILE_PATH"
echo ""
echo "If you need to modify this file, ask the user to do it manually"
echo "or get explicit approval first."
exit 2
fi
exit 0branch-protect.sh
Blocks direct pushes to main/master and force pushes.
#!/usr/bin/env bash
#
# branch-protect.sh — PreToolUse hook
# Blocks direct pushes to main/master branch
#
# Reads tool input from stdin (JSON with tool_name and tool_input)
#
set -euo pipefail
INPUT=$(cat)
parse_json_field() {
local field="$1"
if command -v jq &>/dev/null; then
echo "$INPUT" | jq -r "(.tool_input.${field} // .${field}) // empty" 2>/dev/null || true
elif command -v python3 &>/dev/null; then
echo "$INPUT" | python3 -c "import sys,json;d=json.load(sys.stdin);v=d.get('tool_input',d);print(v.get('${field}',d.get('${field}','')))" 2>/dev/null || true
else
echo "$INPUT" | grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:[[:space:]]*"//;s/"$//' || true
fi
}
TOOL_NAME=$(parse_json_field "tool_name")
# Only check Bash tool
[ "$TOOL_NAME" != "Bash" ] && exit 0
COMMAND=$(parse_json_field "command")
[ -z "$COMMAND" ] && exit 0
# Check for force push (check first — always block regardless of branch)
# Allow --force-with-lease (safer alternative) but block --force and -f
if echo "$COMMAND" | grep -qE 'git\s+push\s+.*--force-with-lease'; then
: # Allow --force-with-lease (only overwrites if remote matches expectations)
elif echo "$COMMAND" | grep -qE 'git\s+push\s+.*--force|git\s+push\s+.*-f\b'; then
echo "BLOCKED: Force push detected"
echo ""
echo "Force pushing can overwrite remote history."
echo "Consider using --force-with-lease for a safer alternative,"
echo "or get explicit approval from the user before force pushing."
exit 2
fi
# Check for git push to protected branches (explicit branch name)
if echo "$COMMAND" | grep -qE 'git\s+push\s+(\S+\s+)?(main|master)\s*($|[;&|])|git\s+push\s+.*\s+HEAD:(main|master)\b'; then
echo "BLOCKED: Direct push to main/master branch"
echo ""
echo "Create a feature branch and open a PR instead:"
echo " git checkout -b feat/your-feature"
echo " git push -u origin feat/your-feature"
exit 2
fi
# Check for bare `git push` when on main/master (no branch specified)
if echo "$COMMAND" | grep -qE '(^|[;&|]\s*)git\s+push(\s+-u)?(\s+origin)?\s*($|[;&|])'; then
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null) || CURRENT_BRANCH=""
if [ -z "$CURRENT_BRANCH" ]; then
exit 0 # Cannot determine branch, allow the push
fi
if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "master" ]; then
echo "BLOCKED: You are on '$CURRENT_BRANCH' — bare 'git push' would push to protected branch"
echo ""
echo "Create a feature branch and open a PR instead:"
echo " git checkout -b feat/your-feature"
echo " git push -u origin feat/your-feature"
exit 2
fi
fi
exit 0block-dangerous-commands.sh
Blocks destructive shell commands — rm -rf /, git reset --hard, DROP TABLE, etc.
#!/usr/bin/env bash
#
# block-dangerous-commands.sh — PreToolUse hook
# Blocks destructive shell commands that are hard to reverse
#
# Reads tool input from stdin (JSON with tool_name and tool_input)
#
set -euo pipefail
INPUT=$(cat)
parse_json_field() {
local field="$1"
if command -v jq &>/dev/null; then
echo "$INPUT" | jq -r "(.tool_input.${field} // .${field}) // empty" 2>/dev/null || true
elif command -v python3 &>/dev/null; then
echo "$INPUT" | python3 -c "import sys,json;d=json.load(sys.stdin);v=d.get('tool_input',d);print(v.get('${field}',d.get('${field}','')))" 2>/dev/null || true
else
echo "$INPUT" | grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:[[:space:]]*"//;s/"$//' || true
fi
}
TOOL_NAME=$(parse_json_field "tool_name")
# Only check Bash tool
[ "$TOOL_NAME" != "Bash" ] && exit 0
COMMAND=$(parse_json_field "command")
[ -z "$COMMAND" ] && exit 0
BLOCKED=false
REASON=""
# Destructive file operations — catch rm -rf, rm -r -f, rm --recursive --force, etc.
RM_RECURSIVE='rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+(-[a-zA-Z]+\s+)*|-r\s+-f\s+|-f\s+-r\s+|--recursive\s+(-f\s+|--force\s+)?|-r\s+--force\s+)'
if echo "$COMMAND" | grep -qE "${RM_RECURSIVE}/[[:space:]]*($|[;&|])"; then
BLOCKED=true
REASON="Recursive delete on root directory"
fi
if echo "$COMMAND" | grep -qE "${RM_RECURSIVE}(~|\\\$HOME|\\\$\{HOME\})\b"; then
BLOCKED=true
REASON="Recursive delete on home directory"
fi
if echo "$COMMAND" | grep -qE "${RM_RECURSIVE}\\.\s*($|[;&|])"; then
BLOCKED=true
REASON="Recursive delete on current directory"
fi
if echo "$COMMAND" | grep -qE "${RM_RECURSIVE}\\*"; then
BLOCKED=true
REASON="Recursive delete with wildcard"
fi
# Git history destruction
if echo "$COMMAND" | grep -qE 'git\s+reset\s+--hard'; then
BLOCKED=true
REASON="git reset --hard discards all uncommitted changes"
fi
if echo "$COMMAND" | grep -qE 'git\s+clean\s+(-[a-zA-Z]*f[a-zA-Z]*d|-[a-zA-Z]*d[a-zA-Z]*f|-[a-zA-Z]*f\s+-[a-zA-Z]*d|-[a-zA-Z]*d\s+-[a-zA-Z]*f)'; then
BLOCKED=true
REASON="git clean -fd permanently deletes untracked files"
fi
# Database destruction
if echo "$COMMAND" | grep -qiE 'DROP\s+(TABLE|DATABASE|SCHEMA)\b'; then
BLOCKED=true
REASON="SQL DROP statement — destructive database operation"
fi
if echo "$COMMAND" | grep -qiE 'TRUNCATE\s+TABLE\b'; then
BLOCKED=true
REASON="SQL TRUNCATE — deletes all rows permanently"
fi
# Docker destruction
if echo "$COMMAND" | grep -qE 'docker\s+system\s+prune\s+-a'; then
BLOCKED=true
REASON="Docker system prune -a removes all unused images and containers"
fi
# chmod/chown on broad paths
if echo "$COMMAND" | grep -qE '(chmod|chown)\s+-R\s+.*\s+/'; then
BLOCKED=true
REASON="Recursive permission change on root path"
fi
if [ "$BLOCKED" = true ]; then
echo "BLOCKED: $REASON"
echo ""
echo "Command: $COMMAND"
echo ""
echo "This command is potentially destructive and hard to reverse."
echo "Get explicit approval from the user before running it."
exit 2
fi
exit 0conventional-commit.sh
Validates commit messages follow the conventional commit format.
#!/usr/bin/env bash
#
# conventional-commit.sh — PreToolUse hook
# Validates commit messages follow conventional commit format
#
# Format: <type>: <description>
# Types: feat, fix, refactor, test, docs, chore, perf, ci, build, style
#
set -euo pipefail
INPUT=$(cat)
parse_json_field() {
local field="$1"
if command -v jq &>/dev/null; then
echo "$INPUT" | jq -r "(.tool_input.${field} // .${field}) // empty" 2>/dev/null || true
elif command -v python3 &>/dev/null; then
echo "$INPUT" | python3 -c "import sys,json;d=json.load(sys.stdin);v=d.get('tool_input',d);print(v.get('${field}',d.get('${field}','')))" 2>/dev/null || true
else
echo "$INPUT" | grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:[[:space:]]*"//;s/"$//' || true
fi
}
TOOL_NAME=$(parse_json_field "tool_name")
# Only check Bash tool
[ "$TOOL_NAME" != "Bash" ] && exit 0
COMMAND=$(parse_json_field "command")
[ -z "$COMMAND" ] && exit 0
# Only check git commit commands
if ! echo "$COMMAND" | grep -qE 'git\s+commit'; then
exit 0
fi
# Extract commit message from -m/--message flag
MSG=$(echo "$COMMAND" | grep -oE '(-m|--message)[[:space:]]*"[^"]*"' | sed -E 's/(-m|--message)[[:space:]]*"//;s/"$//' || echo "")
# Also try single quotes
if [ -z "$MSG" ]; then
MSG=$(echo "$COMMAND" | grep -oE "(-m|--message)[[:space:]]*'[^']*'" | sed -E "s/(-m|--message)[[:space:]]*'//;s/'$//" || echo "")
fi
# If using heredoc or no -m flag, skip validation
if [ -z "$MSG" ]; then
exit 0
fi
# Get first line of commit message
FIRST_LINE=$(echo "$MSG" | head -1)
# Validate conventional commit format
VALID_TYPES="feat|fix|refactor|test|docs|chore|perf|ci|build|style"
if ! echo "$FIRST_LINE" | grep -qE "^($VALID_TYPES)(\(.+\))?: .+"; then
echo "BLOCKED: Commit message doesn't follow conventional commit format"
echo ""
echo " Got: $FIRST_LINE"
echo " Expected: <type>: <description>"
echo ""
echo " Valid types: feat, fix, refactor, test, docs, chore, perf, ci, build, style"
echo ""
echo " Examples:"
echo " feat: add user search endpoint"
echo " fix: handle null response from auth API"
echo " refactor(auth): simplify token validation"
exit 2
fi
exit 0PostToolUse Hooks
secret-scan.sh
Scans edited files for accidentally committed secrets — AWS keys, API tokens, private keys, JWTs, GitHub tokens.
Runs after every Edit or Write operation. Outputs warnings but does not block (exit 0). Change to exit 2 to make it blocking.
#!/usr/bin/env bash
#
# secret-scan.sh — PostToolUse hook
# Scans edited files for accidentally committed secrets
#
# Reads tool input from stdin (JSON with tool_name and tool_input)
#
set -euo pipefail
INPUT=$(cat)
parse_json_field() {
local field="$1"
if command -v jq &>/dev/null; then
echo "$INPUT" | jq -r "(.tool_input.${field} // .${field}) // empty" 2>/dev/null || true
elif command -v python3 &>/dev/null; then
echo "$INPUT" | python3 -c "import sys,json;d=json.load(sys.stdin);v=d.get('tool_input',d);print(v.get('${field}',d.get('${field}','')))" 2>/dev/null || true
else
echo "$INPUT" | grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:[[:space:]]*"//;s/"$//' || true
fi
}
TOOL_NAME=$(parse_json_field "tool_name")
# Only run after file edits
case "$TOOL_NAME" in
Edit|Write|NotebookEdit) ;;
*) exit 0 ;;
esac
FILE_PATH=$(parse_json_field "file_path")
[ -z "$FILE_PATH" ] && exit 0
[ ! -f "$FILE_PATH" ] && exit 0
# Skip binary files (check for "text" in file output — works on both macOS and Linux)
if ! file "$FILE_PATH" | grep -qi "text"; then
exit 0
fi
# Skip lock files and common non-source files
BASENAME=$(basename "$FILE_PATH")
case "$BASENAME" in
package-lock.json|yarn.lock|pnpm-lock.yaml|*.lock) exit 0 ;;
*.min.js|*.min.css|*.bundle.js|*.map) exit 0 ;;
*.g.dart|*.designer.cs|*.generated.swift|*.generated.ts) exit 0 ;;
esac
FINDINGS=""
# AWS keys
if grep -nE 'AKIA[0-9A-Z]{16}' "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - AWS Access Key ID detected"
fi
# Generic API key patterns (key = "...", api_key: "...", etc.)
SQ="'"
if grep -nE "(api[_-]?key|api[_-]?secret|auth[_-]?token|access[_-]?token|secret[_-]?key)[[:space:]]*[=:][[:space:]]*[\"${SQ}][A-Za-z0-9+/=_-]{20,}[\"${SQ}]" "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - API key or token assignment detected"
fi
# Private keys
if grep -nE 'BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY' "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - Private key detected"
fi
# Common password patterns
if grep -nE "(password|passwd|pwd)[[:space:]]*[=:][[:space:]]*[\"${SQ}][^\"${SQ}]{8,}[\"${SQ}]" "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - Hardcoded password detected"
fi
# JWT tokens
if grep -nE 'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}' "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - JWT token detected"
fi
# GitHub tokens
if grep -nE '(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}' "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - GitHub token detected"
fi
# Slack tokens
if grep -nE 'xox[bpars]-[A-Za-z0-9-]{10,}' "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - Slack token detected"
fi
# Google API keys
if grep -nE 'AIza[0-9A-Za-z_-]{35}' "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - Google API key detected"
fi
# Stripe keys
if grep -nE '(sk|pk)_live_[A-Za-z0-9]{20,}' "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - Stripe live key detected"
fi
# SendGrid keys
if grep -nE 'SG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}' "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - SendGrid API key detected"
fi
# Twilio Account SID and Auth Token
if grep -nE 'AC[a-f0-9]{32}' "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - Twilio Account SID detected"
fi
# npm tokens
if grep -nE 'npm_[A-Za-z0-9]{36,}' "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - npm token detected"
fi
# PyPI tokens
if grep -nE 'pypi-[A-Za-z0-9_-]{50,}' "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - PyPI token detected"
fi
# Database connection strings with passwords
if grep -nE '(mongodb|postgres|postgresql|mysql|redis)://[^:]+:[^@]+@' "$FILE_PATH" >/dev/null 2>&1; then
FINDINGS="${FINDINGS}\n - Database connection string with credentials detected"
fi
if [ -n "$FINDINGS" ]; then
echo "WARNING: Potential secrets found in $FILE_PATH"
printf '%b\n' "$FINDINGS"
echo ""
echo "If these are intentional (e.g., test fixtures, examples),"
echo "you can ignore this warning. Otherwise, remove the secrets"
echo "and use environment variables instead."
# Exit 0 (warning only, don't block)
# Change to exit 2 if you want to block edits with secrets
exit 0
fi
exit 0unicode-scan.sh
Detects invisible Unicode characters that may indicate Glassworm-style supply chain attacks or code obfuscation. Scans for:
- Variation Selectors (U+FE00-FE0F) — primary Glassworm payload encoding vector
- Variation Selectors Supplement (U+E0100-E01EF) — secondary Glassworm vector
- Zero Width characters (U+200B-200D, U+2060, U+FEFF mid-file) — code obfuscation
- Tags block (U+E0000-E007F) — hidden text encoding
Uses perl -CSD for reliable UTF-8 detection with python3 fallback. Supports allowlisting via // kit-allow-unicode comment in the first 5 lines of a file.
Runs after every Edit or Write operation. Outputs warnings but does not block (exit 0). Change to exit 2 to make it blocking.
#!/usr/bin/env bash
#
# unicode-scan.sh — PostToolUse hook
# Detects invisible Unicode characters that may indicate Glassworm-style
# supply chain attacks or code obfuscation.
#
# Scans edited files for:
# - Variation Selectors (U+FE00–FE0F) — Glassworm payload encoding
# - Variation Selectors Supplement (U+E0100–E01EF) — Glassworm payload encoding
# - Zero Width characters (U+200B–200D, U+2060, U+FEFF mid-file)
# - Tags block (U+E0000–E007F) — hidden text encoding
#
# Exit 0 = warn only (default)
# Change final exit to "exit 2" to block edits containing invisible Unicode
#
set -euo pipefail
INPUT=$(cat)
parse_json_field() {
local field="$1"
if command -v jq &>/dev/null; then
echo "$INPUT" | jq -r "(.tool_input.${field} // .${field}) // empty" 2>/dev/null || true
elif command -v python3 &>/dev/null; then
echo "$INPUT" | python3 -c "import sys,json;d=json.load(sys.stdin);v=d.get('tool_input',d);print(v.get('${field}',d.get('${field}','')))" 2>/dev/null || true
else
echo "$INPUT" | grep -oE "\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*:[[:space:]]*"//;s/"$//' || true
fi
}
TOOL_NAME=$(parse_json_field "tool_name")
# Only run after file edits
case "$TOOL_NAME" in
Edit|Write|NotebookEdit) ;;
*) exit 0 ;;
esac
FILE_PATH=$(parse_json_field "file_path")
[ -z "$FILE_PATH" ] && exit 0
[ ! -f "$FILE_PATH" ] && exit 0
# Skip binary files
if ! file "$FILE_PATH" | grep -qi "text"; then
exit 0
fi
# Skip files that legitimately contain Unicode (fonts, locale data, etc.)
BASENAME=$(basename "$FILE_PATH")
case "$BASENAME" in
package-lock.json|yarn.lock|pnpm-lock.yaml|*.lock) exit 0 ;;
*.min.js|*.min.css|*.bundle.js|*.map) exit 0 ;;
*.woff|*.woff2|*.ttf|*.otf|*.eot) exit 0 ;;
*.po|*.mo) exit 0 ;;
esac
# Check for allowlist comment in file
if head -5 "$FILE_PATH" | grep -q "kit-allow-unicode" 2>/dev/null; then
exit 0
fi
FINDINGS=""
# Use perl for reliable Unicode detection (available on macOS and most Linux)
if command -v perl &>/dev/null; then
# Zero Width Space (U+200B)
if ! perl -CSD -ne 'exit 1 if /\x{200B}/' "$FILE_PATH" 2>/dev/null; then
FINDINGS="${FINDINGS}\n - Zero Width Space (U+200B) detected"
fi
# Zero Width Non-Joiner (U+200C)
if ! perl -CSD -ne 'exit 1 if /\x{200C}/' "$FILE_PATH" 2>/dev/null; then
FINDINGS="${FINDINGS}\n - Zero Width Non-Joiner (U+200C) detected"
fi
# Zero Width Joiner (U+200D)
if ! perl -CSD -ne 'exit 1 if /\x{200D}/' "$FILE_PATH" 2>/dev/null; then
FINDINGS="${FINDINGS}\n - Zero Width Joiner (U+200D) detected"
fi
# Word Joiner (U+2060)
if ! perl -CSD -ne 'exit 1 if /\x{2060}/' "$FILE_PATH" 2>/dev/null; then
FINDINGS="${FINDINGS}\n - Word Joiner (U+2060) detected"
fi
# BOM mid-file (U+FEFF) — skip first line (legitimate BOM position)
if ! tail -n +2 "$FILE_PATH" | perl -CSD -ne 'exit 1 if /\x{FEFF}/' 2>/dev/null; then
FINDINGS="${FINDINGS}\n - Zero Width No-Break Space / BOM mid-file (U+FEFF) detected"
fi
# Variation Selectors (U+FE00–FE0F) — primary Glassworm vector
if ! perl -CSD -ne 'exit 1 if /[\x{FE00}-\x{FE0F}]/' "$FILE_PATH" 2>/dev/null; then
FINDINGS="${FINDINGS}\n - Variation Selectors (U+FE00-FE0F) detected — GLASSWORM ATTACK VECTOR"
fi
# Variation Selectors Supplement (U+E0100–E01EF) — primary Glassworm vector
if ! perl -CSD -ne 'exit 1 if /[\x{E0100}-\x{E01EF}]/' "$FILE_PATH" 2>/dev/null; then
FINDINGS="${FINDINGS}\n - Variation Selectors Supplement (U+E0100-E01EF) detected — GLASSWORM ATTACK VECTOR"
fi
# Tags block (U+E0000–E007F) — hidden text encoding
if ! perl -CSD -ne 'exit 1 if /[\x{E0000}-\x{E007F}]/' "$FILE_PATH" 2>/dev/null; then
FINDINGS="${FINDINGS}\n - Tags block (U+E0000-E007F) detected — hidden text encoding"
fi
elif command -v python3 &>/dev/null; then
# Fallback: use python3 for detection
PYTHON_RESULT=$(python3 -c "
import sys, re
findings = []
try:
with open(sys.argv[1], 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Skip first-line BOM
lines = content.split('\n')
content_no_first = '\n'.join(lines[1:]) if len(lines) > 1 else ''
checks = [
(r'\u200B', 'Zero Width Space (U+200B)'),
(r'\u200C', 'Zero Width Non-Joiner (U+200C)'),
(r'\u200D', 'Zero Width Joiner (U+200D)'),
(r'\u2060', 'Word Joiner (U+2060)'),
(r'[\uFE00-\uFE0F]', 'Variation Selectors (U+FE00-FE0F) — GLASSWORM ATTACK VECTOR'),
(r'[\U000E0000-\U000E007F]', 'Tags block (U+E0000-E007F) — hidden text encoding'),
(r'[\U000E0100-\U000E01EF]', 'Variation Selectors Supplement (U+E0100-E01EF) — GLASSWORM ATTACK VECTOR'),
]
for pattern, desc in checks:
if re.search(pattern, content):
findings.append(desc)
# BOM mid-file only
if re.search(r'\uFEFF', content_no_first):
findings.append('Zero Width No-Break Space / BOM mid-file (U+FEFF)')
except Exception:
pass
for f in findings:
print(f)
" "$FILE_PATH" 2>/dev/null)
if [ -n "$PYTHON_RESULT" ]; then
while IFS= read -r line; do
FINDINGS="${FINDINGS}\n - ${line}"
done <<< "$PYTHON_RESULT"
fi
else
# No perl or python3 — skip check silently
exit 0
fi
if [ -n "$FINDINGS" ]; then
echo "SECURITY WARNING: Invisible Unicode characters detected in $FILE_PATH"
printf '%b\n' "$FINDINGS"
echo ""
echo "This may indicate a Glassworm-style supply chain attack."
echo "Invisible characters can hide malicious payloads that execute via eval()."
echo ""
echo "Actions:"
echo " 1. Inspect the file with: xxd $FILE_PATH | less"
echo " 2. If intentional, add '// kit-allow-unicode' to the first 5 lines"
echo ""
# Exit 0 = warn only. Change to exit 2 to block edits with invisible Unicode.
exit 0
fi
exit 0skill-compliance.sh (optional)
After file edits, detects which skills are relevant to the edited file and surfaces matching checklist items as a reminder. Matches by file extension and skill content keywords.
auto-lint.sh (optional)
Runs the appropriate linter after file edits — eslint, ruff, gofmt, clippy, rubocop. Detects the language by file extension and finds the project root by looking for package.json, pyproject.toml, go.mod, or Cargo.toml.
auto-format.sh (optional)
Runs the appropriate formatter after file edits — prettier, black, gofmt, rustfmt, rubocop. Same project root detection as auto-lint.
Other Hooks
task-complete-notify.sh (Stop)
Sends a desktop notification when Claude finishes a task. Works on macOS (osascript) and Linux (notify-send). Plays a sound effect.
skill-extract-reminder.sh (UserPromptSubmit, optional)
Injects a reminder into Claude's context to consider extracting non-obvious knowledge as a skill. Runs on every user prompt submission.