Thuta Learning
ExercisesDevOps & Toolsintermediate

Exercise: Using Git Safely with AI Coding Agents

What you'll walk away with

  • Explain the core ideas behind Exercise: Using Git Safely with AI Coding Agents
  • Read the diagram and trace how state or data flows through the Git/GitHub workflow
  • Decide how this applies to your own project or team

Build the mental model

An AI coding agent can produce in thirty seconds what would take a careful developer an hour: a new feature across five files, a refactor touching every controller, a dependency bump plus the code changes it requires. That speed is genuinely useful, and it is exactly why Git matters more here, not less.

A human writing code slowly tends to self-review as they go, sentence by sentence. An agent does not pause between files, and it has no stake in whether the change is actually correct -- it optimizes for producing a plausible diff, not for the judgment of whether that diff should exist.

  • A known-good checkpoint commit before the agent starts.
  • A full `git diff` review before anything gets trusted.
  • A clean way back (restore, reset, or revert) if the result is wrong.

Git is what turns "the agent changed a lot of files" from a leap of faith into something you can actually inspect and, if needed, undo. None of this is exotic -- it is the same commands you already use, applied with one extra rule: never skip the review step just because the agent wrote the code quickly.

Never let an agent run destructive Git commands unsupervised

An AI agent should not be given unrestricted, unreviewed access to destructive Git operations -- force-push, hard reset, history rewriting -- by default. Treat every agent-generated diff the way you'd treat a stranger's pull request: read it before you trust it, and keep the checkpoint commit as your unconditional way back.

text
THE AGENT-EDIT REVIEW LOOP
--------------------------
STEP 1  commit a clean checkpoint (safe point to return to)
   |
   v
STEP 2  AI agent edits code (often touches several files fast)
   |
   v
STEP 3  git diff -- read every changed line before anything else
   |
   v
STEP 4  run lint + tests
   |
   v
STEP 5  extra scrutiny: auth, secrets, permissions, dependencies
   |
   +-------------------------------+
   |                                |
 change is GOOD                 change is BAD
   |                                |
   v                                v
 git add + commit             git restore / reset / revert
 (describe what changed)      (back to the checkpoint)
   |                                |
   +---------------+----------------+
                   |
                   v
         repeat for the next agent task

Connect it to a real scenario

You will work through two rounds of a simulated agent task inside a real Git repository. A checkpoint commit locks in a known-good state -- a small `calc.py` module with a passing test -- before any agent touches it. The recommended workflow, in order:

Commit a checkpoint

Before giving the agent a task, commit a clean, known-good state. This is the point you can always return to.

Let the agent edit

Hand off the task. The agent may touch several files in a single pass.

Review the diff

Run `git diff` and read every changed line before doing anything else. Never assume agent-written code is correct just because it looks plausible.

Run lint and tests

Execute the project's lint and test suite against the change, exactly as you would for a human-written pull request.

Scrutinize security-sensitive files

Give extra attention to any file touching auth, secrets, permissions, or dependencies -- exactly where a subtle, dangerous change can hide.

Commit or roll back

If the change is good, `git add` and `git commit` it with a real description. If it's not, `git restore`, `git reset`, or `git revert` back to the checkpoint rather than hand-editing around the problem.

In round one, the simulated agent edit adds a real helper function and also hardcodes an API key into a config file -- the diff review catches it, and the whole change is rejected with `git restore`. In round two, a well-scoped function-and-test change is reviewed, tested, and committed for real. Same commands both times; what changes is whether you looked before deciding.

Never let an agent run destructive Git commands unsupervised

Agent-generated code should never be blindly committed, and an AI agent should never be given unrestricted, unreviewed access to destructive Git operations -- force-push, hard reset, history rewriting -- by default. The checkpoint commit and the diff review are not optional extras; they are what makes using an agent safe to do quickly.

Try the working example

bash
# One-time setup (once per machine)
git config --global init.defaultBranch main

mkdir project && cd project
git init

# calc.py -- the module the agent will be asked to extend
cat > calc.py <<'EOF'
def add(a, b):
    return a + b
EOF

# test_calc.py -- a passing test for the current code
cat > test_calc.py <<'EOF'
from calc import add


def test_add():
    assert add(2, 3) == 5
EOF

# config.py -- a normal, non-secret config module
cat > config.py <<'EOF'
import os

