Lesson 1.3: Make Your First AI-Assisted Code Edit Using the Diff Loop

Lesson 3 60 min

Lesson 1.3: Make Your First AI-Assisted Code Edit Using the Diff Loop

THE NAIVE APPROACH TRAP

A developer comfortable with Claude.ai chat might attempt this lesson by:

  1. Opening Claude in a browser, pasting a code snippet

  2. Asking Claude to fix it, getting back a rewritten version

  3. Manually copying the diff, applying it to their editor

  4. Repeating for the next file

The failure: This workflow loses Claude Code's core valueβ€”the diff validation loop. Without it:

  • Silent wrong-outputs: You accept changes without reviewing intent-to-code alignment. A variable rename might ripple through 5 files unnoticed.

  • No rollback: Once you copy code, you've committed mentally. The diff loop lets you say "no" and iterate. Chat forces a full redo.

  • Token burn: Each manual handoff resets context. "Remember when I said the config format was TOML?" is now lost; Claude re-explains it. The diff loop keeps session state.

  • No atomic verification: Chat gives you code; you run tests separately. The diff loop integrates test feedback into the next suggestion.


THE FAILURE MODE

Claude Code's diff loop fundamentally changes what "accepting changes" means:

When you run claude --chat and ask Claude to rewrite a function, Claude outputs a full response blob. You read it, copy it, paste it. Your brain is the merge tool. Mistakes in the transition are invisible until tests fail.

When you run claude (no flags) in interactive mode on a file:

  1. Claude reads the file's full content via /add

  2. Claude examines your intent from your prompt

  3. Claude generates a unified diff, not raw code

  4. The CLI prints this diff to stdout with context lines

  5. You accept (y), reject (n), or edit (e) before any write happens

  6. If you accept, the CLI applies the diff and loops for the next edit

The non-obvious mechanics:

  • The diff is idempotentβ€”applying it twice fails safely, not silently

  • Rejection is stateless: Claude stays in the conversation; you're iterating on the same file, not starting over

  • The context window includes full file content plus your entire edit historyβ€”Claude learns from what you rejected last time

  • Exit code is non-zero if the diff fails to apply, catching merge conflicts in scripts

Failure points in naive implementations:

  • Running claude --print without capturing the exit code, so your CI doesn't know a diff failed

  • Accepting diffs in a loop without reading them ("just let Claude do it"), then discovering mid-sprint that the code no longer matches your architecture

  • Not using CLAUDE.md to anchor context, so each session Claude forgets your codebase's conventions

  • Assuming --allowedTools defaults allow all operationsβ€”it doesn't; you need to explicitly permit writes


THE CLAUDEFORGE ARCHITECTURE

Component Architecture

THE NAIVE TRAP (Manual) 1. Copy/Paste to Browser 2. Get Full Code Response 3. Manual Paste to Editor Risk: Silent Overwrites & Token Burn THE DIFF LOOP (Claude Code) 1. Local CLI Request 2. Claude Generates Diff 3. Y / N / E Review 4. Atomic Write to Disk

This lesson is the first hands-on checkpoint in Module 1. It connects:

Backward to Lesson 1.1–1.2: You've installed Claude Code and authenticated with claude login. Your .claude/auth.json holds your session token (Pro subscription, no API key).

Forward to Lesson 1.4: You'll write a production CLAUDE.md that anchors Claude's understanding across future edit sessions. This lesson uses a minimal one.

Cross-reference to Module 2: Lessons 2.1–2.2 will teach /add, /remove, and @-file as deliberate context scoping. This lesson shows why that matters: the diff loop only works when Claude has tight, explicit context.

Critical files in this lesson:

Code
lesson-1-3-diff-loop/
β”œβ”€β”€ sample-api/
β”‚   β”œβ”€β”€ server.py        # Intentionally mediocre code for Claude to fix
β”‚   β”œβ”€β”€ CLAUDE.md        # Minimal project memory
β”‚   └── tests/
β”‚       └── test_server.py
└── demo.sh              # Guided walkthrough script

CLI flags introduced:

  • claude --chat: Interactive prompt loop (used in lessons 1.1–1.2)

  • claude (no flags): File-focused interactive mode with diff loop (this lesson)

  • --allowedTools: Restricts which operations Claude can perform (default: read-only)

  • --print: Outputs diffs to stdout instead of asking for acceptance (used in Module 3)

CLAUDE.md sections Claude weights at session start:

  • ## Context: Your codebase's language, framework, and conventions

  • ## Recent Changes: What you've been working on (Claude uses this to avoid suggesting redundant edits)

  • ## Known Issues: Constraints Claude should respect


IMPLEMENTATION DEEP DIVE

How the Diff Loop Preserves Context Across Edits

Flowchart

Claude API Claude CLI (Merge Tool) Local Filesystem 1. Suggests Unified Diff USER REVIEW: Accept (y)? 2. Safe Atomic Write

