diff --git a/.claude/hooks/block-dangerous-bash.sh b/.claude/hooks/block-dangerous-bash.sh new file mode 100755 index 00000000..b06921fd --- /dev/null +++ b/.claude/hooks/block-dangerous-bash.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +input="$(cat)" +cmd="$(echo "$input" | jq -r '.tool_input.command // empty')" + +[ -z "$cmd" ] && exit 0 + +block() { + echo "Bash command blocked: $1. $2." >&2 + exit 2 +} + +# git reset --hard +if echo "$cmd" | grep -Eq 'git[[:space:]]+reset[[:space:]]+--hard'; then + block "git reset --hard destroys local work" "Use 'git stash' to set work aside, or branch off the current state before resetting" +fi + +# git push --force / -f +if echo "$cmd" | grep -Eq 'git[[:space:]]+push[[:space:]]+(--force([[:space:]]|=|$)|-f([[:space:]]|$))'; then + block "force push rewrites history" "Crewli uses --no-ff merges; never force-push. If the remote diverged, pull/rebase locally and resolve" +fi + +# rm -rf on absolute paths outside /tmp and /home// +if echo "$cmd" | grep -Eq '\brm[[:space:]]+-rf?[[:space:]]+/' && ! echo "$cmd" | grep -Eq '\brm[[:space:]]+-rf?[[:space:]]+/(tmp|var/folders|home/[^/[:space:]]+/[^[:space:]]|Users/[^/[:space:]]+/[^[:space:]])'; then + block "rm -rf on an absolute path outside /tmp" "Verify the path is project-relative; if you really need it, run it manually outside Claude Code" +fi + +# php artisan migrate:fresh — only with --env=testing +if echo "$cmd" | grep -Eq 'php[[:space:]]+artisan[[:space:]]+migrate:fresh\b'; then + if ! echo "$cmd" | grep -Eq -- '--env=testing\b'; then + block "migrate:fresh wipes the database" "Add --env=testing to scope this to the test database, or run a non-destructive 'migrate' / 'migrate:rollback'" + fi +fi + +# php artisan db:wipe — only with --env=testing +if echo "$cmd" | grep -Eq 'php[[:space:]]+artisan[[:space:]]+db:wipe\b'; then + if ! echo "$cmd" | grep -Eq -- '--env=testing\b'; then + block "db:wipe destroys the database" "Add --env=testing to scope this to the test database" + fi +fi + +# composer/pnpm/npm update +if echo "$cmd" | grep -Eq '\b(composer|pnpm|npm)[[:space:]]+update\b'; then + block "blanket dependency update bumps everything without review" "Use targeted 'composer require ' or 'pnpm add ' to bump one package at a time" +fi + +exit 0 diff --git a/.claude/hooks/protect-files.sh b/.claude/hooks/protect-files.sh new file mode 100755 index 00000000..662bf266 --- /dev/null +++ b/.claude/hooks/protect-files.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +input="$(cat)" +path="$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty')" + +[ -z "$path" ] && exit 0 + +block() { + echo "Edit to '$path' blocked: $1. $2." >&2 + exit 2 +} + +# .env files (but not .env.example) +if echo "$path" | grep -Eq '(^|/)\.env(\..*)?$' && ! echo "$path" | grep -Eq '(^|/)\.env\.example$'; then + block "secrets" "Propose changes to .env.example instead" +fi + +# composer.lock +if echo "$path" | grep -Eq '(^|/)composer\.lock$'; then + block "locked dependency tree" "Run composer require deliberately, then commit the regenerated lock file" +fi + +# JS lock files +if echo "$path" | grep -Eq '(^|/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$'; then + block "locked JS dependency tree" "Run pnpm add / npm install deliberately, then commit the regenerated lock file" +fi + +# Laravel default migrations +if echo "$path" | grep -Eq '(^|/)database/migrations/0001_01_01_.*\.php$'; then + block "Laravel default migration" "Never modify Laravel scaffold migrations — write a new migration that alters the table" +fi + +# apps/admin/ — deleted SPA per WS-3 +if echo "$path" | grep -Eq '(^|/)apps/admin/'; then + block "apps/admin/ was deleted in WS-3 and must not return" "Use apps/app/ (Organizer SPA, includes Platform Admin under /platform/*)" +fi + +# .claude/ tooling self-modification +if echo "$path" | grep -Eq '(^|/)\.claude/'; then + block "tooling self-modification — Bert reviews .claude/ changes by hand" "Open the file in an editor outside Claude Code, or ask Bert to authorize the change explicitly" +fi + +# dev-docs/SCHEMA.md +if echo "$path" | grep -Eq '(^|/)dev-docs/SCHEMA\.md$'; then + block "SCHEMA.md is updated only at sprint milestones" "Bert decides when SCHEMA snapshots roll forward — do not edit ad hoc" +fi + +exit 0