DATABASE_URL = os.environ.get("DATABASE_URL", "postgres://localhost/myapp_dev")
DEBUG = True
EOF

export GIT_AUTHOR_NAME="Thuta Learner"
export GIT_AUTHOR_EMAIL="learner@example.com"
export GIT_COMMITTER_NAME="Thuta Learner"
export GIT_COMMITTER_EMAIL="learner@example.com"
export GIT_AUTHOR_DATE="2026-02-01T09:00:00"
export GIT_COMMITTER_DATE="2026-02-01T09:00:00"

git add calc.py test_calc.py config.py
git commit -m "Checkpoint: add() function, tests, and config pass before starting agent task"

# ======================================================================
# ROUND 1 -- a BAD agent edit: a real helper PLUS a hardcoded secret
# ======================================================================
# What the agent's round-1 edit actually did to the two files:
printf '\ndef multiply(a, b):\n    return a * b\n' >> calc.py
printf '\nSTRIPE_API_KEY = "sk_live_51H8x2example_do_not_commit"\n' >> config.py

git status
git diff

# Review catches the hardcoded key in config.py -- reject the whole change
git restore calc.py config.py
git status   # back to a clean checkpoint, nothing lost

# ======================================================================
# ROUND 2 -- a GOOD agent edit: a well-scoped function + test
# ======================================================================
# What the agent's round-2 edit actually did to the two files:
printf '\ndef subtract(a, b):\n    return a - b\n' >> calc.py
printf '\ndef test_subtract():\n    assert subtract(5, 3) == 2\n' >> test_calc.py
sed -i 's/from calc import add$/from calc import add, subtract/' test_calc.py

git diff

# Run the test suite before trusting the change
python -c "import test_calc; test_calc.test_add(); test_calc.test_subtract(); print('2 passed')"

export GIT_AUTHOR_DATE="2026-02-01T09:20:00"
export GIT_COMMITTER_DATE="2026-02-01T09:20:00"
git add calc.py test_calc.py
git commit -m "Add subtract() function with test (reviewed AI agent edit)"
git log --oneline
You should see
Round 1's `git diff` shows exactly what the simulated agent touched:

diff --git a/calc.py b/calc.py
index 4693ad3..3581473 100644
--- a/calc.py
+++ b/calc.py
@@ -1,2 +1,6 @@
 def add(a, b):
     return a + b
+
+
+def multiply(a, b):
+    return a * b
diff --git a/config.py b/config.py
index 9b58e1e..5452405 100644
--- a/config.py
+++ b/config.py
@@ -2,3 +2,4 @@ import os
 
 DATABASE_URL = os.environ.get("DATABASE_URL", "postgres://localhost/myapp_dev")
 DEBUG = True
+STRIPE_API_KEY = "sk_live_51Hc9example1234567890abcdef"

The `multiply()` addition is harmless, but a hardcoded key in `config.py` is a real problem -- so the whole change is rejected. `git restore calc.py config.py` returns `git status` to `nothing to commit, working tree clean`, and the checkpoint commit is untouched.

Round 2's `git diff` only touches calc.py and test_calc.py -- no config or dependency file in sight:

diff --git a/calc.py b/calc.py
index 4693ad3..3b474e9 100644
--- a/calc.py
+++ b/calc.py
@@ -1,2 +1,6 @@
 def add(a, b):
     return a + b
+
+
+def subtract(a, b):
+    return a - b
diff --git a/test_calc.py b/test_calc.py
index d509f32..38c0994 100644
--- a/test_calc.py
+++ b/test_calc.py
@@ -1,5 +1,9 @@
-from calc import add
+from calc import add, subtract
 
 
 def test_add():
     assert add(2, 3) == 5
+
+
+def test_subtract():
+    assert subtract(5, 3) == 2

Running the tests prints `2 passed`. This time the change is accepted:

[main de6c5a8] Add subtract() function with test (reviewed AI agent edit)
 2 files changed, 9 insertions(+), 1 deletion(-)

5-minute try-it

You give an AI coding agent a small task and it comes back having touched two files at once. `git diff` shows a new `multiply()` function in `calc.py` -- clean, tested-looking, unremarkable -- and, in the same pass, a new line in `config.py`: `STRIPE_API_KEY = "sk_live_51Hc9example1234567890abcdef"`. Nothing crashed. Nothing looks obviously broken. The function alone would be a fine change to accept. Before you run `git add` on everything the agent touched, what should stop you, and which specific file in this diff deserves closer attention than the rest?

