#!/usr/bin/env bash # git-lock-guard: PreToolUse hook that clears stale git index.lock files # before git write operations. # # Claude Code's internal git status polling creates stale lock files that # persist after the process exits (anthropics/claude-code#11005). This hook # detects and removes them before LLM-initiated git commands run. # # Only acts on git write commands (add, commit, merge, push, etc.). # Read-only commands (status, log, diff, show) are skipped. set -uo pipefail INPUT=$(cat) # Only act on Bash tool calls TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null) [ "$TOOL_NAME" = "Bash" ] || exit 0 COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null) # Only act on git write commands — skip read-only operations case "$COMMAND" in git\ add*|git\ commit*|git\ merge*|git\ push*|git\ pull*|\ git\ checkout*|git\ stash*|git\ rebase*|git\ reset*|\ git\ cherry-pick*|git\ rm*|git\ mv*|git\ fetch*) ;; *) exit 0 ;; esac # Resolve the git directory for the current working directory CWD=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('cwd',''))" 2>/dev/null) [ -n "$CWD" ] || exit 0 # Find the .git directory (handles both regular repos and worktrees) GIT_DIR=$(git -C "$CWD" rev-parse --git-dir 2>/dev/null) || exit 0 # For worktrees, also check the common git dir (shared index operations) GIT_COMMON_DIR=$(git -C "$CWD" rev-parse --git-common-dir 2>/dev/null) || GIT_COMMON_DIR="$GIT_DIR" for DIR in "$GIT_DIR" "$GIT_COMMON_DIR"; do LOCK="$DIR/index.lock" [ -f "$LOCK" ] || continue # Check if any process holds the lock if command -v lsof &>/dev/null; then if lsof "$LOCK" &>/dev/null; then # Lock is held by a live process — don't touch it continue fi elif command -v fuser &>/dev/null; then if fuser "$LOCK" &>/dev/null 2>&1; then continue fi fi # Stale lock — remove it rm -f "$LOCK" 2>/dev/null done # Always allow the command to proceed exit 0