Skip to content

Chapter 4: Git Workflows & PR Automation ​

Learning Objectives ​

  • Let Claude handle your entire Git workflow: branch, code, commit, PR — without typing git commands yourself
  • Build a real feature (dark mode toggle) with proper branching and atomic commits
  • Use Git Worktree for true parallel development across multiple Claude sessions
  • Break things on purpose and recover with /rewind — building confidence to try bold changes
  • Use pipe mode for instant code review before every merge

Appendix links: A05 Tool Calling Internals explains how Claude selects between Read and Bash when it needs git information. A01 Prompt Engineering covers how to write prompts that produce good commit messages.

Concepts ​

Claude's Git Integration Goes Deep ​

Claude Code doesn't just run git commands. It understands your changes, writes commit messages by analyzing diffs, selectively stages files (never git add -A), and generates structured PR descriptions with test plans.

The full cycle, all through conversation:

1. Create branch     ← Claude picks a name matching your convention
2. Write code        ← Claude's core strength
3. Stage changes     ← Claude adds specific files, skips .env and secrets
4. Commit            ← Claude reads the diff and writes the message
5. Create PR         ← Claude generates title, summary, test plan
6. Code review       ← Claude reviews other people's PRs too

Why "never git add -A"? Because git add -A stages everything — including .env files, credentials, large binaries, and temporary files. Claude always uses git add <specific-files> after checking what changed. This is a security practice, not just style.

GSD's principle applies here: one logical change per commit, atomic tasks. Claude naturally follows this when you give it focused instructions.

The Checkpoint System ​

Every time Claude edits a file, it creates a Git checkpoint. Think of it as auto-save in a video game.

Edit 1: Add dark mode CSS        → checkpoint #1
Edit 2: Add toggle component     → checkpoint #2
Edit 3: Wire up state management → checkpoint #3
Edit 4: Refactor (breaks things) → checkpoint #4

Oh no, Edit 4 was bad:
  /rewind → pick checkpoint #3
  → Everything is back to the working state after Edit 3

This is a safety net. It means you can tell Claude "try an aggressive refactor" without risk. If it doesn't work out, /rewind and you're back. The checkpoint system uses Git's staging area and stash mechanism under the hood, so your actual branch history stays clean.

Git Worktree: Actual Parallelism ​

Git worktree creates multiple working directories from the same repo, each on a different branch. Combined with Claude Code, this means you can run parallel sessions working on completely independent features.

my-app/                  ← main branch
my-app-feature-dark/     ← worktree: feature/dark-mode
my-app-feature-api/      ← worktree: feature/api-v2

Three terminals, three Claude sessions, zero interference.

This isn't branch switching. These are independent directories with independent file states. One Claude session can't accidentally overwrite another's work. Claude Code natively supports this pattern — when you launch claude in a worktree directory, it automatically detects the Git context.


Demo 8: Build Dark Mode with Proper Git Workflow ​

8
Build Dark Mode with Proper Git Workflow
Beginner~15 min

Goal ​

Build a dark mode toggle for a simple web app using proper Git practices: feature branch, atomic commits, and a clean merge back to main.

Steps ​

1. Create the Base Project ​

bash
mkdir -p ~/claude-demos/demo-08 && cd ~/claude-demos/demo-08
git init
git branch -M main

mkdir -p src

cat > src/index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Demo App</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <header>
    <h1>My App</h1>
    <nav>
      <a href="#home">Home</a>
      <a href="#about">About</a>
    </nav>
  </header>
  <main>
    <p>Welcome to the demo app.</p>
  </main>
  <script src="app.js"></script>
</body>
</html>
EOF

cat > src/styles.css << 'EOF'
body {
  font-family: system-ui, sans-serif;
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
  background: #ffffff;
  color: #1a1a1a;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid #e0e0e0;
  padding-bottom: 1rem;
  margin-bottom: 2rem;
}

nav a {
  margin-left: 1rem;
  color: #0066cc;
  text-decoration: none;
}
EOF

cat > src/app.js << 'EOF'
console.log('App initialized');
EOF

cat > CLAUDE.md << 'EOF'
# Demo App

## Key Commands
- Open in browser: open src/index.html (macOS) or start src/index.html (Windows)

## Standards
- Vanilla HTML/CSS/JS, no frameworks
- CSS custom properties (variables) for theming
- Commit messages: Conventional Commits, English
- One logical change per commit
EOF

