Skip to content

Instantly share code, notes, and snippets.

@ondrasek
Last active February 26, 2026 19:20
Show Gist options
  • Select an option

  • Save ondrasek/f157866a809e05072f0887200b6cdde3 to your computer and use it in GitHub Desktop.

Select an option

Save ondrasek/f157866a809e05072f0887200b6cdde3 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# Auto-commit hook: detects dirty git state and delegates to Claude via exit 2
# Follows fail-fast: detects the first problem, reports it, and exits 2.
# Claude fixes it, hook re-runs, catches the next issue, and so on.
#
# Check order (workflow sequence):
# 1. Behind remote → pull/rebase first
# 2. Untracked files → stage or gitignore
# 3. Unstaged changes → stage
# 4. Staged uncommitted → commit
# 5. Unpushed commits → push
#
# Exit code 0 = all clean, nothing to do
# Exit code 2 = blocking — Claude will act on the instructions printed to stderr
fail() {
echo "" >&2
echo "AUTO-COMMIT HOOK: $1" >&2
echo "$2" >&2
exit 2
}
# --- Guard: not inside a git repo → exit silently ---
git rev-parse --is-inside-work-tree &>/dev/null || exit 0
# Guard against infinite loop: exit if we're already in a hook
[ -n "$CLAUDE_HOOK_RUNNING" ] && exit 0
export CLAUDE_HOOK_RUNNING=1
# Navigate to the appropriate working tree.
# In a worktree .git is a file (gitdir: pointer), not a directory.
# If already in a worktree, stay here — CLAUDE_PROJECT_DIR may point to
# the main repo and we must not auto-commit the wrong branch.
_toplevel=$(git rev-parse --show-toplevel 2>/dev/null)
if [ -n "$_toplevel" ] && [ -f "$_toplevel/.git" ]; then
cd "$_toplevel"
else
cd "${CLAUDE_PROJECT_DIR:-.}"
fi
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
HAS_REMOTE=false
if [ -n "$BRANCH" ] && git rev-parse "origin/$BRANCH" &>/dev/null; then
HAS_REMOTE=true
fi
# ─── 1. Behind remote (must integrate before anything else) ──────────────────
if $HAS_REMOTE; then
behind=$(git log "HEAD..origin/$BRANCH" --oneline 2>/dev/null)
if [ -n "$behind" ]; then
behind_count=$(echo "$behind" | wc -l | tr -d ' ')
fail "Branch is behind remote — integrate now." \
"Your local branch is $behind_count commit(s) behind origin/$BRANCH. You must integrate the remote changes using git before doing anything else. Do not make any other changes until the branch is up to date."
fi
fi
# ─── 2. Untracked files (not gitignored) ────────────────────────────────────
untracked=$(git ls-files --others --exclude-standard)
if [ -n "$untracked" ]; then
file_count=$(echo "$untracked" | wc -l | tr -d ' ')
file_list=$(echo "$untracked" | head -20)
detail="$file_count untracked file(s) not in .gitignore:"$'\n'"$file_list"
[ "$file_count" -gt 20 ] && detail="$detail"$'\n'" ... and $((file_count - 20)) more"
fail "Untracked files must be dealt with." \
"$detail"$'\n'"You must handle every untracked file listed above. Use git to track files that belong in the repository, and add entries to .gitignore for files that do not. Do not leave any untracked files unresolved."
fi
# ─── 3. Unstaged modifications to tracked files ─────────────────────────────
unstaged=$(git diff --name-only)
if [ -n "$unstaged" ]; then
unstaged_count=$(echo "$unstaged" | wc -l | tr -d ' ')
unstaged_list=$(echo "$unstaged" | head -20)
detail="$unstaged_count tracked file(s) with unstaged changes:"$'\n'"$unstaged_list"
[ "$unstaged_count" -gt 20 ] && detail="$detail"$'\n'" ... and $((unstaged_count - 20)) more"
fail "Tracked files have unstaged modifications." \
"$detail"$'\n'"You must stage all modified tracked files listed above using git. Do not proceed until every modification is staged."
fi
# ─── 4. Staged but uncommitted changes ──────────────────────────────────────
staged=$(git diff --cached --name-only)
if [ -n "$staged" ]; then
staged_count=$(echo "$staged" | wc -l | tr -d ' ')
staged_list=$(echo "$staged" | head -20)
detail="$staged_count staged file(s) awaiting commit:"$'\n'"$staged_list"
[ "$staged_count" -gt 20 ] && detail="$detail"$'\n'" ... and $((staged_count - 20)) more"
fail "Staged changes must be committed." \
"$detail"$'\n'"You must commit all staged changes now using git. Follow Conventional Commits (https://www.conventionalcommits.org) for the message format. Write a detailed commit body explaining what changed and why. Include Co-Authored-By: Claude <noreply@anthropic.com> in the commit message."
fi
# ─── 5. Unpushed commits (ahead of remote) ──────────────────────────────────
if $HAS_REMOTE; then
unpushed=$(git log "origin/$BRANCH..HEAD" --oneline 2>/dev/null)
if [ -n "$unpushed" ]; then
unpushed_count=$(echo "$unpushed" | wc -l | tr -d ' ')
fail "Local commits must be pushed." \
"There are $unpushed_count unpushed commit(s) on $BRANCH that exist only locally. You must push them to origin using git. Do not continue until the remote is up to date with your local branch."
fi
fi
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment