Skip to content
Lesson 7 of 22

Git Workflows for Vibe Coders

16 min read

Why Git is Non-Negotiable

AI coding tools make big changes fast. In a single prompt, Claude Code can rewrite an entire component, restructure a directory, or modify a dozen files. That speed is incredible — until something goes wrong and you want to go back to how things were five minutes ago.

Without Git, you can't. Your previous working version is gone, overwritten by the AI's changes, and your only option is to try to manually undo everything (good luck) or start over.

With Git, you can revert to any previous state of your project in seconds. It is your undo button, your safety net, and your time machine. It lets you experiment fearlessly because you always have a working version to fall back to.

If you take only one thing from this lesson, let it be this: commit early, commit often. Every commit is a save point. The more save points you have, the less you can lose.

Git is also how professional software teams collaborate. If you're building something with others, or if you ever want to share your code on GitHub, Git is the tool that makes it possible. Learning it now pays dividends for the rest of your career as a builder.

Git Basics for Non-Developers

If you've never used Git before, here are the essential concepts and commands. Don't worry about memorizing them — you'll learn them through repetition, and Claude Code can handle most Git operations for you. But understanding what's happening under the hood makes you a better vibe coder.

git init — Start Tracking a Project

git init

This command turns a regular folder into a Git-tracked project. It creates a hidden .git directory that stores all of your version history. You only need to run this once per project. If you cloned a project from GitHub or created it with create-next-app, Git is already initialized.

git add — Stage Your Changes

git add app/page.tsx
git add .

Before you can save a snapshot (commit), you need to tell Git which changes to include. This is called "staging." Think of it as putting items on a conveyor belt before they go into storage.

git add app/page.tsx stages a specific file. git add . stages everything that changed. When you're starting out, git add . is fine. As you get more comfortable, staging specific files gives you more control over what goes into each commit.

The staging area is a holding zone between your working files and the permanent history. It exists so you can review and organize your changes before committing them.

git commit -m "message" — Save a Snapshot

git commit -m "add contact form with email validation"

A commit is a permanent snapshot of your project at a specific point in time. It records every staged change along with a message describing what changed. You can always come back to any commit, no matter how much the project changes afterward.

The -m flag lets you write the commit message inline. The message should describe what changed and optionally why. Good messages save you time when you need to find a specific version later.

git status — See What Changed

git status

This shows you what's been modified, what's staged, and what's untracked. It's your dashboard for understanding the current state of your project. Run it often — before committing, after making changes, whenever you're not sure what state things are in.

The output tells you:

  • Changes to be committed — staged and ready to commit
  • Changes not staged for commit — modified but not yet staged
  • Untracked files — new files Git hasn't seen before

git log --oneline — View Your History

git log --oneline

This shows a compact list of your commits, newest first. Each line shows a short commit hash (a unique identifier) and the commit message.

a3f2c1d add contact form with email validation
b7e4a89 set up homepage hero section
c1d9f32 initial project setup

This is your project's timeline. When you need to go back to a specific version, these messages help you find the right one.

git diff — See What Changed in Detail

git diff

This shows the exact lines that changed in your files. Added lines are shown in green with a + prefix, removed lines in red with a - prefix. It's the most detailed view of what you've been working on.

git diff --staged

This shows what's currently staged and about to be committed. Always review this before committing to make sure you're not including unintended changes.

The Checkpoint Pattern

The checkpoint pattern is the single most important workflow for vibe coding. The idea is simple: commit before every big AI change.

Think of commits as save points in a video game. Before you enter the boss fight (ask AI to make a major change), you save your progress. If the boss fight goes badly, you reload from the save point.

Here's how it works in practice:

# You just finished setting up the homepage and it looks great
git add .
git commit -m "feat: complete homepage with hero, features, and CTA sections"

# Now you want AI to add authentication — a big, risky change
# Before asking AI, you already have a save point

# AI makes the changes... but authentication broke the layout
# No problem — you can see what changed:
git diff

# If you want to undo everything AI just did:
git checkout -- .

# You're back to your working homepage, ready to try again

Naming Checkpoints Meaningfully

Good checkpoint names make it easy to find the right save point later. Here's a naming convention that works well:

# Use conventional commit prefixes
git commit -m "feat: add user registration form"
git commit -m "fix: resolve mobile nav not closing on click"
git commit -m "refactor: extract header into separate component"
git commit -m "style: update pricing cards to match new design"
git commit -m "chore: update dependencies to latest versions"

The prefix tells you what kind of change it was. The description tells you what specifically changed. Together, they create a history you can actually navigate.

Bad checkpoint names:

"update"
"fix stuff"
"changes"
"wip"
"asdf"

Good checkpoint names:

"feat: add Stripe checkout integration"
"fix: prevent duplicate form submissions"
"refactor: move auth logic to shared middleware"
"style: redesign dashboard sidebar for mobile"

When you're scrolling through git log trying to find the commit where everything still worked, you'll thank yourself for writing clear messages.

Branching for Experiments