git add -A
git commit -m "feat: initial app with basic layout and styling"

2. Let Claude Build Dark Mode ​

bash
claude
I want to add dark mode to this app. Here's the plan:

1. Create a feature/dark-mode branch
2. Add CSS custom properties for colors (light theme as default)
3. Add a dark theme using a [data-theme="dark"] attribute on <html>
4. Add a toggle button in the header
5. Write the JS to toggle themes and persist the choice in localStorage
6. Make separate commits for each logical step

Go ahead.

What you'll actually see:

terminal
> [Your prompt above]

╭─ Bash ──────────────────────────────────────────────╮
│  git checkout -b feature/dark-mode                   │
╰─────────────────────────────────────────────────────╯
> y

Switched to a new branch 'feature/dark-mode'

╭─ Read ──────────────────────────────────────────────╮
│  src/styles.css                                      │
╰─────────────────────────────────────────────────────╯
> a

[Reading src/styles.css... 20 lines]

I'll start by refactoring the hardcoded colors to CSS custom properties.

╭─ Edit ──────────────────────────────────────────────╮
│  src/styles.css                                      │
│  Replace hardcoded colors with CSS variables         │
╰─────────────────────────────────────────────────────╯
> y

[Edited src/styles.css]

╭─ Bash ──────────────────────────────────────────────╮
│  git add src/styles.css                              │
│  git commit -m "refactor: use CSS custom properties  │
│  for theming"                                        │
╰─────────────────────────────────────────────────────╯
> y

[main 2a1b3c4] refactor: use CSS custom properties for theming

Now adding the dark theme color scheme...

╭─ Edit ──────────────────────────────────────────────╮
│  src/styles.css                                      │
│  Add [data-theme="dark"] rules                       │
╰─────────────────────────────────────────────────────╯
> y

[...]

╭─ Bash ──────────────────────────────────────────────╮
│  git add src/styles.css                              │
│  git commit -m "feat: add dark theme color scheme"   │
╰─────────────────────────────────────────────────────╯
> y

[feature/dark-mode 5d6e7f8] feat: add dark theme color scheme

Now I'll add the toggle button and JavaScript...

╭─ Edit ──────────────────────────────────────────────╮
│  src/index.html                                      │
│  Add toggle button in header                         │
╰─────────────────────────────────────────────────────╯
> y

╭─ Edit ──────────────────────────────────────────────╮
│  src/app.js                                          │
│  Add theme toggle with localStorage persistence      │
╰─────────────────────────────────────────────────────╯
> y

╭─ Bash ──────────────────────────────────────────────╮
│  git add src/index.html src/app.js                   │
│  git commit -m "feat: add dark mode toggle with      │
│  localStorage persistence"                           │
╰─────────────────────────────────────────────────────╯
> y

[feature/dark-mode 9a0b1c2] feat: add dark mode toggle...

Done! Three atomic commits on the feature/dark-mode branch.

3. Verify the Work ​

bash
git log --oneline --all --graph

Expected output:

terminal
$ git log --oneline --all --graph

* 9a0b1c2 (HEAD -> feature/dark-mode) feat: add dark mode toggle with localStorage persistence
* 5d6e7f8 feat: add dark theme color scheme
* 2a1b3c4 refactor: use CSS custom properties for theming
* 1234567 (main) feat: initial app with basic layout and styling

4. Merge Back ​

Still in the Claude session:

Switch back to main and merge feature/dark-mode. Then show me the final git log.

What Just Happened? ​

1
Bash
git checkout -b feature/dark-mode
↓
2
Read
src/styles.css
↓
3
Edit
src/styles.css
↓
4
Bash
git add src/styles.css && git commit
↓
5
Edit
src/styles.css
↓
6
Bash
git add && git commit
↓
7
Edit
src/index.html, src/app.js
↓
8
Bash
git add src/index.html src/app.js && git commit

What Claude did right:

  • Created a feature branch following the convention from CLAUDE.md
  • Made 3 atomic commits — each is one logical change, independently reviewable
  • Generated commit messages by reading the actual diff
  • Selectively staged files — always git add <file>, never git add -A
  • Added the Co-Authored-By footer to indicate AI involvement

Demo 9: Parallel Development with Worktree ​

9
Parallel Development with Git Worktree
Beginner~15 min

Goal ​