When you start an interactive session with claude src/server.py, Claude's session state includes:

  1. Full file content (from /add implicit at start)

  2. System context (from CLAUDE.md)

  3. Edit history β€” a record of every diff you accepted, rejected, or edited

This history is ephemeral per session, not saved to disk. If you close the session, it's gone. This is intentionalβ€”it forces you to write important context to CLAUDE.md for cross-session continuity.

Why Diff Presentation Matters

A unified diff shows:

Code
@@ -12,3 +12,5 @@
 def health_check():
-    return {"status": "ok"}
+    return {
+        "status": "ok",
+        "timestamp": int(time.time())
+    }

Three critical properties:

  1. Reviewable: You see the exact change before it lands.

  2. Safe to reject: Saying n doesn't delete the file; Claude's context stays intact for the next attempt.

  3. Applicability: The CLI verifies the patch applies cleanly. If your file has changed since the edit started, the diff fails with exit code 1, not a silent merge conflict.

The --allowedTools Permission Model

By default, claude starts with --allowedTools=read-only (even though this is not in the help text). This means:

  • Claude can read files via /add

  • Claude cannot execute touch, rm, or shell commands

  • Claude can suggest diffs; you apply them manually

To permit Claude to write directly (not recommended for beginners; used in Module 3 pipelines):

bash
claude --allowedTools=bash,filesystem src/

Why this matters for lesson 1.3: You're explicitly reviewing and accepting each diff. This is the safe, human-centered default. Don't change it yet.

CLAUDE.md Parsing at Session Start

When you run claude src/server.py, the CLI:

  1. Looks for ./CLAUDE.md in the current or ancestor directories

  2. Parses it as Markdown, extracting key sections by heading

  3. Passes the full content to Claude's system prompt

Example minimal CLAUDE.md for this lesson:

markdown
## Project: Sample API

## Context
Python 3.11+, Flask framework. Health check endpoint must return 200 with JSON.

## Recent Changes
- Converted to async where applicable
- Added structured logging

## Known Issues
- Config is environment-variable-driven; don't hardcode secrets

Claude sees this at the start of your session and weighs it heavily in every response. A vague CLAUDE.md ("Fix bugs") produces vague diffs. A specific one ("Return ISO 8601 timestamps in UTC") produces precise, reviewable changes.


PRODUCTION READINESS

Metrics for Real Deployments

State Machine

CLAUDE.md ## Context: Python 3.11 ## Recent: Async refactor ## Issues: No hardcoding Informs System Prompt Prevents Regressions FOUNDATION FOR EVERY DIFF LOOP SESSION

Per-session observability:

  1. Diff acceptance rate: How many diffs did you accept vs. reject? >80% suggests Claude is well-calibrated to your codebase. <60% suggests CLAUDE.md is too vague.

  2. Edits per fix: How many "uh, actually can you change it to..." iterations happen per feature? Ideal: 1–2. >5 suggests you're not being specific enough in your initial prompt.

  3. Token usage from /status: A 10-file refactor should not burn >2.5M tokens. If it does, your context is bloated (use @-file more precisely).

Post-merge signal:

  1. Test pass rate: If you're accepting diffs without running tests, you'll merge broken code. Always run pytest before accepting.

  2. Revert rate: If PRs using Claude Code diffs get reverted >5% of the time, you're not reviewing thoroughly.

Failure Signals to Monitor

  1. claude command hangs on diff approval prompt: Your /add context is over 50KB; Claude's response is slow. Reduce context scope.

  2. Exit code 1 on --print in CI: The diff failed to apply. Someone modified the file between Claude's generation and the CLI's application. Use a pre-check step.

  3. CLAUDE.md ignored: You wrote a CLAUDE.md, but Claude keeps suggesting Python 2 patterns when you said "Python 3.11+". Restart your session; the CLAUDE.md is only read at session start.


STEP-BY-STEP GUIDE

Prerequisites

  • Node.js 20 LTS (verified: node --version β‰₯ 20.0)

  • Python 3.11+ (verified: python3 --version β‰₯ 3.11)

  • Claude Code CLI latest stable:

bash
npm install -g @anthropic-ai/claude-code
claude --version  # Should show v0.X.X (no "dev" suffix)
  • Claude.ai Pro subscription with claude login completed in Lesson 1.1

    • Verify: cat ~/.claude/auth.json should exist and be valid JSON

      • DO NOT set ANTHROPIC_API_KEY for this module; it overrides your Pro billing

  • macOS 14+, Ubuntu 22.04, or Windows 11 WSL2

Execution

Step 1: Clone and navigate to the lesson workspace

bash
# macOS / Linux / WSL2
git clone https://github.com/anthropics/claudeforge.git
cd claudeforge/lesson-1-3-diff-loop

# Verify structure
ls -la sample-api/
# Expected output:
# -rw-r--r-- server.py
# -rw-r--r-- CLAUDE.md
# drwxr-xr-x tests/