Sometimes you want to try something risky without any chance of messing up your working code. That's what branches are for.

A branch is a parallel version of your project. You can make any changes you want on a branch without affecting the main version. If the experiment works, you merge it back. If it fails, you delete the branch. Your main code was never touched.

The Basic Branch Workflow

# You're on main, everything works
# You want to try adding a dark mode feature

# Create and switch to a new branch
git checkout -b feature/dark-mode

# Now you're on the feature/dark-mode branch
# Ask AI to implement dark mode
# Test it, iterate, make it work

# If it works — merge it back to main
git checkout main
git merge feature/dark-mode

# If it doesn't work — delete the branch
git checkout main
git branch -d feature/dark-mode
# All dark mode changes are gone, main is untouched

When to Use Branches

Use branches when:

  • You're trying something experimental. "Let's see if we can completely restructure the data model" is a branch-worthy experiment.
  • You're making changes you might want to discard. If there's a realistic chance you won't keep the changes, make them on a branch.
  • You're working on multiple things at once. You can have a feature/dark-mode branch and a fix/login-bug branch going simultaneously.
  • You want a clean review. Branches make it easy to see all the changes for a single feature grouped together, separate from other work.

For small, low-risk changes — fixing a typo, adjusting padding, changing a color — branching is overkill. Just commit to main. Reserve branches for changes that span multiple files or involve significant risk.

Recovering from AI Mistakes

AI will sometimes produce code that breaks things. That's normal, and Git gives you several ways to recover. Here are the tools, ordered from gentlest to most aggressive.

Undo All Uncommitted Changes

git checkout -- .

This discards all changes since your last commit. Every modified file goes back to how it was. This is your "nope, undo everything" button. Use it when AI made changes that are completely wrong and you want to start fresh.

Warning: This permanently deletes all uncommitted changes. Make sure you actually want to lose them.

Undo the Last Commit (Keep Changes)

git reset HEAD~1

This undoes the last commit but keeps all the file changes in your working directory. The changes are no longer committed, but they're still there for you to modify or selectively re-commit.

Use this when you committed too quickly and want to make adjustments before recommitting.

Temporarily Shelve Changes

# Save your current changes for later
git stash

# Your working directory is now clean (back to last commit)
# Do whatever you need to do...

# Bring your changes back
git stash pop

Stashing is useful when you're in the middle of something and need to quickly switch contexts. Maybe you're working on a feature when you discover a bug that needs fixing right now. Stash your feature work, fix the bug, commit the fix, then pop your stash to continue where you left off.

When to Use Each Recovery Method

Here's a quick decision guide:

"AI made changes and I haven't committed them yet. I want to undo everything." Use git checkout -- . to discard all uncommitted changes.

"I committed AI's changes but they're wrong. I want to try a different approach." Use git reset HEAD~1 to undo the commit but keep the files, then use git checkout -- . to discard the changes completely.

"I'm in the middle of working on feature A but need to quickly fix bug B." Use git stash to shelve feature A, fix and commit bug B, then git stash pop to resume feature A.

"I want to see an older version of a specific file." Use git show HEAD~3:app/page.tsx to see what that file looked like 3 commits ago, without changing anything in your current directory.

"Everything is broken and I just want to go back to a known good state." Use git log --oneline to find the good commit hash, then git reset --hard <commit-hash>. Warning: this permanently discards all changes after that commit.

The PR Workflow with AI

A Pull Request (PR) is a formal way to propose changes to a project. Even if you're working solo, PRs are valuable because they create a review checkpoint before changes are merged into your main branch.

Here's the workflow:

# 1. Create a branch for your feature
git checkout -b feature/user-profile

# 2. Make changes with AI assistance
# ... prompt, iterate, test ...

# 3. Commit your changes
git add .
git commit -m "feat: add user profile page with avatar upload"

# 4. Push the branch to GitHub
git push -u origin feature/user-profile

# 5. Create a Pull Request
gh pr create --title "Add user profile page" --body "Adds a profile page where users can view and edit their information, including avatar upload."

# 6. Review the PR (or have someone else review it)
# 7. Merge when ready
gh pr merge

Why PRs Matter

For solo projects: PRs create a natural review point. Instead of committing directly to main, you can review all the changes together, run tests, and make sure everything looks right before merging. It adds a checkpoint between "AI generated this" and "this is in production."

For team projects: PRs are how teams collaborate. Each person works on their own branch, creates a PR, and the team reviews the changes before they're merged. This prevents one person's changes from breaking another person's work.

For your portfolio: A GitHub profile with well-organized PRs shows that you follow professional development practices. It demonstrates that you don't just write code — you review it, organize it, and manage it.

Commit Message Conventions

Conventional Commits is a standard format for commit messages that makes your history easy to read and navigate. The format is:

type: description

Here are the types and when to use each:

feat: — A New Feature

git commit -m "feat: add email notification system"
git commit -m "feat: implement dark mode toggle"
git commit -m "feat: add CSV export to dashboard"

