chore(config): add git-lock-guard PreToolUse hook

Clears stale .git/index.lock files before git write commands.
Claude Code's internal git status polling leaves orphan locks
(anthropics/claude-code#11005) that block add/commit/merge/push.

The hook checks lsof/fuser before removing — only stale locks are
cleared, never locks held by live processes. Read-only git commands
are skipped entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 09:57:30 +01:00
co-authored by Claude Opus 4.6
parent 2f8218400d
commit ce8c748462
2 changed files with 77 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
#!/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
+14
View File
@@ -80,5 +80,19 @@
"Bash(git clean -f *)",
"Bash(rm -rf *)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/git-lock-guard.sh",
"timeout": 5
}
]
}
]
}
}