Sam's first hook refuses two mistakes: a committed .env file with the database password, and JavaScript that does not parse. node --check - parses a script from standard input without running it:
cat > .git/hooks/pre-commit <<'EOF'
#!/bin/sh
# Refuse secrets and JavaScript that does not parse. It checks the staged
# version of each file (":path"), which is what the commit will contain.
fail=0
for f in $(git diff --cached --name-only --diff-filter=ACM); do
case "$f" in
.env|*/.env) echo "pre-commit: $f holds secrets; unstage it"; fail=1 ;;
*.js) git show ":$f" | node --check - 2>/dev/null ||
{ echo "pre-commit: $f has a syntax error"; fail=1; } ;;
esac
done
exit $fail
EOF
chmod +x .git/hooks/pre-commit
# Edit app.js: start a request logger and forget its closing "});"
git commit -qam "Log each request"; echo "exit status $?"
git restore --staged --worktree app.jsOutput
pre-commit: app.js has a syntax error exit status 1
--diff-filter=ACM lists only staged files that still exist, so a deletion does not break the loop. git show ":$f" reads the index, the version being committed, not the working tree, which may hold unstaged edits. The hook reports every problem before exiting. Nothing was recorded, and git restore discarded the broken edit.