Help us improve
Share bugs, ideas, or general feedback.
From squid
Installs PreToolUse hooks that block destructive git/shell commands (force-push to protected branches, git reset --hard with unstaged work, rm -rf outside worktree). Useful before unattended runs or after a near-miss.
npx claudepluginhub iusztinpaul/squid --plugin squidHow this skill is triggered — by the user, by Claude, or both
Slash command
/squid:git-guardrailsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Claude Code's `PreToolUse` hooks fire **before** a tool call executes. If the hook script exits non-zero, the call is blocked and the failure message returned to the agent. This skill writes a small guardian script and a settings entry that together catch the worst classes of accidental damage:
Sets up Claude Code PreToolUse hooks to block dangerous git commands (push, reset --hard, clean, branch -D) before execution. Prevents destructive git ops in AI coding.
Blocks destructive Bash commands like rm -rf, DROP TABLE, git force-push, reset --hard, and restricts file edits to a specific directory. Use for protection on critical systems.
Installs 695 pre-built safety hooks for Claude Code to block file deletions, credential leaks, git force-pushes, and token waste in autonomous AI coding. Run npx cc-safe-setup.
Share bugs, ideas, or general feedback.
Claude Code's PreToolUse hooks fire before a tool call executes. If the hook script exits non-zero, the call is blocked and the failure message returned to the agent. This skill writes a small guardian script and a settings entry that together catch the worst classes of accidental damage:
git push --force / --force-with-lease to main / master / prod (configurable).git reset --hard when there's unstaged or uncommitted work.git push --no-verify (skipping pre-push hooks).git commit --no-verify / --no-gpg-sign / -c commit.gpgsign=false (skipping pre-commit hooks or signing).rm -rf of paths that resolve outside the current worktree.git worktree remove --force of a worktree that has uncommitted changes.These guardrails complement, not replace, the retry caps in docs/PROCESS.md. Caps stop runaway loops; guardrails stop a single agent call from deleting your work.
/night for unattended runs in a project./day (supervised, you watch every diff). The hooks don't hurt there, but the human is already the guardrail.pre-commit-hooks.md for the project side.Parse $ARGUMENTS for --protected-branches=<csv>. Default protected list: main,master,prod.
If the project has a non-standard default branch (e.g. develop), surface that and ask the user once: "Default branch is develop. Add to protected list? (Y/n)".
Before writing anything, show the user exactly what will land:
.claude/scripts/git-guardrails.sh).Ask explicit approval. Do NOT write on assumed approval — this skill modifies a settings file the user owns.
Path: .claude/scripts/git-guardrails.sh. Make it idempotent — overwriting a previous version is fine.
#!/usr/bin/env bash
# git-guardrails — invoked by Claude Code PreToolUse hooks.
# Reads the proposed Bash command from $CLAUDE_TOOL_INPUT_command (set by the harness).
# Exits 0 to allow, non-zero with a stderr message to block.
set -uo pipefail
cmd="${CLAUDE_TOOL_INPUT_command:-}"
[ -z "$cmd" ] && exit 0
# Configurable: protected branch list (comma-separated, no spaces).
PROTECTED="${GIT_GUARDRAILS_PROTECTED:-main,master,prod}"
block() {
echo "git-guardrails: blocked — $1" >&2
echo " command: $cmd" >&2
echo " override: tell the human and have them run the command themselves." >&2
exit 2
}
# Rule 1: force-push to a protected branch.
if echo "$cmd" | grep -qE 'git[[:space:]]+push.*(--force|--force-with-lease|-f([[:space:]]|$))'; then
for br in ${PROTECTED//,/ }; do
if echo "$cmd" | grep -qE "(^|[[:space:]])${br}(\$|[[:space:]:])"; then
block "force-push to protected branch '$br'"
fi
done
fi
# Rule 2: git reset --hard with unstaged / uncommitted work present.
if echo "$cmd" | grep -qE 'git[[:space:]]+reset[[:space:]]+(--hard|--merge[[:space:]]+--hard)'; then
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
block "'git reset --hard' would discard uncommitted work (run 'git stash' first if you mean it)"
fi
fi
# Rule 3: --no-verify on push or commit (skips hooks).
if echo "$cmd" | grep -qE 'git[[:space:]]+(push|commit).*--no-verify'; then
block "--no-verify skips hooks; fix the underlying lint/test failure instead"
fi
# Rule 4: skipping commit signing.
if echo "$cmd" | grep -qE 'git[[:space:]]+commit.*(--no-gpg-sign|-c[[:space:]]+commit\.gpgsign=false)'; then
block "commit-signing bypass requested; do not bypass without explicit human direction"
fi
# Rule 5: rm -rf of paths escaping the current working tree.
if echo "$cmd" | grep -qE 'rm[[:space:]]+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r|-rf|-fr)'; then
# Extract path arguments (very rough — sufficient for blocking obvious cases).
paths=$(echo "$cmd" | sed -E 's/^.*rm[[:space:]]+-[a-zA-Z]*[[:space:]]+//; s/[[:space:]]*&&.*//; s/[[:space:]]*\|\|.*//; s/[[:space:]]*;.*//')
worktree="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
for p in $paths; do
case "$p" in
/*|~*|..*|"$HOME"*) block "'rm -rf' on absolute / home / parent path: $p" ;;
esac
abs=$(cd "$(dirname "$p")" 2>/dev/null && pwd)/$(basename "$p") || continue
case "$abs" in
"$worktree"*) ;; # inside worktree — allowed
*) block "'rm -rf' on path outside worktree ($worktree): $p" ;;
esac
done
fi
# Rule 6: git worktree remove --force when worktree has uncommitted changes.
if echo "$cmd" | grep -qE 'git[[:space:]]+worktree[[:space:]]+remove.*--force'; then
block "'git worktree remove --force' can drop uncommitted work; remove without --force, or commit/stash first"
fi
exit 0
chmod +x the file after writing it.
.claude/settings.jsonRead the existing file if present (create it if missing). Merge without disturbing anything else:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": ".claude/scripts/git-guardrails.sh" }
]
}
]
}
}
If hooks.PreToolUse already has a Bash matcher, append our command to its hooks array — don't replace. Use a stable equality check on the command string so re-running the skill is idempotent (no duplicate entries).
Run two tests against a scratch worktree (do NOT run against the user's main worktree):
# 1. Should block.
CLAUDE_TOOL_INPUT_command='git push --force origin main' .claude/scripts/git-guardrails.sh; echo "exit=$?"
# Expect: exit=2 with a 'force-push to protected branch' message.
# 2. Should allow.
CLAUDE_TOOL_INPUT_command='git push origin feat/example' .claude/scripts/git-guardrails.sh; echo "exit=$?"
# Expect: exit=0.
Surface both results to the user as confirmation before declaring success.
Single markdown block:
## git-guardrails installed
**Wrote:** `.claude/scripts/git-guardrails.sh` (executable)
**Merged into:** `.claude/settings.json` — `hooks.PreToolUse[Bash]`
**Protected branches:** main, master, prod (override with `GIT_GUARDRAILS_PROTECTED` env var)
**Smoke tests:** force-push to main → blocked; push to feature branch → allowed.
The next time an agent attempts a destructive command, the call will be blocked and the failure message returned to it.
eval, base64-decoded commands) are out of scope. The threat model is honest agent runs into a sharp edge, not adversarial./git-guardrails to refresh after a plugin update never duplicates anything.