Add dotclaude configuration files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Poshan Pandey
2026-03-26 17:16:27 -07:00
parent c10636b330
commit 491a45dd43
37 changed files with 2737 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
# Hooks
Hook scripts are deterministic enforcement — unlike rules (advisory), hooks **guarantee** behavior by blocking or modifying tool calls before/after they execute.
Hooks are wired in `settings.json` under the `"hooks"` key. Each hook specifies an event, a matcher, and a command to run.
## Available Hooks
### protect-files.sh
**Event**: PreToolUse (Edit|Write)
Blocks edits to sensitive and generated files. Fails closed (blocks if `jq` is missing).
- `.env`, `.env.*` — secrets (by basename and path)
- `*.pem`, `*.key`, `*.crt`, `*.p12`, `*.pfx` — certificates and keys
- `id_rsa`, `id_ed25519`, `credentials.json`, `.npmrc`, `.pypirc` — credentials
- `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml` — lock files
- `*.gen.ts`, `*.generated.*` — generated code
- `*.min.js`, `*.min.css` — minified bundles
- Anything inside `.git/`, `secrets/`, or `.claude/hooks/`
- Self-protecting: blocks edits to hook scripts and `settings.json`
### warn-large-files.sh
**Event**: PreToolUse (Edit|Write)
Blocks writes to build artifacts, dependency directories, and binary files. Fails closed.
- `node_modules/`, `vendor/`, `dist/`, `build/`, `.next/`, `__pycache__/`, `.venv/`
- `*.wasm`, `*.so`, `*.dylib`, `*.dll`, `*.exe`, `*.zip`, `*.tar.*`
- `*.mp4`, `*.mov`, `*.mp3`, `*.pyc`, `*.class`
### block-dangerous-commands.sh
**Event**: PreToolUse (Bash)
Blocks dangerous shell commands. Detects patterns even in chained commands (`&&`, `;`). Fails closed.
- **Git**: `git push origin main/master`, `git push --force` (allows `--force-with-lease`), bare `git push` on main
- **Filesystem**: `rm -rf /`, `rm -rf ~`, recursive delete on root/home paths
- **Database**: `DROP TABLE/DATABASE`, `DELETE FROM` without WHERE, `TRUNCATE TABLE`
- **System**: `chmod 777`, piping `curl`/`wget` to `bash`/`sh`, `mkfs`, `dd if=`, writes to `/dev/`
### format-on-save.sh
**Event**: PostToolUse (Edit|Write)
Auto-formats files after Claude edits them. Auto-detects formatters by checking for both the binary and a config file:
- Biome: `biome.json` + `node_modules/.bin/biome`
- Prettier: `.prettierrc*` or `package.json` prettier key + `node_modules/.bin/prettier`
- Ruff: `ruff.toml` or `pyproject.toml [tool.ruff]` + `ruff` binary
- Black: `pyproject.toml [tool.black]` + `black` binary
- rustfmt: standard for Rust (no config needed)
- gofmt: standard for Go (no config needed)
### session-start.sh
**Event**: SessionStart
Injects dynamic project context at session start: current branch (or detached HEAD warning), last commit, uncommitted changes count, staged changes indicator, and stash count.
## Adding Your Own
1. Create a `.sh` script in this directory
2. Make it executable: `chmod +x your-hook.sh`
3. Wire it in `settings.json`:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/your-hook.sh"
}
]
}
]
}
}
```
- Exit 0 = allow, Exit 2 = block
- Scripts receive JSON on stdin with `tool_input`
- Requires `jq` for JSON parsing
See [Claude Code docs](https://code.claude.com/docs/en/hooks) for all hook events.
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
# Blocks dangerous shell commands: push to main, force push, destructive operations.
# Used as a PreToolUse hook for Bash operations.
# Exit 2 = block the action. Exit 0 = allow.
# Requires jq for JSON parsing — fail closed if missing
if ! command -v jq >/dev/null 2>&1; then
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"jq is required for command protection hooks but is not installed.\"}}"
exit 2
fi
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if [ -z "$COMMAND" ]; then
exit 0
fi
deny() {
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"$1\"}}"
exit 2
}
# ──────────────────────────────────────────────
# Git protections
# ──────────────────────────────────────────────
# Check if the command contains git push (handles chaining with &&, ;, |, subshells)
if echo "$COMMAND" | grep -qE '(^|[;&|()]+[[:space:]]*)git[[:space:]]+push'; then
# Block push to main or master
if echo "$COMMAND" | grep -qE 'git[[:space:]]+push.*(origin[[:space:]]+|:)(main|master)\b'; then
deny "Blocked: cannot push directly to main/master. Use a feature branch and create a PR."
fi
# Block bare "git push" when on main/master
if echo "$COMMAND" | grep -qE 'git[[:space:]]+push[[:space:]]*($|[;&|])'; then
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null)
if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "master" ]; then
deny "Blocked: you are on $CURRENT_BRANCH. Use a feature branch and create a PR."
fi
fi
# Block force push (allow --force-with-lease)
if echo "$COMMAND" | grep -qE 'git[[:space:]]+push.*(-[a-zA-Z]*f|--force)([[:space:]]|$)' && ! echo "$COMMAND" | grep -q '\-\-force-with-lease'; then
deny "Blocked: force push is not allowed. Use --force-with-lease if you need to overwrite remote."
fi
fi
# ──────────────────────────────────────────────
# Destructive filesystem operations
# ──────────────────────────────────────────────
# Block rm -rf on root, home, or broad paths
if echo "$COMMAND" | grep -qE 'rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*f[[:space:]]+(\/|~|\$HOME|\.\.\/\.\.)'; then
deny "Blocked: recursive force-delete on root/home/parent paths. Specify a safe target directory."
fi
# Block rm -rf / or rm -rf /* or rm -rf ~
if echo "$COMMAND" | grep -qE 'rm[[:space:]]+-[a-zA-Z]*r.*[[:space:]]+(\/[[:space:]]|\/\*|\/$|~\/?\*?[[:space:]]|~\/?\*?$)'; then
deny "Blocked: recursive delete targeting root or home directory."
fi
# ──────────────────────────────────────────────
# Dangerous database operations
# ──────────────────────────────────────────────
# Block DROP TABLE/DATABASE without safeguards
if echo "$COMMAND" | grep -qiE 'DROP[[:space:]]+(TABLE|DATABASE|SCHEMA)[[:space:]]'; then
deny "Blocked: DROP TABLE/DATABASE/SCHEMA detected. This is destructive and irreversible. Run manually if intended."
fi
# Block DELETE FROM without WHERE
if echo "$COMMAND" | grep -qiE 'DELETE[[:space:]]+FROM[[:space:]]+[a-zA-Z_]+[[:space:]]*($|;)' && ! echo "$COMMAND" | grep -qiE 'WHERE'; then
deny "Blocked: DELETE FROM without WHERE clause would delete all rows. Add a WHERE clause."
fi
# Block TRUNCATE TABLE
if echo "$COMMAND" | grep -qiE 'TRUNCATE[[:space:]]+TABLE'; then
deny "Blocked: TRUNCATE TABLE detected. This is destructive and irreversible. Run manually if intended."
fi
# ──────────────────────────────────────────────
# Dangerous system commands
# ──────────────────────────────────────────────
# Block chmod 777
if echo "$COMMAND" | grep -qE 'chmod[[:space:]]+777'; then
deny "Blocked: chmod 777 gives everyone read/write/execute. Use more restrictive permissions (e.g., 755 or 644)."
fi
# Block piping curl/wget to shell execution
if echo "$COMMAND" | grep -qE '(curl|wget)[[:space:]].*\|[[:space:]]*(bash|sh|zsh|sudo)'; then
deny "Blocked: piping downloaded content directly to a shell is dangerous. Download first, inspect, then execute."
fi
# Block disk/partition destructive commands
if echo "$COMMAND" | grep -qE '(mkfs|dd[[:space:]]+if=|>[[:space:]]*/dev/)'; then
deny "Blocked: destructive disk operation detected. This can cause irreversible data loss."
fi
# ──────────────────────────────────────────────
# Destructive git operations
# ──────────────────────────────────────────────
# Block git reset --hard (loses uncommitted work permanently)
if echo "$COMMAND" | grep -qE 'git[[:space:]]+reset[[:space:]]+--hard'; then
deny "Blocked: git reset --hard discards uncommitted changes permanently. Use git stash or git reset --soft instead."
fi
# Block git clean -f (permanently deletes untracked files)
if echo "$COMMAND" | grep -qE 'git[[:space:]]+clean[[:space:]]+-[a-zA-Z]*f'; then
deny "Blocked: git clean -f permanently deletes untracked files. Review with git clean -n first, then run manually if intended."
fi
# ──────────────────────────────────────────────
# Accidental package publishing
# ──────────────────────────────────────────────
if echo "$COMMAND" | grep -qE '(npm|yarn|pnpm|bun)[[:space:]]+publish'; then
deny "Blocked: publishing npm packages should be done manually or via CI, not through Claude Code."
fi
if echo "$COMMAND" | grep -qE 'cargo[[:space:]]+publish'; then
deny "Blocked: publishing crates should be done manually or via CI, not through Claude Code."
fi
if echo "$COMMAND" | grep -qE 'gem[[:space:]]+push'; then
deny "Blocked: publishing gems should be done manually or via CI, not through Claude Code."
fi
if echo "$COMMAND" | grep -qE 'twine[[:space:]]+upload'; then
deny "Blocked: publishing Python packages should be done manually or via CI, not through Claude Code."
fi
exit 0
+125
View File
@@ -0,0 +1,125 @@
#!/bin/bash
# Auto-formats files after Claude edits them.
# Used as a PostToolUse hook for Edit|Write operations.
# Auto-detects formatters — requires both the binary AND a config file to activate.
# Requires jq for JSON parsing
if ! command -v jq >/dev/null 2>&1; then
exit 0
fi
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
exit 0
fi
EXTENSION="${FILE_PATH##*.}"
FORMATTED=false
# Find the project root (nearest directory with package.json, pyproject.toml, Cargo.toml, go.mod, or .git)
find_project_root() {
local dir="$PWD"
while [ "$dir" != "/" ]; do
if [ -f "$dir/package.json" ] || [ -f "$dir/pyproject.toml" ] || [ -f "$dir/Cargo.toml" ] || [ -f "$dir/go.mod" ] || [ -d "$dir/.git" ]; then
echo "$dir"
return
fi
dir=$(dirname "$dir")
done
echo "$PWD"
}
ROOT=$(find_project_root)
# --- Biome (JS/TS all-in-one — check first, it's faster than Prettier) ---
if [ "$FORMATTED" = false ] && [ -f "$ROOT/node_modules/.bin/biome" ] && [ -f "$ROOT/biome.json" -o -f "$ROOT/biome.jsonc" ]; then
case "$EXTENSION" in
js|jsx|ts|tsx|json|css)
npx biome format --write "$FILE_PATH" 2>/dev/null && FORMATTED=true
;;
esac
fi
# --- Prettier (Node.js / TypeScript) ---
if [ "$FORMATTED" = false ] && [ -f "$ROOT/node_modules/.bin/prettier" ]; then
# Check for Prettier config (any common format)
HAS_PRETTIER_CONFIG=false
for cfg in .prettierrc .prettierrc.json .prettierrc.yml .prettierrc.yaml .prettierrc.js .prettierrc.cjs .prettierrc.mjs .prettierrc.toml prettier.config.js prettier.config.cjs prettier.config.mjs; do
if [ -f "$ROOT/$cfg" ]; then
HAS_PRETTIER_CONFIG=true
break
fi
done
# Also check package.json for "prettier" key
if [ "$HAS_PRETTIER_CONFIG" = false ] && [ -f "$ROOT/package.json" ] && grep -q '"prettier"' "$ROOT/package.json" 2>/dev/null; then
HAS_PRETTIER_CONFIG=true
fi
if [ "$HAS_PRETTIER_CONFIG" = true ]; then
case "$EXTENSION" in
js|jsx|ts|tsx|json|css|scss|md|yaml|yml|html)
npx prettier --write "$FILE_PATH" 2>/dev/null && FORMATTED=true
;;
esac
fi
fi
# --- Ruff (Python — modern replacement for Black + isort) ---
if [ "$FORMATTED" = false ] && command -v ruff >/dev/null 2>&1; then
HAS_RUFF_CONFIG=false
if [ -f "$ROOT/ruff.toml" ] || [ -f "$ROOT/.ruff.toml" ]; then
HAS_RUFF_CONFIG=true
elif [ -f "$ROOT/pyproject.toml" ] && grep -q '\[tool\.ruff\]' "$ROOT/pyproject.toml" 2>/dev/null; then
HAS_RUFF_CONFIG=true
fi
if [ "$HAS_RUFF_CONFIG" = true ]; then
case "$EXTENSION" in
py)
ruff format "$FILE_PATH" 2>/dev/null
ruff check --fix "$FILE_PATH" 2>/dev/null
FORMATTED=true
;;
esac
fi
fi
# --- Black + isort (Python — fallback if Ruff not configured) ---
if [ "$FORMATTED" = false ] && command -v black >/dev/null 2>&1; then
HAS_BLACK_CONFIG=false
if [ -f "$ROOT/pyproject.toml" ] && grep -q '\[tool\.black\]' "$ROOT/pyproject.toml" 2>/dev/null; then
HAS_BLACK_CONFIG=true
fi
if [ "$HAS_BLACK_CONFIG" = true ]; then
case "$EXTENSION" in
py)
black --quiet "$FILE_PATH" 2>/dev/null
command -v isort >/dev/null 2>&1 && isort --quiet "$FILE_PATH" 2>/dev/null
FORMATTED=true
;;
esac
fi
fi
# --- Rust (rustfmt is standard — no config check needed) ---
if [ "$FORMATTED" = false ] && command -v rustfmt >/dev/null 2>&1; then
case "$EXTENSION" in
rs)
rustfmt "$FILE_PATH" 2>/dev/null && FORMATTED=true
;;
esac
fi
# --- Go (gofmt is standard — no config check needed) ---
if [ "$FORMATTED" = false ] && command -v gofmt >/dev/null 2>&1; then
case "$EXTENSION" in
go)
gofmt -w "$FILE_PATH" 2>/dev/null && FORMATTED=true
;;
esac
fi
exit 0
+77
View File
@@ -0,0 +1,77 @@
#!/bin/bash
# Blocks edits to sensitive or generated files.
# Used as a PreToolUse hook for Edit|Write operations.
# Exit 2 = block the action. Exit 0 = allow.
# Requires jq for JSON parsing — fail closed if missing
if ! command -v jq >/dev/null 2>&1; then
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"jq is required for file protection hooks but is not installed.\"}}"
exit 2
fi
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
# Protected patterns — add your own
PROTECTED_PATTERNS=(
".env"
".env.*"
"*.pem"
"*.key"
"*.crt"
"*.p12"
"*.pfx"
"id_rsa"
"id_ed25519"
"credentials.json"
".npmrc"
".pypirc"
"package-lock.json"
"yarn.lock"
"pnpm-lock.yaml"
"*.gen.ts"
"*.generated.*"
"*.min.js"
"*.min.css"
)
BASENAME=$(basename "$FILE_PATH")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
case "$BASENAME" in
$pattern)
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Protected file: $BASENAME matches pattern '$pattern'\"}}"
exit 2
;;
esac
done
# Block anything in common sensitive directories (handles both relative and absolute paths)
case "$FILE_PATH" in
.git/*|*/.git/*)
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Cannot edit files inside .git/\"}}"
exit 2
;;
secrets/*|*/secrets/*)
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Cannot edit files inside secrets/\"}}"
exit 2
;;
.env|.env.*|*/.env|*/.env.*)
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Cannot edit .env files\"}}"
exit 2
;;
.claude/hooks/*|*/.claude/hooks/*)
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Cannot edit hook scripts — these enforce security boundaries.\"}}"
exit 2
;;
.claude/settings.json|*/.claude/settings.json)
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"ask\",\"permissionDecisionReason\":\"Editing settings.json — this controls permissions and hooks. Confirm this change.\"}}"
exit 2
;;
esac
exit 0
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
# Scans file content for accidental secrets before writing.
# Used as a PreToolUse hook for Edit|Write operations.
# Exit 2 = block. Exit 0 = allow.
# Requires jq for JSON parsing — allow if missing (don't block the user)
if ! command -v jq >/dev/null 2>&1; then
exit 0
fi
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
# Extract the content being written
if [ "$TOOL_NAME" = "Write" ]; then
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
elif [ "$TOOL_NAME" = "Edit" ]; then
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty')
else
exit 0
fi
if [ -z "$CONTENT" ]; then
exit 0
fi
# --- High-confidence secret patterns ---
MATCHES=""
# AWS Access Key IDs
if echo "$CONTENT" | grep -qE 'AKIA[0-9A-Z]{16}'; then
MATCHES="$MATCHES AWS access key (AKIA...);"
fi
# AWS Secret Access Keys (40 chars base64 after a key assignment)
if echo "$CONTENT" | grep -qiE '(aws_secret_access_key|secret_key)[[:space:]]*[=:][[:space:]]*["\x27]?[A-Za-z0-9/+=]{40}'; then
MATCHES="$MATCHES AWS secret key;"
fi
# GitHub tokens (PAT, OAuth, App)
if echo "$CONTENT" | grep -qE '(ghp_|gho_|ghs_|ghr_|github_pat_)[a-zA-Z0-9_]{20,}'; then
MATCHES="$MATCHES GitHub token;"
fi
# OpenAI / Stripe / Anthropic style keys (sk-...)
if echo "$CONTENT" | grep -qE 'sk-[a-zA-Z0-9]{20,}'; then
MATCHES="$MATCHES API key (sk-...);"
fi
# Slack tokens
if echo "$CONTENT" | grep -qE 'xox[bpras]-[0-9a-zA-Z-]{10,}'; then
MATCHES="$MATCHES Slack token;"
fi
# Private key blocks
if echo "$CONTENT" | grep -qE -- '-----BEGIN[[:space:]]+(RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----'; then
MATCHES="$MATCHES private key block;"
fi
# Connection strings with embedded credentials
if echo "$CONTENT" | grep -qE '(mongodb|postgres|mysql|redis|amqp|smtp)(\+[a-z]+)?://[^:[:space:]]+:[^@[:space:]]+@'; then
MATCHES="$MATCHES connection string with credentials;"
fi
# Generic password/secret/token assignments with literal string values
# Matches: password = "actual_value", SECRET_KEY: 'actual_value', api_token="actual_value"
# Excludes: env var references like process.env.*, os.environ.*, ${...}, getenv(...)
if echo "$CONTENT" | grep -qiE '(password|secret|token|api_key|apikey|api_secret)[[:space:]]*[=:][[:space:]]*["\x27][^"\x27]{8,}["\x27]' && \
! echo "$CONTENT" | grep -qiE '(password|secret|token|api_key|apikey|api_secret)[[:space:]]*[=:][[:space:]]*["\x27]?(process\.env|os\.environ|getenv|\$\{|ENV\[|env\()'; then
MATCHES="$MATCHES hardcoded credential;"
fi
if [ -n "$MATCHES" ]; then
# Use "ask" not "deny" — warn the user but let them override (could be test fixtures)
REASON="Possible secret detected in content:$MATCHES Review carefully before allowing."
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"ask\",\"permissionDecisionReason\":\"$REASON\"}}"
exit 2
fi
exit 0
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
# Injects dynamic project context at session start.
# Used as a SessionStart hook.
CONTEXT=""
# Current branch (or detached HEAD)
BRANCH=$(git branch --show-current 2>/dev/null)
if [ -n "$BRANCH" ]; then
CONTEXT="Branch: $BRANCH"
elif git rev-parse --git-dir >/dev/null 2>&1; then
SHORT_SHA=$(git rev-parse --short HEAD 2>/dev/null)
CONTEXT="HEAD: detached at $SHORT_SHA"
fi
# Last commit
LAST_COMMIT=$(git log --oneline -1 2>/dev/null)
if [ -n "$LAST_COMMIT" ]; then
CONTEXT="$CONTEXT | Last commit: $LAST_COMMIT"
fi
# Uncommitted changes count
CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
if [ "$CHANGES" -gt 0 ] 2>/dev/null; then
CONTEXT="$CONTEXT | Uncommitted changes: $CHANGES files"
fi
# Staged changes indicator
if ! git diff --cached --quiet 2>/dev/null; then
CONTEXT="$CONTEXT | Staged: yes"
fi
# Stash count
STASH_COUNT=$(git stash list 2>/dev/null | wc -l | tr -d ' ')
if [ "$STASH_COUNT" -gt 0 ] 2>/dev/null; then
CONTEXT="$CONTEXT | Stashes: $STASH_COUNT"
fi
# Active PR on current branch (if gh CLI is available)
if command -v gh >/dev/null 2>&1; then
PR_INFO=$(gh pr view --json number,title,state --jq '"PR #\(.number): \(.title) (\(.state))"' 2>/dev/null)
if [ -n "$PR_INFO" ]; then
CONTEXT="$CONTEXT | $PR_INFO"
fi
fi
if [ -n "$CONTEXT" ]; then
echo "$CONTEXT"
fi
exit 0
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
# Blocks writes to build artifacts, binary files, and dependency directories.
# Used as a PreToolUse hook for Edit|Write operations.
# Exit 2 = block the action. Exit 0 = allow.
# Requires jq for JSON parsing — fail closed if missing
if ! command -v jq >/dev/null 2>&1; then
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"jq is required for file protection hooks but is not installed.\"}}"
exit 2
fi
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
# Block dependency and build directories
case "$FILE_PATH" in
node_modules/*|*/node_modules/*)
REASON="Cannot write into node_modules/ — install dependencies via package manager instead." ;;
vendor/*|*/vendor/*)
REASON="Cannot write into vendor/ — use dependency manager instead." ;;
dist/*|*/dist/*|build/*|*/build/*|.next/*|*/.next/*)
REASON="Cannot write into build output directories — these are generated by the build process." ;;
__pycache__/*|*/__pycache__/*)
REASON="Cannot write into __pycache__/ — these are generated by Python." ;;
.venv/*|*/.venv/*|venv/*|*/venv/*)
REASON="Cannot write into virtual environment directories." ;;
*)
REASON="" ;;
esac
if [ -n "$REASON" ]; then
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"$REASON\"}}"
exit 2
fi
# Block binary and archive file extensions
BASENAME=$(basename "$FILE_PATH")
case "$BASENAME" in
*.wasm|*.so|*.dylib|*.dll|*.exe|*.o|*.a)
REASON="Cannot write binary files — these should be compiled, not hand-written." ;;
*.zip|*.tar|*.tar.gz|*.tar.bz2|*.tgz|*.rar|*.7z)
REASON="Cannot write archive files." ;;
*.mp4|*.mov|*.avi|*.mkv|*.mp3|*.wav|*.flac)
REASON="Cannot write media files — add these manually outside Claude Code." ;;
*.pyc|*.pyo|*.class)
REASON="Cannot write compiled bytecode files." ;;
esac
if [ -n "$REASON" ]; then
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"$REASON\"}}"
exit 2
fi
exit 0