I had two Claude Code sessions open in the same repo. One was fixing a header, the other was halfway through a refactor. The first one finished, ran git add -A && git commit, and committed half of the refactor with it. Nothing was broken yet, but the history was, and untangling it took longer than the header fix.
My CLAUDE.md already said "add files by name, never git add -A". Claude followed it most of the time. Most of the time is the problem: a rule in a prompt competes with everything else in the context, and on a long session it loses sometimes.
A hook runs every time
Claude Code can run a script before each tool call. If the script exits with code 2, the call is cancelled and whatever the script printed on stderr goes back to Claude as the reason. Claude reads it and changes approach.
Here is the whole hook:
#!/usr/bin/env python3
import json, re, sys
event = json.load(sys.stdin)
cmd = (event.get("tool_input") or {}).get("command", "")
if re.search(r"\bgit\b[^;&|\n]*\badd\b[^;&|\n]*(\s-A\b|\s--all\b|\s\.(\s|$))", cmd):
print("Seatbelt blocked this command: git add -A / git add . sweeps in .env files "
"and other sessions' work. "
"Add files by name.", file=sys.stderr)
sys.exit(2)
Save it as ~/.claude/hooks/no_add_all.py and register it in ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "python3 ~/.claude/hooks/no_add_all.py" }]
}
]
}
}
Restart Claude Code. Next time it tries, you see this:
● Bash(git add -A && git commit -m "fix header spacing")
⎿ Seatbelt blocked this command: git add -A / git add . sweeps in
.env files and other sessions' work. Add files by name.
● Bash(git add src/Header.tsx && git commit -m "fix header spacing")
⎿ [main 4e1c9a2] fix header spacing
Three details that matter
- The message is for Claude, not for you. "Blocked" alone makes it retry the same thing or give up. "Add files by name" tells it what to do instead.
-
Test the pass cases too.
git add .gitignoremust go through. The\s\.(\s|$)part only matches a lone dot. - Exit code 2, not 1. Exit 1 shows an error to you and lets the command run.
The free version
I packaged this one with rm -rf ~ and force push as a plugin: seatbelt-lite, MIT, two commands to install.
Top comments (0)