AI in the Git Workflow
Before you start
You need: Git (Track 16) and Articles 01–06.
Time: about 45 minutes, at the keyboard.
Learning objective
Use Git so that any AI-assisted change is reviewable, reversible and attributable.
Topics
- Why Git matters more with AI
- Commit before generating
- Reading the diff
- Commit boundaries
- Commit messages and attribution
- Pull requests with generated code
- Recovering from an agent
- Automated checks
Why Git matters more with AI
AI produces more code, faster, and it produces code you did not write. Both raise the value of everything Git provides:
| Git gives you | Why it matters more now |
|---|---|
| A restore point | Undoing a bad generation costs one command |
| A diff | The only reliable account of what changed |
| Small commits | The unit you can actually review |
| Blame and history | Why a line exists, when nobody remembers writing it |
| Bisect | Finding which generated change broke something |
Without commit discipline, an agent editing files leaves you unable to tell what changed. With it, every generation is a diff you can read, accept or discard.
Commit before generating
git status # must be clean
git commit -am "Add fee summary endpoint"
# now generate
A clean working directory before generating is the single most useful habit in this article.
With it:
git diff # exactly what the AI changed, nothing else
git restore . # undo everything it did
Without it, your own uncommitted work and the AI's changes are mixed in one diff, and git restore destroys both.
Stash if you cannot commit:
git stash -u # -u includes untracked files
# generate
git stash pop
Reading the diff
git diff # unstaged
git diff --staged
git diff --stat # which files, how many lines
git diff -- src/Data/ # one folder
git add -p # review hunk by hunk while staging
Read the diff, not the AI's summary of what it did. A summary is a description; the diff is what happened. They differ more often than you would expect — an agent asked to change one method may reformat the file, add a using directive, or touch a second file it decided was related.
git diff --stat first. If you asked for one method and five files changed, stop and look at why before reading any content.
src/Data/FeeRepository.cs | 24 ++++++--
src/Services/FeeService.cs | 18 ++++--
src/Core/FeeAccount.cs | 3 +- ← you did not ask for this
appsettings.json | 2 +- ← or this
git add -p is the best review tool for generated code, because it forces a decision on every hunk: y to stage, n to skip, d to skip the rest of the file. Accept the good hunks, reject the rest.
Commit boundaries
One commit per verified change, as always — but with generated code there is a second rule:
Never mix generated and hand-written changes in one commit.
Bad: Add fee defaulters feature
[generated repository + your controller changes + a refactor]
Good: Add GetFeeDefaultersAsync to FeeRepository
Add fee defaulters endpoint
Extract due-date logic into FeePolicy
When the middle one turns out to be wrong, reverting it leaves the other two intact.
Refactor and behaviour change stay separate, exactly as in the Git track. Generated refactors change behaviour more often than they announce, so the separation matters more here.
Commit messages and attribution
Add fee defaulter query to FeeRepository
Returns fee accounts where the balance is above zero and the due date has
passed, for one school. Filters by SchoolId from the auth claim and
excludes soft-deleted students.
Generated with Claude, reviewed against the multi-tenant checklist and
tested against the seed data including a student with two accounts.
The "why" is unchanged. A diff cannot say why a rule exists, whoever wrote the lines.
Attribution is a team convention. Some teams require a trailer or a note; some consider it irrelevant since the committer is responsible either way. Follow whatever the team agreed, and if there is no convention, ask rather than inventing one.
Recording what you verified is worth more than recording the tool. "Tested against a student with two accounts" tells a reviewer which risk you already covered.
Never write a message describing code you have not read. A message that misdescribes the change is worse than none.
Pull requests with generated code
The description should say what you verified:
## What
Adds the fee defaulters report — students with an outstanding balance
past the due date.
## Why
The accounts team currently exports the full fee list and filters it
in Excel. #153.
## How
- FeeRepository.GetFeeDefaultersAsync — single query, joined to Student
- FeeController.GetDefaulters — Admin and Principal only
- React page reusing the existing table component
## AI assistance
The repository method and the SQL were generated with Claude.
Reviewed against the security and multi-tenant checklists.
The controller and the frontend are hand-written.
## Testing
- Unit tests: no defaulters, one, many, a student with two fee accounts
- Verified SchoolId comes from the claim: signing in as School 1 and
requesting School 2 returns 403
- Verified soft-deleted students are excluded
- Checked money types are decimal end to end
- Row count verified against a manual SSMS query: 23 defaulters
## Notes
Not paginated. 23 rows today, capped at 500. #158 covers pagination.
Saying what was generated is not an admission — it directs the reviewer's attention. A reviewer who knows the SQL was generated checks the tenant filter first, which is exactly where the risk is.
"Testing" carries the weight. It shows the silent failures were considered: tenant isolation, soft delete, money types, multiple accounts.
Review generated code with more care than hand-written code, not less, because there is no author to ask why.
Recovering from an agent
Tools that edit the repository directly (Claude Code, Cursor, Copilot Workspace) can change more than you intended.
| Situation | Recovery |
|---|---|
| Uncommitted changes are wrong | git restore . |
| One file is wrong | git restore src/Services/FeeService.cs |
| Committed but not pushed | git reset --soft HEAD~1 to keep the changes, --hard to discard |
| Pushed | git revert <hash> |
| Untracked files created | git clean -n, read the list, then git clean -fd |
| Lost work after a bad reset | git reflog |
git clean -n before git clean -fd, always. An agent's new files sit alongside your .env and local certificates, and -fdx would take all of them.
git status --short # what changed, including untracked
git diff --stat # scale
git diff # content
Those three commands, in that order, after every agent run.
Keep the working directory clean between generations so each run's changes are isolated. A generation on top of an unreviewed generation is two changes you can no longer separate.
Automated checks
Make the mechanical failures impossible to merge:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- run: dotnet restore
- run: dotnet build --no-restore -warnaserror
- run: dotnet test --no-build --verbosity normal
- run: dotnet list package --vulnerable --include-transitive
-warnaserror catches CS4014 — a task not awaited — which is a warning nobody reads and a bug that silently discards exceptions.
A pre-commit hook for the checks a build cannot do:
#!/bin/sh
# .git/hooks/pre-commit
if git diff --cached | grep -qE "^\+.*(Password\s*=\s*\"|ApiKey\s*=\s*\"|Server=.*User Id=)"; then
echo "Possible secret in staged changes. Review before committing."
exit 1
fi
if git diff --cached | grep -qE "^\+.*(<<<<<<<|>>>>>>>)"; then
echo "Conflict markers in staged changes."
exit 1
fi
if git diff --cached --name-only | grep -qE "^src/Data/.*\.cs$"; then
if ! git diff --cached -- src/Data/ | grep -q "SchoolId"; then
echo "Data-layer change with no SchoolId reference. Confirm the tenant filter."
exit 1
fi
fi
The last check is deliberately crude — it will fire on legitimate changes. That is acceptable for the multi-tenant rule, because the failure it guards against is a data breach with no error message, and confirming a false positive costs seconds.
Secret scanning is worth enabling at the server too (GitHub → Settings → Code security), since a hook only runs on machines that have it installed.
Errors you will hit
| The mistake | Consequence |
|---|---|
| Generating with uncommitted work present | Your changes and the tool's are inseparable |
| Reading the summary instead of the diff | Files you did not expect are committed |
Not checking --stat | Five files changed when you asked for one |
| Mixing generated and hand-written in one commit | Reverting one loses the other |
git clean -fd without -n | .env and local certificates gone |
| No CI | Mechanical failures reach review |
A clean working directory before generating is the single most useful habit here — git diff then shows exactly what the tool did.
Common mistakes
- Generating with uncommitted work present
- Reading the summary instead of the diff
- Not checking
--statfor unexpected files - One commit containing generated and hand-written changes
- A commit message describing code you have not read
- Not saying in the PR which parts were generated
- Reviewing generated code less carefully
git clean -fdwithout-n- No CI, so mechanical failures reach review
- Building on top of an unreviewed generation
Practice
The course exercise is use Git as the safety net.
- Generate a change with uncommitted work present, then try to separate the two diffs.
- Commit first, generate, then use
git diffto see exactly what changed. - Run
git diff --statafter a generation and check whether any unexpected file appears. - Use
git add -pto accept some hunks of a generated change and reject others. - Compare an AI's summary of its change with the actual diff.
- Generate a change, dislike it, and undo it with
git restore .. - Commit a generated change, then undo it with
reset --softand with--hard. Note the difference. - Push a generated change and undo it with
revert. - Let an agent create untracked files, then use
git clean -nbefore removing them. - Write a commit message recording both the why and what you verified.
- Write a PR description with an "AI assistance" section and a testing section listing the silent failures you checked.
- Set up the CI workflow and confirm
-warnaserrorfails on a missingawait. - Install the pre-commit hook and try to commit a fake connection string.
- Trigger the data-layer
SchoolIdcheck with a legitimate change, and decide whether the false positive is worth it. - Run two generations without reviewing the first, then try to separate them.
Exercises 1 and 15 both demonstrate the same thing: without commit boundaries, generated changes become inseparable.
You can now
- Commit before generating
- Read the diff, not the summary
- Keep generated and hand-written changes in separate commits
- Recover from an agent that changed too much
- Automate the checks that catch mechanical failures
Review questions
- Why commit before generating?
- Why read the diff rather than the AI's summary?
- Why should generated and hand-written changes be in separate commits?
- What does saying "this part was generated" give a reviewer?