Last active
May 15, 2026 08:55
-
-
Save ondrasek/f157866a809e05072f0887200b6cdde3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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."$'\n'"Spawn a sub-agent using the Agent tool (subagent_type: general-purpose) to handle this — do not perform the git work yourself in the main conversation. Give the sub-agent the full context above. The sub-agent's ultimate goal is to leave the working tree clean and the branch pushed, by carrying out this full sequence in one pass: (1) integrate remote changes via 'git pull' (creating a merge commit; do NOT rebase or squash), resolving any merge conflicts, (2) stage any intended changes — tracking new files or adding them to .gitignore, (3) commit with a Conventional Commits message (https://www.conventionalcommits.org) including a detailed body explaining what and why and a 'Co-Authored-By: Claude <noreply@anthropic.com>' trailer, (4) push to origin. Currently blocked on step 1: integrating origin/$BRANCH into the local branch. Tell the sub-agent to continue through steps 2-4 after integration so the next hook run finds a clean tree." | |
| 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'"Spawn a sub-agent using the Agent tool (subagent_type: general-purpose) to handle this — do not perform the git work yourself in the main conversation. Give the sub-agent the full context above. The sub-agent's ultimate goal is to leave the working tree clean and the branch pushed, by carrying out this full sequence in one pass: (1) integrate any remote changes via 'git pull' (creating a merge commit; do NOT rebase or squash), resolving any merge conflicts, (2) stage all intended changes — classifying every listed untracked file as either 'git add'-worthy or '.gitignore'-worthy and acting accordingly, plus staging any other modified tracked files, (3) commit with a Conventional Commits message (https://www.conventionalcommits.org) including a detailed body explaining what and why and a 'Co-Authored-By: Claude <noreply@anthropic.com>' trailer, (4) push to origin. Currently blocked on step 2: classifying and handling the untracked files above. Tell the sub-agent to continue through steps 3-4 after that so the next hook run finds a clean tree." | |
| 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'"Spawn a sub-agent using the Agent tool (subagent_type: general-purpose) to handle this — do not perform the git work yourself in the main conversation. Give the sub-agent the full context above. The sub-agent's ultimate goal is to leave the working tree clean and the branch pushed, by carrying out this full sequence in one pass: (1) integrate any remote changes via 'git pull' (creating a merge commit; do NOT rebase or squash), resolving any merge conflicts, (2) stage all modified tracked files listed above, (3) commit with a Conventional Commits message (https://www.conventionalcommits.org) including a detailed body explaining what and why and a 'Co-Authored-By: Claude <noreply@anthropic.com>' trailer, (4) push to origin. Currently blocked on step 2: staging the modified files above. Tell the sub-agent to continue through steps 3-4 after staging so the next hook run finds a clean tree." | |
| 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'"Spawn a sub-agent using the Agent tool (subagent_type: general-purpose) to handle this — do not perform the git work yourself in the main conversation. Give the sub-agent the full context above. The sub-agent's ultimate goal is to leave the working tree clean and the branch pushed, by carrying out this full sequence in one pass: (1) integrate any remote changes via 'git pull' (creating a merge commit; do NOT rebase or squash), resolving any merge conflicts, (2) confirm everything intended is staged, (3) commit the staged changes using a Conventional Commits message (https://www.conventionalcommits.org) with a detailed body explaining what changed and why and a 'Co-Authored-By: Claude <noreply@anthropic.com>' trailer, (4) push to origin. Currently blocked on step 3: committing the staged files above. Tell the sub-agent to continue through step 4 immediately after committing so the next hook run finds a clean tree." | |
| 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."$'\n'"Spawn a sub-agent using the Agent tool (subagent_type: general-purpose) to handle this — do not perform the git work yourself in the main conversation. Give the sub-agent the full context above. The sub-agent's ultimate goal is to leave the working tree clean and the branch pushed, by carrying out this full sequence in one pass: (1) integrate any remote changes via 'git pull' (creating a merge commit; do NOT rebase or squash) if origin/$BRANCH has moved, (2) stage any intended changes, (3) commit with a Conventional Commits message (https://www.conventionalcommits.org) including a detailed body and a 'Co-Authored-By: Claude <noreply@anthropic.com>' trailer, (4) push $BRANCH to origin. Currently blocked on step 4: pushing the $unpushed_count local commit(s). Tell the sub-agent to push now so the next hook run finds a clean tree." | |
| fi | |
| fi | |
| exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment