#!/usr/bin/env bash
# PreToolUse hook: refuse to let Claude Code write to a secrets file.
#
# WHY THIS EXISTS
# "Never touch my .env file" written in CLAUDE.md is a request. The model reads
# it, usually respects it, and one day does not. This script is enforcement: it
# runs on every Write and Edit, before the file is touched, whatever the model
# intended.
#
# INSTALL (two steps, no coding required)
#   1. Save this file as .claude/hooks/protect-secrets.sh in your project, then
#      make it runnable:  chmod +x .claude/hooks/protect-secrets.sh
#   2. Copy the block in settings-snippet.json into .claude/settings.json.
#
# TEST IT WITHOUT CLAUDE (paste this whole line into your terminal)
#   echo '{"tool_name":"Write","tool_input":{"file_path":".env"}}' | .claude/hooks/protect-secrets.sh
#   You should see a JSON line containing "permissionDecision":"deny".
#
# HOW IT DECIDES
# Claude Code hands this script a JSON description of what it is about to do, on
# standard input. The script prints a JSON decision back. "deny" stops the edit
# and sends the reason to the model, which is why the reason is written as an
# instruction rather than an error.

set -uo pipefail

# Add or remove patterns here. One per line, shell glob syntax.
PROTECTED=(
  "*.env"
  ".env"
  ".env.*"
  "*.pem"
  "*.key"
  "*credentials*"
  "*secrets*"
)

# The hook needs python3 only to read one field out of the incoming JSON. If it
# is missing we FAIL CLOSED: a guard that cannot check must not wave things
# through, because a broken guard that returns "allow" is worse than no guard
# at all. You would never know it stopped working.
if ! command -v python3 >/dev/null 2>&1; then
  printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"protect-secrets: python3 not found, so the secrets check could not run. Failing closed on purpose. Install python3 or remove this hook deliberately."}}\n'
  exit 0
fi

EVENT=$(cat)
FILE_PATH=$(printf '%s' "$EVENT" | python3 -c '
import json, sys
try:
    event = json.load(sys.stdin)
except Exception:
    print("")
    sys.exit(0)
print(event.get("tool_input", {}).get("file_path", "") or "")
')

# No file path means this event is not a file write we care about. Allow.
if [ -z "$FILE_PATH" ]; then
  printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}\n'
  exit 0
fi

BASENAME=$(basename "$FILE_PATH")

# The matched name gets interpolated into the JSON decision below. Escape it
# first, so a path containing a quote or backslash cannot break the JSON and
# turn a deny into a hook error.
json_escape() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
SAFE_BASENAME=$(json_escape "$BASENAME")
SAFE_FILE_PATH=$(json_escape "$FILE_PATH")

for PATTERN in "${PROTECTED[@]}"; do
  # Match the bare filename AND the full path, because Claude may pass either an
  # absolute path, a repo-relative path, or a ./-prefixed path. Checking only one
  # shape is the most common reason a guard like this looks installed and never
  # actually fires.
  # shellcheck disable=SC2254
  case "$BASENAME" in
    $PATTERN)
      printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"protect-secrets: %s is a protected file and must be edited by a human, not by Claude. If this change is genuinely needed, tell the user exactly what to change and let them make the edit themselves."}}\n' "$SAFE_BASENAME"
      exit 0
      ;;
  esac
  # shellcheck disable=SC2254
  case "$FILE_PATH" in
    $PATTERN|*/$PATTERN)
      printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"protect-secrets: %s is a protected file and must be edited by a human, not by Claude. If this change is genuinely needed, tell the user exactly what to change and let them make the edit themselves."}}\n' "$SAFE_FILE_PATH"
      exit 0
      ;;
  esac
done

printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}\n'
exit 0