Work on two features simultaneously: a frontend enhancement and a backend API, in parallel Claude sessions that don't interfere with each other.

Steps ​

1. Create Worktrees from Demo 8 ​

bash
cd ~/claude-demos/demo-08
git checkout main

# Create worktrees for two features
git worktree add ../demo-08-search feature/search
git worktree add ../demo-08-api feature/api-endpoints

What this creates:

terminal
$ git worktree list

/home/user/claude-demos/demo-08          1234567 [main]
/home/user/claude-demos/demo-08-search   1234567 [feature/search]
/home/user/claude-demos/demo-08-api      1234567 [feature/api-endpoints]

2. Terminal 1: Frontend Feature ​

Open a terminal:

bash
cd ~/claude-demos/demo-08-search
claude
Add a search feature to this app:
1. Add a search input in the header
2. Add some sample content paragraphs to the main section
3. Implement client-side search that highlights matching text and hides non-matching paragraphs
4. Commit when done

3. Terminal 2: Backend Feature ​

Open a second terminal simultaneously:

bash
cd ~/claude-demos/demo-08-api
claude
Add a simple API backend:
1. Create a server.js using Node's built-in http module (no Express needed)
2. Implement GET /api/status that returns { status: "ok", uptime: process.uptime() }
3. Implement GET /api/config that returns the app configuration
4. Add a start script to package.json (create it if it doesn't exist)
5. Commit when done

4. Both Sessions Run Simultaneously ​

While both Claude sessions are working, neither knows about the other. They're in completely separate directories with separate file states. No merge conflicts from concurrent edits.

5. Merge Both Features ​

bash
cd ~/claude-demos/demo-08
git merge feature/search
git merge feature/api-endpoints

git log --oneline --all --graph

6. Clean Up ​

bash
git worktree remove ../demo-08-search
git worktree remove ../demo-08-api

What Just Happened? ​

Why worktrees instead of branch switching?

ApproachProblem
Branch switchingMust stash or commit before switching. Only one session at a time. Risk of forgetting which branch you're on.
Git worktreeEach feature has its own directory. Multiple Claude sessions work truly in parallel. Complete isolation by design.

This pattern becomes critical in Ch9 Agent Teams, where you'll have 3+ Claude agents working on the same repo simultaneously.


Demo 10: Break Things, Then /rewind ​

10
Break Things, Then /rewind
Beginner~10 min

Goal ​

Intentionally let Claude make a bad refactor, then recover using the checkpoint system. This builds confidence to let Claude try bold changes without fear.

Steps ​

1. Set Up a Working App ​

bash
mkdir -p ~/claude-demos/demo-10 && cd ~/claude-demos/demo-10
git init

cat > calculator.py << 'EOF'
"""A simple calculator module with history tracking."""

class Calculator:
    def __init__(self):
        self.history = []

    def add(self, a, b):
        result = a + b
        self.history.append(f"{a} + {b} = {result}")
        return result

    def subtract(self, a, b):
        result = a - b
        self.history.append(f"{a} - {b} = {result}")
        return result

    def multiply(self, a, b):
        result = a * b
        self.history.append(f"{a} * {b} = {result}")
        return result

    def divide(self, a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero")
        result = a / b
        self.history.append(f"{a} / {b} = {result}")
        return result

    def get_history(self):
        return list(self.history)

    def clear_history(self):
        self.history.clear()

if __name__ == "__main__":
    calc = Calculator()
    print(calc.add(10, 5))
    print(calc.subtract(10, 3))
    print(calc.multiply(4, 7))
    print(calc.divide(15, 3))
    print("History:", calc.get_history())
EOF

cat > test_calculator.py << 'EOF'
"""Tests for the calculator module."""
import pytest
from calculator import Calculator

def test_add():
    calc = Calculator()
    assert calc.add(2, 3) == 5

def test_subtract():
    calc = Calculator()
    assert calc.subtract(10, 4) == 6

def test_multiply():
    calc = Calculator()
    assert calc.multiply(3, 7) == 21

def test_divide():
    calc = Calculator()
    assert calc.divide(10, 2) == 5.0

def test_divide_by_zero():
    calc = Calculator()
    with pytest.raises(ValueError):
        calc.divide(10, 0)

def test_history():
    calc = Calculator()
    calc.add(1, 2)
    calc.subtract(5, 3)
    history = calc.get_history()
    assert len(history) == 2
    assert "1 + 2 = 3" in history[0]
EOF

git add -A
git commit -m "feat: calculator with tests"

2. Let Claude Make Progressive Changes ​

bash
claude

First, a good change:

Add power() and sqrt() methods to the Calculator class, with history tracking and tests. Run the tests to make sure everything passes.
terminal
> [prompt above]

[Claude edits calculator.py, adds power() and sqrt()]
[Claude edits test_calculator.py, adds new tests]

╭─ Bash ──────────────────────────────────────────────╮
│  python -m pytest test_calculator.py -v              │
╰─────────────────────────────────────────────────────╯
> y

test_calculator.py::test_add PASSED
test_calculator.py::test_subtract PASSED
test_calculator.py::test_multiply PASSED
test_calculator.py::test_divide PASSED
test_calculator.py::test_divide_by_zero PASSED
test_calculator.py::test_history PASSED
test_calculator.py::test_power PASSED
test_calculator.py::test_sqrt PASSED

8 passed ✓

Good. Tests pass. Checkpoint created.

Second, another good change:

Add a memory feature: store_memory(slot_name, value) and recall_memory(slot_name). With tests.

Good. Tests still pass. Another checkpoint.

Now, the risky one:

Refactor the entire Calculator class to use a plugin architecture. Each operation (add, subtract, etc.) should be a separate plugin class that registers with the calculator. This is a major refactor -- go for it.

This is a big change. Claude will rewrite most of the file. Maybe the tests break. Maybe the refactor overcomplicates things. It doesn't matter.

3. Recover with /rewind ​

/rewind

What you'll see:

terminal
> /rewind

Available checkpoints:
  [1] feat: calculator with tests (initial)
  [2] Added power() and sqrt() methods ← 2 min ago
  [3] Added memory feature ← 1 min ago
  [4] Plugin architecture refactor ← just now

Rewind to which checkpoint? (1-4): 3

Pick checkpoint 3 — the state where power, sqrt, and memory all work.

4. Verify the Recovery ​

Run pytest to confirm everything works
terminal
╭─ Bash ──────────────────────────────────────────────╮
│  python -m pytest test_calculator.py -v              │
╰─────────────────────────────────────────────────────╯

10 passed ✓

Tests pass. The aggressive refactor is gone. Power, sqrt, and memory are intact.

What Just Happened? ​

1
Edit
calculator.py
↓
2
Bash
pytest
↓
3
Edit
calculator.py (risky refactor)
↓
4
Bash
/rewind to checkpoint #3

The lesson: /rewind means you never need to be afraid of Claude trying something ambitious. "Try a functional programming approach." "Rewrite this in a completely different pattern." "What if we used composition instead of inheritance?" Ask freely. If it doesn't work, /rewind takes you back in seconds.


When Things Go Wrong ​

Merge Conflicts ​

terminal
$ git merge feature-branch
Auto-merging src/styles.css
CONFLICT (content): Merge conflict in src/styles.css
Automatic merge failed; fix conflicts and then commit the result.

Fix: Start Claude in the conflicted directory:

bash
claude
Resolve the merge conflict in src/styles.css. Keep functionality from both sides.

Claude reads the conflict markers (<<<<<<<, =======, >>>>>>>), understands the intent of both branches, and produces a clean merge. It's good at this because it can see the full context of both changes.

/rewind Goes Too Far Back ​

If you accidentally rewind past a change you wanted to keep, check git reflog:

bash
git reflog
# Find the commit hash of the state you want
git checkout <hash> -- <file>

Worktree Branch Already Exists ​

terminal
$ git worktree add ../demo-api feature/api
fatal: 'feature/api' is already checked out at '/path/to/other'

Fix: Either remove the existing worktree first (git worktree remove <path>) or create a new branch name (feature/api-v2).

Claude Stages Unwanted Files ​

If Claude ever stages something you didn't want committed:

Unstage that file. I don't want credentials.json in the commit.

Or manually: git reset HEAD credentials.json


Going Deeper ​

Pipe Mode for Instant Code Review ​

This is one of the most underrated features:

bash
# Review uncommitted changes
git diff | claude -p "Review these changes. Flag security issues, performance concerns, and style violations."

# Review a specific branch
git diff main..feature/dark-mode | claude -p "Review this PR. Would you approve it?"

# Review the last commit
git show | claude -p "Rate this commit from 1-10 on: message quality, change cohesion, and test coverage."

# Generate a changelog from commits
git log --oneline v1.0..v2.0 | claude -p "Generate a user-facing changelog from these commits. Group by feature/fix/breaking."

Example pipe mode review output:

terminal
$ git diff | claude -p "Review these changes for security issues"

## Security Review

**1 issue found:**

- **HIGH**: `src/api.js:42` — User input is passed directly to `eval()`.
  This is a code injection vulnerability. Use `JSON.parse()` instead
  if you're parsing JSON, or a sandboxed evaluator if you genuinely
  need dynamic evaluation.

**No other security concerns.** The rest of the changes (CSS updates,
README edits) are safe.

Pipe mode is non-interactive — Claude reads stdin, answers, and exits. Perfect for integrating into scripts and CI pipelines. More on this in Ch5 Hooks.

How Claude Writes Commit Messages ​

Claude doesn't guess. It follows a process:

  1. Reads git diff --staged to see exactly what changed
  2. Checks git log to match the project's existing commit style
  3. Groups related files into single commits
  4. Skips .env, credentials, and other sensitive files
  5. Adds a Co-Authored-By: Claude ... footer

Example of a well-crafted commit message:

feat(auth): add JWT token refresh endpoint

- Add POST /auth/refresh endpoint with rate limiting
- Persist refresh tokens in separate table with encryption
- Add audit logging for token refresh events

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

PR Creation ​

If you have a GitHub remote and gh CLI installed:

Create a PR from the current branch to main. Include a descriptive title, a summary of changes with context, and a test plan.

Claude uses gh pr create and generates a structured PR body. The description references what was changed and why, not just a list of files.


Knowledge Check ​

Claude needs to commit changes. What does it use to stage files?
git add -A (stage everything)
git add . (stage everything in current directory)
git add <specific-files> (stage only changed files)
git commit -a (auto-stage and commit)
What's the difference between branch switching and Git worktree?
Worktrees are faster than branch switching
Worktrees create separate directories, allowing truly parallel work in multiple terminals
They are the same thing with different syntax
Worktrees only work with Claude Code, not regular Git

Exercise: Full Git Workflow, Conversation Only ​

Task ​

Do an entire Git workflow using only conversation with Claude. You may not type any git commands yourself.

  1. Initialize a new Git repo
  2. Create a project: a Node.js CLI tool that converts Markdown to HTML
  3. Commit the initial code
  4. Create a feature/frontmatter branch
  5. Add YAML frontmatter parsing support
  6. Commit with a good message
  7. Switch back to main
  8. Merge the feature branch
  9. Show the final git log

Success Criteria ​

  • [ ] Zero manual git commands typed
  • [ ] At least 3 commits exist with Conventional Commits format
  • [ ] Feature branch was created and merged properly
  • [ ] git log --graph shows the branch history
  • [ ] Claude used selective staging (not git add -A)
Hint
Initialize a git repo, create a Node.js CLI tool in index.js that reads
a markdown file and converts it to HTML using a simple parser (no external deps).
Add a package.json with a bin entry. Commit everything.
Create a feature/frontmatter branch. Add YAML frontmatter parsing -- when the
markdown starts with --- delimited YAML, extract it as metadata and include
it as <meta> tags in the HTML output. Commit.
Switch to main, merge feature/frontmatter, show git log --graph --oneline.

Chapter Summary ​

  • Claude handles the full Git lifecycle: branch, code, stage, commit, PR, review
  • Atomic commits are the standard. One logical change per commit, selective staging, meaningful messages.
  • Checkpoints auto-save on every edit. /rewind is your undo button — it lets you try bold changes fearlessly.
  • Git Worktree enables real parallel development. Multiple Claude sessions, multiple features, zero conflicts.
  • Pipe mode (git diff | claude -p) is instant code review. Use it before every PR.
  • Claude writes commit messages by reading diffs, not guessing. It matches your project's existing style.
  • When merge conflicts happen, Claude can resolve them by understanding the intent of both branches.

You've finished the Beginner Stage.

Everything you've learned — tool-calling, CLAUDE.md, context management, Git workflows — comes together in the Capstone Project. It's not a todo app. You're going to build something you'll actually use.

Or skip ahead to Stage 2: Intermediate — Chapter 5: Hooks System to start automating Claude Code itself.

Released under MIT License