Setup Git Guardrails
Sets up a PreToolUse hook that intercepts and blocks a fixed list of destructive git commands before Claude executes them.
Source: adapted from mattpocock/skills — misc/git-guardrails-claude-code, imported 2026-08-26. Upstream blocks every invocation of git push, including plain feature-branch pushes. This adaptation narrows that to force-push and direct pushes to protected branches only, because system-level/core.md's git workflow requires routine feature-branch pushes for the standard worktree → branch → push → PR flow — blocking all pushes would make this repo's own workflow unusable. The rest of upstream's blocked-command list (reset --hard, clean -f/-fd, branch -D, bare checkout/restore) is unchanged.
What Gets Blocked
git pushwith--force/--force-with-lease/-fgit pushdirectly tomain,master, orstaginggit reset --hardgit clean -f/git clean -fdgit branch -Dgit checkout ./git restore .(bare, discards uncommitted changes)
Routine git push origin <feature-branch> is not blocked.
When blocked, Claude sees a message telling it that it does not have authority to run the command.
Steps
1. Ask scope
Ask the user: install for this project only (.claude/settings.json) or all projects (~/.claude/settings.json)?
2. Copy the hook script
The bundled script is at: scripts/block-dangerous-git.sh
Copy it to the target location based on scope:
- Project:
.claude/hooks/block-dangerous-git.sh - Global:
~/.claude/hooks/block-dangerous-git.sh
Make it executable with chmod +x.
3. Add hook to settings
Add to the appropriate settings file, merging into any existing hooks.PreToolUse array — don't overwrite other settings:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/block-dangerous-git.sh"
}
]
}
]
}
}
(Project scope: use "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous-git.sh" instead.)
4. Ask about customization
Ask if the user wants to add or remove any protected branch names or command rules. Edit the copied script's PROTECTED_BRANCHES array for branch names and is_dangerous_git_command for command rules.
5. Verify
The hook tokenizes each shell command segment, recognizes git and absolute paths to the Git executable, consumes Git global options, and then checks the subcommand arguments. This avoids matching quoted prose that merely mentions Git operations. Test both directions before trusting the hook in a new environment:
# Should exit 2 (BLOCKED) — real dangerous commands, one per pipe-test:
echo '{"tool_input":{"command":"git push origin main"}}' | <path-to-script>
echo '{"tool_input":{"command":"git -C /tmp/example reset --hard origin/main"}}' | <path-to-script>
# Should exit 0 (allowed) — routine push and prose that merely mentions these words:
echo '{"tool_input":{"command":"git push origin feature/my-branch"}}' | <path-to-script>
echo '{"tool_input":{"command":"gh issue create --body \"discusses git push and main\""}}' | <path-to-script>
Then prove it fires live in-session: run a real (but harmless-context) git push --force or similar in the Bash tool and confirm the hook intercepts it with a PreToolUse:Bash hook error, per this repo's own update-config-style hook-construction discipline (pipe-test → jq -e schema check → live fire proof).