#!/bin/bash # --- work from the project root ---------------------------------------------- # Every path below is repo-relative, so a hook invoked with a working directory # that is not the repo root would silently read and write the WRONG TREE -- # returning a near-empty result instead of the session-recovery block, and # creating stray trees such as docs/production/session-logs/ on write. # # PRECEDENCE IS LOAD-BEARING. A cwd that IS a project root carries real # information and must win: a caller sitting inside another project means that # project, not this one. Resolving to the script's own location first would # override them. So, in order: # 1. cwd holds project.yaml -> cwd (a project root) # 2. cwd holds .claude/ -> cwd (a project root not yet configured) # 3. CLAUDE_PROJECT_DIR -> that (populated in the hook environment) # 4. this script's location -> /.claude/hooks/../.. by construction # Rule 4 always works and needs no environment at all; rules 1-2 stop it from # overriding a caller that legitimately means somewhere else. # # NOT an upward search: that resolves a nested project to its parent's config. if [ -f "project.yaml" ] || [ -d ".claude" ]; then CCGS_ROOT="$PWD" elif [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "${CLAUDE_PROJECT_DIR}" ]; then CCGS_ROOT="$CLAUDE_PROJECT_DIR" else CCGS_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." 2>/dev/null && pwd)" fi [ -n "$CCGS_ROOT" ] && cd "$CCGS_ROOT" 2>/dev/null || true # Claude Code PreToolUse hook: Validates git push commands # Warns on pushes to protected branches # Exit 0 = allow, Exit 2 = block # # Input schema (PreToolUse for Bash): # { "tool_name": "Bash", "tool_input": { "command": "git push origin main" } } INPUT=$(cat) # Parse command -- use jq if available, fall back to grep if command -v jq >/dev/null 2>&1; then COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') else COMMAND=$(echo "$INPUT" | grep -oE '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/"command"[[:space:]]*:[[:space:]]*"//;s/"$//') fi # Only process git push commands if ! echo "$COMMAND" | grep -qE '^git[[:space:]]+push'; then exit 0 fi CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) MATCHED_BRANCH="" # Check if pushing to a protected branch for branch in develop main master; do if [ "$CURRENT_BRANCH" = "$branch" ]; then MATCHED_BRANCH="$branch" break fi # Also check if pushing to a protected branch explicitly (quote branch name for safety) if echo "$COMMAND" | grep -qE "[[:space:]]${branch}([[:space:]]|$)"; then MATCHED_BRANCH="$branch" break fi done if [ -n "$MATCHED_BRANCH" ]; then echo "Push to protected branch '$MATCHED_BRANCH' detected." >&2 echo "Reminder: Ensure build passes, unit tests pass, and no S1/S2 bugs exist." >&2 # Allow the push but warn -- uncomment below to block instead: # echo "BLOCKED: Run tests before pushing to $CURRENT_BRANCH" >&2 # exit 2 fi exit 0