Step 2: Examine the intentionally imperfect code

bash
cat sample-api/server.py

Expected content:

python
from flask import Flask
app = Flask(__name__)

def health_check():
    return {"status": "ok"}

@app.route("/health", methods=["GET"])
def health():
    result = health_check()
    return result, 200

This code is correct but inelegant: the health_check() function is separate from the endpoint, and we're missing error handling and structured logging (per CLAUDE.md's "recent changes" section).

Step 3: Read the minimal CLAUDE.md

bash
cat sample-api/CLAUDE.md

Expected content anchors Claude's suggestions:

markdown
## Sample API

## Context
Python 3.11, Flask. We're converting to async and adding structured logging.

## Known Issues
- No error handling for edge cases
- Logging is not structured (using print)

Step 4: Start an interactive session

bash
cd sample-api
claude

Expected prompt:

Code
Files in context: 0
Commands: /add, /remove, /status, /done
Type your instruction or /help

Step 5: Add the server file to context

Code
/add server.py

Expected output:

Code
Added server.py
Files in context: 1
Code-lens features enabled

Verify with /status:

Code
/status

Output shows token estimate:

Code
Tokens used so far: ~850
Estimated tokens for full session: ~2100

Step 6: Make a specific edit request

Code
Turn health_check into an async function and add structured logging using the logging module. Keep the existing route intact.

Claude responds with a diff:

diff
--- a/server.py
+++ b/server.py
@@ -1,9 +1,16 @@
 from flask import Flask
+import logging
+
 app = Flask(__name__)
+logger = logging.getLogger(__name__)
 
-def health_check():
+async def health_check():
     return {"status": "ok"}
 
 @app.route("/health", methods=["GET"])
 def health():
     result = health_check()
+    logger.info("health check called")
     return result, 200

Review the diff carefully. Does it match your intent? Does it follow the "async everywhere" pattern from CLAUDE.md?

Step 7: Accept or reject the diff

Code
y

Expected output:

Code
βœ“ Diff applied to server.py
Files modified: 1

Your server.py is now updated on disk.

Step 8: Verify the change locally

bash
/done

Exit the session. Run tests to confirm the change works:

bash
python3 -m pytest tests/test_server.py -v

Expected test output (assuming the test file exists):

Code
test_health_returns_200 PASSED
test_health_returns_json PASSED

Verification

Check that the file was modified:

bash
git diff sample-api/server.py

Should show your accepted diff.

Confirm Claude.ai recognizes the skill is active:
Navigate to claude.ai in your browser β†’ Settings β†’ Skills. You should see "Claude Code" listed under "Connected Tools" (not under "Available Skills"β€”Claude Code is built-in, not a separate Skill).

If you're using VS Code with the Claude Code extension:

  • Open the sample-api folder

  • Create a new file: endpoints.py

  • Cmd/Ctrl+K to open inline chat

  • Type: "Add a /metrics endpoint that logs request count"

  • The inline chat uses the same diff loop as the CLI


HOMEWORK

Task: Apply this to your own codebase (30–60 min)

  1. Pick a real file from your team's repository (or a personal project) that you've wanted to refactor but have been putting off. Aim for <200 lines.

  2. Write a 3-line CLAUDE.md at your repo root, covering:

    • Language / framework version and any recent architectural changes

      • One specific, measurable change you want Claude to make (not "improve the code")

  3. Run claude at the repo root, /add the file, and request the change.

  4. Accept the first diff only if it matches your intent exactly. If not, reject and iterate. Aim for 1–2 acceptances, not more.

  5. Run your test suite before committing.

  6. Deliverable you keep: A commit message documenting what changed, why Claude's suggestion was better than your first instinct, and what you'd do differently next time.

Why this matters: This is the moment you'll internalize why the diff loop exists. You'll feel the difference between "Claude generated code I copied" and "I reviewed and accepted a specific, bounded change."


APPENDIX: Troubleshooting

Q: Claude suggests code that's already in the file.

  • Cause: Your /add context doesn't include the full file, or you edited the file outside this session.

  • Fix: Exit with /done, restart with claude src/myfile.py, and /add src/myfile.py fresh.

Q: Diff fails to apply (exit code 1).

  • Cause: Someone or something modified the file between Claude's generation and your acceptance.

  • Fix: Reject the diff, let Claude re-generate with the updated file content.

Q: CLAUDE.md isn't helpingβ€”Claude still ignores my style guidelines.

  • Cause: CLAUDE.md is read at session start only. Changes made mid-session are ignored.

  • Fix: Exit and restart the session. Or add a reminder to your prompt: "Remember, we're using async/await everywhere."

Q: I set ANTHROPIC_API_KEY and now Claude Code uses my API credits instead of my Pro subscription.

  • Fix: Unset it: unset ANTHROPIC_API_KEY, then exit and restart claude. Pro billing resumes automatically.

Need help?