auto-git
Automatic, per-round git safety net for code projects. The goal is simple: the user should never lose a round of work, and their history should be readable without them having to think about committing.
The two-part design (read this first)
This skill combines two mechanisms because they have different strengths:
A Stop hook — the backbone. A project-local
autocommithook commits any uncommitted changes after every round, automatically, whether or not anyone remembers. The harness runs it, so it cannot be forgotten. This is what guarantees nothing is lost. It only commits — everything it does is local.Two optional companions are offered separately during setup and never installed by default (see step 4):
progress-check— if a round changed files but left both progress files (status.md,progress.md) untouched, it blocks the stop once with a reminder so you update them while you still have the context (it never writes content itself, and astop_hook_activeguard means it can never loop).autopush— pushes afterautocommit, and only when the branch already has an upstream and is ahead of it. Commit and push are deliberately separate hooks: committing is a local safety net that costs nothing, while pushing publishes work to a remote, and that should be its own decision.
You writing real commit messages — the polish. A hook is a dumb shell script; it can only write a generic timestamped message. A commit message that actually says what changed can only come from you, during the turn. So when you're working in an auto-git project, you commit meaningful changes yourself as the last step of a round. The hook then finds a clean tree and does nothing (or sweeps up anything you left behind).
The result: meaningful messages whenever you're active, and a guaranteed commit every round regardless.
When to activate
Activate (run the setup below) when a code project is being started or set up and auto-git is not already installed. Signals that it's a code project:
- The user writes or asks for a
CLAUDE.md, or gives a project overview that describes building software. - Source files or package manifests are present or being created:
package.json,requirements.txt/pyproject.toml,go.mod,Cargo.toml,*.csproj/*.sln,pom.xml/build.gradle,Gemfile,composer.json, asrc/directory of code, etc. - The user asks to set up version control, auto-commit, or "stop losing work".
Do not activate for pure writing, research notes, or one-off git questions. If it's ambiguous whether the user wants this, ask once rather than assuming.
Setup procedure
Every step is idempotent — check the current state before acting, and re-running setup on an already-configured project should change nothing.
0. Bail out early if already set up
If .claude/settings.json already contains a Stop hook that runs
autocommit, auto-git is installed. Briefly confirm the pieces exist
(.gitignore, the hook scripts, the initial commit) and stop. Don't duplicate
anything.
Three exceptions — migrate before you stop. Updating this skill never reaches projects that were set up earlier, so an existing install has to be repaired on the spot:
Refresh stale hook scripts (do this whenever it applies). Every script in
scripts/carries a# auto-git-version: Nstamp within its first two lines. Compare each against the same stamp in the project's.claude/hooks/. If an installed script has a lower stamp, or none at all, it predates fixes it needs: overwrite it fromscripts/— every script the project has installed, not just the one you happened to check. An unstampedautocommitis the version that pinned itself toCLAUDE_PROJECT_DIRand commits the main checkout instead of the worktree Claude is in; it also pushes, which is nowautopush's job alone.Also check the registered Stop command itself: if it uses a relative path (
bash .claude/hooks/autocommit.sh), rewrite it to the absolute$CLAUDE_PROJECT_DIRform from step 4. A relative path is resolved against the session's cwd, so the hook dies withNo such file or directorythe moment Claude works anywhere but the main checkout — a git worktree, most commonly.Refreshing scripts must not change which hooks are registered. If the project has no
autopush, leave it that way — its absence means the user didn't opt in, and silently starting to push their work would be exactly the surprise this skill exists to avoid. Offer it only if they ask (step 4).Worktree-ignore repair. If
.gitignorehas no.claude/worktrees/line, add it (step 3). Then check whether a worktree already got committed:git ls-files -s .claude/worktreesprinting anything means the main branch is carrying a phantom gitlink. Drop it withgit rm --cached -r --quiet .claude/worktreesand commit — that only adds a commit, rewrites nothing, and leaves the worktree on disk untouched.Layout migration. If the project has a
progress.mdbut nostatus.md, it was set up under the old single-file layout. Do the split from step 5 and the note upgrade from step 6.
Then stop.
Note: autocommit present without progress-check is a normal,
complete install — progress-check is opt-in, and its absence usually means
the user declined it. Don't re-offer it on every re-run; add it (per step 4,
inserted before the autocommit entry) only if the user asks for it.
1. Initialize the repository (if needed)
Nested-repo guard first: before running git init, check
git rev-parse --is-inside-work-tree. If it succeeds, you're already inside a
repo — compare git rev-parse --show-toplevel to the current directory. If they
match, this directory is already a repo (skip init). If the toplevel is a
parent directory, you'd be creating a repo nested inside another one — stop and
ask the user first, since that's rarely what they want.
Otherwise initialize:
git init -b main # sets the initial branch to main
If the running git is too old for -b, fall back to git init and rename the
branch after the first commit with git branch -M main.
2. Ensure a git identity is configured
Commits made by the hook run non-interactively, so a git identity must be
configured or every commit — including the initial one — fails silently with
exit 128. Do this after git init, because setting a repo-local identity
requires the repo to already exist. Check:
git config user.email
git config user.name
If either is empty (no global or local value), ask the user for the name/email to use, then set it — locally for this repo unless they prefer global:
git config user.email "them@example.com"
git config user.name "Their Name"
3. Write .gitignore
The point is to keep "irrelevant" files out of history: build output, dependencies, secrets, and OS/editor junk.
- If no
.gitignoreexists, create one fromassets/gitignore-base.txt, then append the language-specific block(s) for whatever stack you detected. The per-language blocks are inreferences/gitignore-languages.md— read it and copy the relevant sections. - If a
.gitignorealready exists, do not overwrite it. Read it, and append only the critical entries it's missing (especially the secrets block:.env,*.pem,*.key, credentials). Mention to the user what you added.
Two entries are mandatory either way:
- The secrets block. Secrets must never be committed.
.claude/worktrees/. Claude Code puts its worktrees there, inside the main checkout. Without this linegit add -Arecords a worktree as a phantom gitlink on the main branch — see the Git worktrees section.
4. Install the Stop hooks — ask for explicit approval first
A Stop hook runs a shell command automatically after every round, so Claude Code treats installing one as security-sensitive: under auto-accept / auto mode the change is intercepted by the safety classifier rather than applied silently. That's correct behavior — a hook should never appear without the user knowing. So don't try to slip it in via auto-accept. Make installation a deliberate, approved step.
Show the user exactly what will be installed, then get a yes/no before writing
anything. Present the hook script's path and the precise command that will run
each round, and ask with a clear approve/decline prompt (use AskUserQuestion, or
an equivalent explicit choice) — for example: "Install the auto-commit Stop
hook? It will run bash "$CLAUDE_PROJECT_DIR/.claude/hooks/autocommit.sh" after
every round to commit your changes."
- If the user approves, copy the script(s) and write the hook(s) (below).
They'll also see Claude Code's own edit-permission prompt for
settings.json— that's expected, and now they know to accept it. - If the user declines, skip the hooks entirely and continue with the rest of
setup. Everything else still works — git init,
.gitignore, progress files, and the per-round commits you make by hand. Tell the user plainly that without the hook there's no automatic safety net, so they're relying on your end-of-round commits.
The optional progress-check hook — always ask, never install by default.
When the user approves autocommit, offer this one as a separate, explicit
choice, and spell out the consequence so they can actually decide — for example:
"Optionally, also install the
progress-checkStop hook? What it changes: whenever a round modified files but left bothstatus.mdandprogress.mduntouched, the hook blocks the stop once and I keep working — updating the progress files and committing — before the round truly ends. That keeps them reliably current, but some rounds take one extra beat, and it can also fire when you edited files yourself and only asked me a question. If you skip it, nothing else changes: autocommit still commits everything; progress updates just rely on my per-round routine alone."
Install it only on an explicit yes. On a no — or no clear answer — don't register it, and don't re-ask in later rounds.
The optional autopush hook — always ask, never install by default.
autocommit never pushes; committing is local and costs nothing, while pushing
publishes the user's work, so it is a separate hook and a separate decision.
Offer it as its own explicit choice — for example:
"Optionally, also install the
autopushStop hook? It pushes after each round's commit, but only when the current branch already has an upstream and is ahead of it — no remote configured means it does nothing at all. It never creates remotes or upstreams and never force-pushes. Say no and everything stays local until you push yourself."
Install it only on an explicit yes. If the project has no remote yet, still ask rather than assuming — the hook is harmless until an upstream exists, and it is the user's call whether adding a remote later should start publishing automatically.
When approved, copy the hook script(s) for this OS and register them:
- Windows: copy
scripts/autocommit.ps1→.claude/hooks/(plusscripts/progress-check.ps1andscripts/autopush.ps1, each only if opted in). Hook commands:- progress-check (if opted in):
pwsh -NoProfile -File "$CLAUDE_PROJECT_DIR/.claude/hooks/progress-check.ps1" - autocommit:
pwsh -NoProfile -File "$CLAUDE_PROJECT_DIR/.claude/hooks/autocommit.ps1" - autopush (if opted in):
pwsh -NoProfile -File "$CLAUDE_PROJECT_DIR/.claude/hooks/autopush.ps1"(fall back topowershell -NoProfile -ExecutionPolicy Bypass -File ...ifpwshisn't available). The.shscripts work on Windows too — Claude Code runs hook commands through Git Bash — so either pair is fine there.
- progress-check (if opted in):
- macOS / Linux: copy
scripts/autocommit.sh(plusscripts/progress-check.shandscripts/autopush.sh, each only if opted in) →.claude/hooks/and mark them executable (chmod +x). Hook commands:- progress-check (if opted in):
bash "$CLAUDE_PROJECT_DIR/.claude/hooks/progress-check.sh" - autocommit:
bash "$CLAUDE_PROJECT_DIR/.claude/hooks/autocommit.sh" - autopush (if opted in):
bash "$CLAUDE_PROJECT_DIR/.claude/hooks/autopush.sh"
- progress-check (if opted in):
Always register the absolute $CLAUDE_PROJECT_DIR form, never a bare relative
path. The harness expands that variable in the command string (on Windows
too), and a relative path is resolved against the session's cwd — which stops
being the main checkout as soon as Claude enters a git worktree, and the hook
then fails with No such file or directory. Quote the path: project paths
contain spaces.
Register by merging into any existing .claude/settings.json (never
clobber other settings or hooks). With autocommit alone the shape is:
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "<autocommit command from above>" }
]
}
]
}
}
Each hook the user opted into joins that same inner array. With all three:
"hooks": [
{ "type": "command", "command": "<progress-check command from above>" },
{ "type": "command", "command": "<autocommit command from above>" },
{ "type": "command", "command": "<autopush command from above>" }
]
Order matters, and it is always this one: progress-check → autocommit →
autopush. progress-check has to see the dirty tree before autocommit
sweeps it clean, and autopush can only push what autocommit has already
committed. Drop whichever entries the user declined; the survivors keep their
relative order.
If a Stop array already exists, add these entries to it rather than replacing
it. Don't add duplicates if these commands are already present.
5. Set up progress tracking (status.md + progress.md)
auto-git also keeps the project's progress files current, so the state of the work is always readable and survives context loss between sessions. There are two files, with opposite semantics — the whole point is that they never blur into each other:
| file | what it is | write semantics |
|---|---|---|
status.md |
a snapshot of the project now | overwrite — delete what no longer applies |
progress.md |
the project's history — how it got here | append-only — never edit past entries |
During setup, create both from assets/status-template.md and
assets/progress-template.md if they don't already exist (never overwrite an
existing one).
status.md — the snapshot. It answers "where is this project right now?":
# Status
## Current status
## Active decisions & constraints
## Next steps
## Known issues
Every section has overwrite semantics — updating means rewriting it to current
truth and deleting what no longer applies: completed steps, superseded
decisions, fixed issues. Deleting here is safe and expected: anything worth
remembering was written to progress.md when it happened, and every previous
version is one git log -p -- status.md away. Two writing rules keep the
snapshot trustworthy:
- Claims carry pointers. "Auth done (src/auth.ts)" — a later session can verify that in seconds; "auth done" alone it can only believe or doubt.
- Size tracks task complexity, not elapsed time. A
status.mdthat only ever grows has become a ledger — trim it on the spot.
progress.md — the history. Append-only, newest entry at the top, one
entry per meaningful node:
# Progress
## 2026-08-05 14:30 — Login endpoint landed
- JWT verification in src/auth.ts
- Fixed sessions not refreshing on expiry
## 2026-08-05 11:02 — Project initialized
- Scaffold + .gitignore
"Meaningful node" is the filter that keeps this file worth reading: a feature
landing, a decision made, a real bug fixed, a direction changed, a milestone
reached. Not every round. Typo fixes, formatting passes, and tiny tweaks get
no entry — git log already has them, and padding the history with them is what
turns it into noise. Past entries are the record: never rewrite or delete them,
only prepend new ones.
Migrating an older project (do this, don't skip it). If the project has a
progress.md that is really a snapshot — either the four sections above, or
this skill's oldest shape with a "Current status" plus an append-only ## Log
— split it now:
- Create
status.mdand move the snapshot sections into it verbatim. - Leave
progress.mdholding history only. If there was a## Log, convert its entries into top-level## <date> — <summary>entries, reordered newest-first. If there was no log at all, start it with a single entry noting the split and the state at that point. - Update the project's
CLAUDE.mdnote (step 6) to the two-file wording.
Never drop content during the split — every line either moves to status.md or
stays in progress.md.
Leave any other progress-type files the project already has (plan.md,
TODO.md, ROADMAP.md, a changelog, etc.) in place — you'll keep them current
each round too. Don't fabricate a planning structure the user didn't ask for
beyond these two files.
6. Record the per-round instructions in CLAUDE.md
Future sessions won't have this skill loaded, so leave a durable reminder that
carries the per-round behavior forward. Append this section to the project's
CLAUDE.md (create the file if it doesn't exist), unless an equivalent note is
already there:
## Auto-git: per-round routine
Two progress files, opposite semantics — keep them distinct:
- `status.md` — a snapshot of the project **now**. Overwrite semantics.
- `progress.md` — the project's **history**. Append-only, newest at the top.
At the end of any round that changed the project, do these before finishing:
1. **Rewrite `status.md`** to current truth — sections (Current status / Active
decisions & constraints / Next steps / Known issues) updated, and anything
completed, superseded, or fixed **deleted**. Deleting is safe: what mattered
is already in `progress.md`, and old versions are in `git log`. Give claims
pointers to where they live in the code ("auth done — src/auth.ts"). If the
file only ever grows, it is drifting into a ledger — trim it.
2. **Append to `progress.md` only if this round was a meaningful node** — a
feature landed, a decision made, a real bug fixed, a direction changed. Add
a new `## YYYY-MM-DD HH:MM — <one-line summary>` section at the **top** of
the file, with a few bullets under it. Never edit or delete existing
entries. Trivial rounds (typos, formatting, small tweaks) get no entry —
`git log` already has them.
3. **Refresh other progress files** that already exist — check off completed
items in `plan.md` / `TODO.md`, add a changelog line, etc.
4. **Update `CLAUDE.md` itself only when this round changed something it
documents** — architecture, key decisions, commands, or structure. Don't
rewrite it every round.
5. **Commit** with a clear, descriptive message as the final step. A Stop hook is
a safety net that commits anything left over. Commit locally; never rewrite
history or force-push. Whether commits also get pushed automatically depends
on whether this project installed the separate `autopush` hook — check
`.claude/settings.json` rather than assuming either way.
Rounds that only answered a question and changed nothing need no update or commit.
**Hook freshness — do this once when you notice it, not every round.** The
scripts in `.claude/hooks/` carry a `# auto-git-version: N` stamp. If the
auto-git skill is available in the session and its `scripts/` carry a higher
number, the copies here predate fixes they need: overwrite them from the skill,
leave the set of registered hooks unchanged, and commit. The copies are
per-project, so updating the skill never reaches this project on its own.
Upgrade an older note in place — don't just skip it. "An equivalent note is
already there" means one that already carries this two-file discipline, not merely
any auto-git mention. If the project's CLAUDE.md has an older or weaker version
of this note — one that treats progress.md as the snapshot, or that still tells
sessions to keep an append-only "Log" inside it — rewrite it to the wording above
instead of leaving it stale, and split the files per step 5. Preserve any deliberate
project-specific choices the old note encoded (non-default progress-file names, a
project working language, an explicit no-push policy) and change only
the progress-file discipline. One caveat: if the project's progress files are
gitignored, keep the "delete freely, history is in git log" clause out of its note
— with nothing versioned, status.md deletions lose history unless the corresponding
progress.md entry was written, so make writing that entry the precondition for
trimming. This in-place upgrade is what keeps existing projects from drifting when
this skill is later updated — updating the central skill never touches a project
that was set up earlier, so the note has to be refreshed the next time setup runs
there.
7. Make the initial commit
Stage everything that isn't ignored and commit the baseline — include all files currently present:
git add -A
git commit -m "chore: initial commit (auto-git baseline)"
Then tell the user what you set up in a couple of lines: repo initialized,
.gitignore written, status.md + progress.md created, which Stop hooks are
installed (and which they declined), and the initial commit made. Be explicit
about pushing: if they skipped autopush, say plainly that nothing will ever
leave the machine on its own; if they took it, say it stays dormant until they
add a remote and an upstream.
Working in an auto-git project (every round after setup)
When a round changed the project, wrap it up in this order before you finish:
- Rewrite
status.md. It is the snapshot of the project now: sections to current truth, deleting anything completed, superseded, or fixed, claims with pointers into the code. This happens every round that changed something. - Prepend to
progress.md— only if the round was a meaningful node. A feature landed, a decision made, a real bug fixed, a direction changed. New## YYYY-MM-DD HH:MM — <summary>entry at the top, a few bullets under it, existing entries untouched. A round that fixed a typo or reformatted something gets nothing here. - Refresh the rest. Check off / adjust any existing
plan.md,TODO.md, changelog, etc. UpdateCLAUDE.mdonly if this round changed something it documents (architecture, decisions, commands, structure) — don't churn it every round. - Commit last. Stage and commit with a message that describes what actually changed — a real subject line, not a timestamp. Committing last (after the progress updates) means those updates land in the same commit and the hook doesn't have to sweep up after you.
Group a round's work into one sensible commit when you can; a couple of commits
is fine if the round did genuinely separate things. Pushing is not automatic
unless this project installed the optional autopush hook — without it, commits
stay local until someone pushes.
A round that only answered a question and changed nothing needs no progress
update and no commit. And if you forget the wrap-up, nothing is lost: if the
optional progress-check hook is installed it blocks once with a reminder so
you can catch up, and either way the autocommit hook commits the leftovers
with a generic message — the routine just keeps status.md, progress.md, and
git history readable.
Git worktrees
Claude Code can move a session into a git worktree (its own live under
.claude/worktrees/<branch>). Three things follow, and the hooks are built for
all three:
- The session's cwd becomes the worktree, while
CLAUDE_PROJECT_DIRkeeps pointing at the main checkout. So the hook command must reference the script through$CLAUDE_PROJECT_DIR(that's where.claude/hooks/physically lives), while the script itself must operate on the cwd, which is the tree actually being edited. The scripts resolve that from the hook payload'scwdfield, falling back to the process cwd and thenCLAUDE_PROJECT_DIR, and use the first one that's inside a git work tree. Result: work on a worktree branch is committed to that branch, and the main checkout is left alone. .claude/hooks/and.claude/settings.jsonare tracked, so a worktree gets its own copies. That is deliberate. It means a session started directly inside a hand-made worktree still loads the hooks, and auto-git is active there with nothing copied by hand. It also means anyone who clones the repo inherits the hooks — which is intended, and is why installing them takes explicit approval in step 4..claude/worktrees/must be gitignored (step 3). Being under.claude/does not exclude it — nothing else in there is excluded either. Without that line,git add -Ain the main checkout sees a nested repository and records the worktree as a phantom gitlink on the main branch, which then re-commits every time the worktree's branch moves. Step 0 repairs projects set up before the line existed.
Safety rules (non-negotiable)
These exist because auto-committing tools are dangerous when they're too aggressive — the whole point is to help, never to surprise or destroy:
- Local-first, no history rewriting. Never
git reset --hard,rebase,commit --amend, force-push, or anything that discards work. Only ever add commits. - Committing never publishes.
autocommitonly commits; it cannot push. Pushing lives inautopush, a separate hook the user has to opt into, so a project never starts sending work to a remote because it got a safety net. - Push only when it's safe.
autopushpushes solely when the current branch already has an upstream tracking branch and is ahead of it. It never creates remotes, sets upstreams, or force-pushes. If a push fails (e.g. the remote moved), it stays quiet and leaves the commit local — it does not retry destructively. - Never commit a worktree into its parent.
.gitignoremust contain.claude/worktrees/. Without it,git add -Ain the main checkout records the worktree as a phantom gitlink on the main branch. - Never commit secrets. The
.gitignoresecrets block is mandatory. If you notice a secret-looking file that isn't ignored, flag it rather than committing it. - Idempotent. Re-running setup must never duplicate hooks or clobber the
user's
.gitignore/ settings.
Bundled resources
scripts/autocommit.ps1— Windows Stop-hook script (the commit safety net; it does not push).scripts/autocommit.sh— macOS/Linux Stop-hook script.scripts/autopush.ps1— Windows Stop-hook script (safe push; opt-in only — asked separately during setup, registered after autocommit when accepted).scripts/autopush.sh— macOS/Linux counterpart.scripts/progress-check.ps1— Windows Stop-hook script (stale progress-file reminder; opt-in only — asked separately during setup, registered before autocommit when accepted).scripts/progress-check.sh— macOS/Linux counterpart.assets/gitignore-base.txt— universal.gitignorestarting point.assets/status-template.md— starting structure forstatus.md(the overwrite-semantics snapshot of current state).assets/progress-template.md— starting structure forprogress.md(the append-only, newest-first history).references/gitignore-languages.md— per-language.gitignoreblocks to append based on the detected stack.