One important caution

Committing an agent's change immediately without reading the diff defeats the entire safety net -- reviewing before trusting is the whole point.

Letting an agent run `git push --force`, `git reset --hard`, or rewrite history on its own removes your ability to recover if something goes wrong.

Pro Git -- Git Basics: Undoing ThingsGit & GitHub

Git & GitHub Glossary — Common Terms

TermMeaning
Version ControlA system that records changes to files over time so you can review or restore any earlier version.
GitThe distributed version-control tool that tracks a project's history locally on your machine.
GitHubA web platform built on top of Git that hosts repositories and adds collaboration features like Pull Requests, Issues, and Actions.
RepositoryThe folder Git tracks, containing your files plus the full history of changes to them.
Local RepositoryThe copy of a repository that lives on your own machine, where you actually commit changes.
Remote RepositoryA copy of a repository hosted elsewhere, such as on GitHub, that your local repository pushes to and pulls from.
Working DirectoryThe actual files on disk that you're currently editing, before anything is staged or committed.
Staging AreaThe holding area (also called the index) where changes wait after `git add` and before `git commit`.
CommitA saved snapshot of the staged changes, permanently recorded in the repository's history.
Commit HashThe unique SHA identifier Git assigns to every commit, used to reference it exactly.
BranchA movable pointer to a line of commits, letting you develop separate lines of work in parallel.
mainThe conventional default branch name for a repository's primary line of history.
HEADA pointer to the commit your working directory currently reflects -- usually the tip of your current branch.
Detached HEADA state where HEAD points directly at a commit instead of a branch, so new commits won't belong to any branch unless you create one.
MergeCombining the changes from one branch into another, creating a new commit that joins both histories.
Merge ConflictA situation where Git can't automatically combine two changes to the same lines and asks you to resolve it by hand.
CloneDownloading a full copy of a remote repository, including its entire history, to your own machine.
ForkA personal, independent copy of someone else's GitHub repository that you can freely change without affecting the original.
RemoteA named reference to another copy of a repository, most commonly the one hosted on GitHub.
originThe conventional default name Git gives the remote a repository was cloned from.
PushSending your local commits to a remote repository so others can see them.
PullFetching commits from a remote and merging them into your current local branch in one step.
FetchDownloading a remote's latest commits and branches without merging them into your working branch.
Pull RequestA GitHub request to merge one branch's changes into another, opening a space for discussion and review first.
Code ReviewThe practice of having another person examine a proposed change before it's merged, to catch bugs and share knowledge.
IssueA GitHub tracker entry used to report a bug, request a feature, or discuss project work.
TagA fixed, named pointer to a specific commit, typically used to mark a release point in history.
Semantic VersioningA MAJOR.MINOR.PATCH version-numbering convention that signals whether a release is breaking, additive, or a fix.
ReleaseA packaged, named snapshot of a repository at a tag, often bundled with build artifacts and release notes.
RebaseReplaying a branch's commits onto a new base commit, producing a straighter, linear history.
ResetMoving the current branch pointer to a different commit, optionally changing the staging area and working directory too.
RevertCreating a new commit that undoes the changes from an earlier commit, without rewriting history.
StashTemporarily shelving uncommitted changes so you can switch context and reapply them later.
Cherry-pickApplying one specific commit from another branch onto your current branch.
BisectA binary-search tool that finds the exact commit that introduced a bug by testing commits between a known-good and known-bad point.
ReflogGit's local log of every place HEAD and branches have pointed to, useful for recovering commits that seem lost.
Git HookA script Git runs automatically at a specific point in the workflow, such as before a commit or before a push.
CIContinuous Integration: automatically building and testing every change as soon as it's pushed.
CDContinuous Delivery/Deployment: automatically preparing or shipping a passing build to users after CI succeeds.
GitHub ActionsGitHub's built-in automation platform for running CI/CD workflows directly from a repository.
SecretA sensitive value, like an API key or password, stored securely rather than committed into source code.
SSHA secure protocol commonly used to authenticate with GitHub over the network without typing a password each time.
Personal Access TokenA password-like credential you generate on GitHub to authenticate scripts, tools, or HTTPS Git operations.
OrganizationA shared GitHub account that holds multiple repositories and manages team member permissions collectively.
Branch ProtectionGitHub repository settings that require checks, like reviews or passing CI, before a branch such as `main` can be updated.
Open SourceSoftware whose source code is publicly available for anyone to view, use, modify, and contribute to.

