diff --git a/.claude/hooks/git-push-protected-branch.sh b/.claude/hooks/git-push-protected-branch.sh new file mode 100755 index 0000000..73e7f7d --- /dev/null +++ b/.claude/hooks/git-push-protected-branch.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Prompt before a git push whose target branch is main or master. +# Emits no output for any other push, letting normal permission rules decide. +set -uo pipefail + +[[ -t 0 ]] && exit 0 +command=$(jq -r '.tool_input.command // ""' 2>/dev/null) || exit 0 + +# Positional arguments after `git push` are the remote and its refspecs. +read -ra parts <<<"$command" +args=() +for part in "${parts[@]:2}"; do + [[ "$part" == -* ]] || args+=("$part") +done + +# With no refspec git pushes the current branch; otherwise the last refspec +# wins, and its destination is whatever follows the colon. +if [[ ${#args[@]} -le 1 ]]; then + target=HEAD +else + target="${args[-1]}" + [[ "$target" == *:* ]] && target="${target##*:}" +fi + +target="${target#refs/heads/}" +if [[ -z "$target" || "$target" == HEAD ]]; then + target=$(git symbolic-ref --quiet --short HEAD 2>/dev/null || echo "") +fi + +if [[ "$target" =~ ^(main|master)$ ]]; then + jq -n --arg t "$target" '{ + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "ask", + permissionDecisionReason: ("Push targets protected branch: " + $t) + } + }' +fi diff --git a/.claude/settings.json b/.claude/settings.json index e0c8cff..46b4006 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -7,7 +7,10 @@ }, "permissions": { "deny": [ - "Bash(rm -rf *)", + "Bash(rm -rf /)", + "Bash(rm -rf ~)", + "Bash(rm -rf ~/)", + "Bash(sudo rm *)", "Bash(dd *)", "Bash(mkfs *)", "Bash(shred *)", @@ -18,11 +21,31 @@ "Bash(cat /etc/shadow)", "Bash(cat /etc/ssh/**)", "Bash(cat ~/.ssh/*)", - "Read(~/.ssh/**)" + "Read(~/.ssh/**)", + "Read(~/.aws/**)", + "Read(**/.env)" + ], + "ask": [ + "Bash(rm *)", + "Bash(git reset --hard*)", + "Bash(git clean -*)", + "Bash(gh pr merge *)" ], "defaultMode": "auto" }, - "model": "sonnet", + "autoMode": { + "hard_deny": [ + "$defaults", + "Never read, print, copy, or transmit SSH keys, cloud credentials, or .env contents, by any means including cat, head, grep, cp, base64, or a script" + ], + "soft_deny": [ + "$defaults", + "Never discard uncommitted work (git reset --hard, git clean, git checkout --)", + "Never rewrite published history, force-push, or delete a remote branch", + "Never merge, close, or delete a pull request, release, or repository" + ] + }, + "model": "opus", "hooks": { "SessionStart": [ { @@ -43,6 +66,17 @@ "command": "rtk hook claude" } ] + }, + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "if": "Bash(git push*)", + "command": "bash ~/.dotfiles/.claude/hooks/git-push-protected-branch.sh", + "statusMessage": "Checking push target" + } + ] } ] },