Use feat: when you're adding something that didn't exist before. A new page, a new component, a new API endpoint, a new capability.

fix: — A Bug Fix

git commit -m "fix: resolve login form not submitting on mobile Safari"
git commit -m "fix: correct pricing calculation for annual plans"
git commit -m "fix: prevent duplicate webhook processing"

Use fix: when you're correcting something that was broken. The thing existed before but wasn't working correctly.

refactor: — Code Restructuring

git commit -m "refactor: extract auth logic into shared middleware"
git commit -m "refactor: replace class components with functional components"
git commit -m "refactor: reorganize utility functions by domain"

Use refactor: when you're changing how code is organized or structured without changing what it does. The app behaves exactly the same, but the code is cleaner.

style: — Visual Changes

git commit -m "style: update button colors to match new brand guidelines"
git commit -m "style: improve mobile spacing on pricing page"
git commit -m "style: add hover animations to feature cards"

Use style: for purely visual changes — CSS updates, layout adjustments, design tweaks. No behavior changes.

docs: — Documentation

git commit -m "docs: add API endpoint documentation"
git commit -m "docs: update README with setup instructions"
git commit -m "docs: add inline comments to auth flow"

test: — Tests

git commit -m "test: add unit tests for pricing calculation"
git commit -m "test: add integration tests for checkout flow"

chore: — Maintenance

git commit -m "chore: update dependencies to latest versions"
git commit -m "chore: configure CI/CD pipeline"
git commit -m "chore: add pre-commit hooks for linting"

Use chore: for tasks that don't change the application code — updating configs, managing dependencies, setting up tooling.

Why Consistency Matters

When your commit history follows a consistent format, you can:

  • Quickly scan for all bug fixes: look for fix: commits
  • Find when a feature was added: look for feat: commits
  • Generate changelogs automatically from commit messages
  • Understand the project's evolution at a glance

Inconsistent messages like "stuff," "fixed it," and "more changes" tell you nothing. Consistent messages create a readable narrative of your project's development.

.gitignore Essentials

Your .gitignore file tells Git which files and directories to ignore — they won't be tracked, committed, or pushed to GitHub. This is critical for keeping your repository clean and secure.

Here's a solid .gitignore for most JavaScript/TypeScript projects:

# Dependencies
node_modules/

# Environment variables (contains secrets!)
.env
.env.local
.env.production

# Build output
.next/
dist/
build/
out/

# OS files
.DS_Store
Thumbs.db

# IDE files
.vscode/
.idea/

# Debug logs
npm-debug.log*
yarn-debug.log*

# TypeScript cache
*.tsbuildinfo

# Test coverage
coverage/

Why These Files Should Not Be Tracked

node_modules/ — This directory can contain tens of thousands of files and hundreds of megabytes. It's fully reproducible from package.json by running npm install. Tracking it would bloat your repository and slow down every Git operation.

.env and .env.local — These files contain API keys, database passwords, and other secrets. If you commit them, anyone who can see your repository can see your secrets. This is one of the most common security mistakes developers make. Never commit environment files.

.next/ and dist/ — These are build artifacts generated from your source code. They're reproducible and often large. Track the source, not the output.

.DS_Store and Thumbs.db — These are operating system metadata files that have no relevance to your project. They just add noise.

If you accidentally committed a file that should be ignored, adding it to .gitignore won't remove it from history. You need to explicitly remove it:

git rm --cached .env
git commit -m "chore: remove accidentally committed .env file"

Git + Claude Code Integration

One of the great things about Claude Code is that it understands Git natively. You can ask it to perform Git operations using natural language, and it will run the appropriate commands.

Creating commits:

Commit the current changes with a descriptive message following
conventional commits format

Claude Code will look at what changed, craft an appropriate commit message, and create the commit.

Creating branches:

Create a new branch called feature/user-settings and switch to it

Reviewing changes:

Show me what changed since the last commit and explain each change

This is incredibly powerful — Claude Code can not only show you the diff but explain what each change does in plain language.

Creating Pull Requests:

Push this branch and create a PR with a summary of all the changes
we made in this session

Claude Code will push the branch, then use the GitHub CLI to create a PR with a well-written description.

Recovering from mistakes:

The last set of changes broke the login page. Revert everything
back to the previous commit.

Checking history:

Show me the last 10 commits and tell me which one added the payment feature

You don't need to memorize Git commands. Understanding the concepts is enough — Claude Code can translate your intentions into the right commands. But knowing what's possible helps you ask for the right things.

What's Next

You now have a safety net. With Git in your workflow, you can ask AI to make bold changes knowing that you can always undo them. You can experiment on branches, recover from mistakes, and maintain a clean history of your project's evolution.

But what happens when AI's changes don't quite work? When the build fails, the styling looks wrong, or a feature behaves unexpectedly? In the next lesson, we'll master iterating and debugging with AI — the feedback loop that turns "almost right" into "exactly right." You'll learn how to give effective feedback, read error messages, and know when to iterate versus when to start fresh.