Git Command Cheat Sheet

CommandWhat it does
git initCreate a brand-new Git repository in the current folder.
git clone <url>Download a full copy of a remote repository, including its history.
git statusShow the working directory and staging area's current state.
git logShow commit history; add --oneline --graph --all for a compact branch map.
git diffShow exactly what changed, line by line, before you stage or commit it.
git add <file>Move a change from the working directory into the staging area.
git commit -m "..."Save the staged changes as a new, permanent snapshot in history.
git branchList, create, or delete branches.
git switch <branch>Move your working directory to point at a different branch.
git merge <branch>Combine another branch's changes into your current branch.
git remote -vList the remotes this repository knows about and their URLs.
git fetchDownload a remote's latest commits and branches without merging them.
git pullFetch and merge a remote's changes into your current branch in one step.
git pushSend your local commits to a remote repository.
git tag <name>Mark the current commit with a fixed, named pointer, typically for a release.
git restore <file>Discard uncommitted changes to a file, or unstage it, without touching history.
git revert <commit>Undo a commit's changes safely by creating a new commit, keeping history intact.
git reset <commit>Move the current branch pointer to a different commit, rewriting local history.
git rebase <branch>Replay your branch's commits onto a new base for a straighter history.
git cherry-pick <commit>Apply one specific commit from another branch onto your current branch.
git stashTemporarily shelve uncommitted changes so you can switch context cleanly.
git reflogShow every place HEAD has pointed to recently, useful for recovering 'lost' commits.
git bisectBinary-search through commit history to find exactly which commit introduced a bug.

Easy traps

  • Committing an agent's change immediately without reading the diff defeats the entire safety net -- reviewing before trusting is the whole point.
  • Letting an agent run `git push --force`, `git reset --hard`, or rewrite history on its own removes your ability to recover if something goes wrong.
  • Always run `git status` before any destructive or history-rewriting command, to know exactly what state you're in.

Exercise

You give an AI coding agent a small task and it comes back having touched two files at once. `git diff` shows a new `multiply()` function in `calc.py` -- clean, tested-looking, unremarkable -- and, in the same pass, a new line in `config.py`: `STRIPE_API_KEY = "sk_live_51Hc9example1234567890abcdef"`. Nothing crashed. Nothing looks obviously broken. The function alone would be a fine change to accept. Before you run `git add` on everything the agent touched, what should stop you, and which specific file in this diff deserves closer attention than the rest?

You'll know it worked when: Round 1's `git diff` shows exactly what the simulated agent touched: diff --git a/calc.py b/calc.py index 4693ad3..3581473 100644 --- a/calc.py +++ b/calc.py @@ -1,2 +1,6 @@ def add(a, b): return a + b + + +def multiply(a, b): + return a * b diff --git a/config.py b/config.py index 9b58e1e..5452405 100644 --- a/config.py +++ b/config.py @@ -2,3 +2,4 @@ import os DATABASE_URL = os.environ.get("DATABASE_URL", "postgres://localhost/myapp_dev") DEBUG = True +STRIPE_API_KEY = "sk_live_51Hc9example1234567890abcdef" The `multiply()` addition is harmless, but a hardcoded key in `config.py` is a real problem -- so the whole change is rejected. `git restore calc.py config.py` returns `git status` to `nothing to commit, working tree clean`, and the checkpoint commit is untouched. Round 2's `git diff` only touches calc.py and test_calc.py -- no config or dependency file in sight: diff --git a/calc.py b/calc.py index 4693ad3..3b474e9 100644 --- a/calc.py +++ b/calc.py @@ -1,2 +1,6 @@ def add(a, b): return a + b + + +def subtract(a, b): + return a - b diff --git a/test_calc.py b/test_calc.py index d509f32..38c0994 100644 --- a/test_calc.py +++ b/test_calc.py @@ -1,5 +1,9 @@ -from calc import add +from calc import add, subtract def test_add(): assert add(2, 3) == 5 + + +def test_subtract(): + assert subtract(5, 3) == 2 Running the tests prints `2 passed`. This time the change is accepted: [main de6c5a8] Add subtract() function with test (reviewed AI agent edit) 2 files changed, 9 insertions(+), 1 deletion(-)

Exercise: Using Git Safely with AI Coding Agents | Thuta Learning