A Claude Code hook that nudges granular commits after every turn. Green squares don't grow themselves.
Your GitHub contribution graph is a lie of omission. You code all day with Claude, but your graph stays grey because nobody commits mid-conversation. This hook fixes that. Every time Claude finishes a response, it gets one instruction: commit now. That's it. Your graph goes from "does this person even code?" to "someone call a doctor."
@bcherny hit 266 contributions in a single day. I looked at my contribution graph and felt personally attacked.
The problem is simple: you're coding with Claude Code for hours, making real progress, but your GitHub graph doesn't know that. Commits pile up in your working tree. You push once at the end of the day. GitHub thinks you worked for 5 minutes.
This hook turns every Claude interaction into a commit. Is it useful for granular commit history? Sure. But let's be honest -- it's mostly about the green squares.
BEFORE (sad, grey, concerning)
Mon . . . . . . . . . . . .
Wed . . . . # . . . . . . .
Fri . . . . . . . . . . . .
AFTER (commitmaxxing)
Mon # # # # # # # # # # # #
Wed # # # # # # # # # # # #
Fri # # # # # # # # # # # #
Same amount of work. Different graph. Different you.
/plugin marketplace add longevityboris/commitmaxxing-hook
/plugin install commitmaxxingThat's it. The Stop and SubagentStop hooks register themselves.
1. Download the hook:
mkdir -p ~/.claude/hooks
curl -o ~/.claude/hooks/commitmaxxing.sh \
https://raw.githubusercontent.com/longevityboris/commitmaxxing-hook/main/commitmaxxing.sh
chmod +x ~/.claude/hooks/commitmaxxing.sh2. Add to ~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/commitmaxxing.sh",
"timeout": 15
}
]
}
],
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/commitmaxxing.sh",
"timeout": 15
}
]
}
]
}
}Done. Start a Claude Code session and watch the commits roll in.
Stop hook fires after every Claude response. The script delivers its message via exit 2 + stderr — the documented path that actually injects feedback into Claude's context for Stop events. Claude sees "uncommitted changes present, commit before stopping" and commits. Your graph goes green.
#!/usr/bin/env bash
input="$(cat)"
# Loop guard — Claude Code's stop_hook_active flag prevents infinite re-entry.
case "$input" in
*'"stop_hook_active":true'*|*'"stop_hook_active": true'*) exit 0 ;;
esac
cd "${CLAUDE_PROJECT_DIR:-$PWD}" >/dev/null 2>&1 || exit 0
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
git_dir="$(git rev-parse --git-dir 2>/dev/null)" || exit 0
# Don't fight a parallel git op or interrupt in-progress git states.
[ -e "$git_dir/index.lock" ] && exit 0
for m in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD REBASE_HEAD BISECT_LOG rebase-merge rebase-apply sequencer; do
p="$(git rev-parse --git-path "$m" 2>/dev/null)" || exit 0
[ -e "$p" ] && exit 0
done
# Skip detached HEAD; only nudge on real tracked changes (no untracked junk).
git symbolic-ref -q HEAD >/dev/null 2>&1 || exit 0
git status --porcelain=v1 --ignore-submodules=dirty 2>/dev/null \
| grep -qE '^[ MADRCU]' || exit 0
msg="Uncommitted changes present. If this is a coherent checkpoint, commit with a descriptive message before stopping. If still mid-flight, keep going until a sane commit boundary."
# Surface green-squares failure modes.
remote_url="$(git remote get-url origin 2>/dev/null)"
if [ -z "$remote_url" ]; then
msg="$msg Note: no 'origin' remote configured — local commits won't sync anywhere."
elif ! printf '%s' "$remote_url" | grep -q "github.com"; then
msg="$msg Note: 'origin' is not GitHub — commits won't appear on your GitHub contribution graph."
fi
# Nudge to push when commits accumulate locally.
upstream="$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null)"
if [ -n "$upstream" ]; then
ahead="$(git rev-list --count "@{u}..HEAD" 2>/dev/null || echo 0)"
if [ "${ahead:-0}" -ge 3 ]; then
msg="$msg You have $ahead unpushed commits — consider pushing."
fi
fi
echo "$msg" >&2
exit 2This started as five lines. Those five lines printed JSON with hookSpecificOutput.additionalContext — and additionalContext is only valid for UserPromptSubmit and SessionStart hooks. For Stop, Claude Code silently drops it. So the original version was advertising a behavior it never delivered.
The current version uses exit 2 + stderr, which is the documented mechanism for Stop per the hooks reference. The extra lines are guards, not features — and most of them are early-exits, so the script does nothing in 99% of states.
- Pure bash, zero dependencies. No
jq, no Python. - Loop-safe. Honors
stop_hook_activeso it never traps Claude in an infinite stop loop. - Workflow-safe. Stays silent during rebase, merge, cherry-pick, revert, bisect, sequencer state, detached HEAD, or
.git/index.lockcontention. - Quiet on clean trees. Only nudges when there are real tracked changes — never on untracked junk alone.
- Green-squares aware. Warns when
originis missing or non-GitHub (so you know your graph won't fill). - Push-aware. Suggests pushing once 3+ unpushed commits accumulate.
- Descriptive commits. Claude writes the messages — and Claude is good at it.
| Case | Behavior |
|---|---|
| Clean tree | silent (no nudge) |
| Only untracked files | silent (no junk commits) |
| Mid-rebase / merge / cherry-pick / bisect / revert | silent (no interference) |
| Detached HEAD | silent (no orphan commits) |
.git/index.lock present |
silent (no race with concurrent git) |
| Empty repo (no HEAD yet) | silent |
| Outside any git repo | silent |
stop_hook_active=true |
silent (no infinite loop) |
| Tracked changes + GitHub remote | nudge to commit |
| Tracked changes + no remote | nudge + warn local-only |
| Tracked changes + non-GitHub remote | nudge + warn no green squares |
| 3+ unpushed commits | nudge + suggest push |
See CONTRIBUTING.md. Improvements welcome — especially edge cases this script doesn't handle yet.
MIT. Go turn your contribution graph radioactive.