← Back to Blog

Practical Claude Code Patterns for Real Projects

Patterns, Not Theory

After building 21 games and 5 blog posts with Claude Code, I've distilled the process into 7 patterns that work. No theory, no frameworks — just bash scripts, config files, and hard-won lessons.

This is a recipe book. Each pattern is self-contained: a problem statement, a code block you can copy, and notes on when to use it. If you want the full story of how these patterns came together into an autonomous coding system, read How Ralph Loop Works. This post skips the narrative and gives you the recipes.

All code here is from a real production codebase — the one that built this site. These aren't hypothetical examples. They ran overnight, produced working software, and caught real bugs.

Pattern 1

The Headless Loop

The problem: You want Claude to run tasks autonomously — overnight builds, CI pipelines, batch processing — without you sitting at the keyboard.

The pattern: Call claude -p in a while true loop. The -p flag runs Claude in non-interactive "prompt mode" — it takes a string, does its work, and exits. Wrap that in a loop, and you get an agent that keeps working until you tell it to stop.

#!/usr/bin/env bash
set -euo pipefail

PROMPT_FILE="prompt.md"
STOP_FILE="loop_stop.md"

echo $$ > loop.pid  # write PID for watchdog

while true; do
    # Graceful shutdown via file
    [[ -f "$STOP_FILE" ]] && echo "Stop file found. Exiting." && exit 0

    # Read and run
    PROMPT="$(cat "$PROMPT_FILE")"
    claude -p "$PROMPT" \
        --output-format stream-json \
        --dangerously-skip-permissions \
        2>&1 | tee -a stream.jsonl

    # Check again (Claude may have written the stop file)
    [[ -f "$STOP_FILE" ]] && echo "Stop file found. Exiting." && exit 0

    sleep 3
done

The key flags:

The stop mechanism is a file. Claude itself writes loop_stop.md when it decides all work is done. The loop checks both before and after each invocation. No signals, no IPC — just a file on disk.

Use when: Automated overnight builds, CI pipelines, batch processing, any task where Claude should work without human input.
Don't use when: Exploratory coding, pair programming, learning a new codebase. Use interactive mode instead — the conversation matters.
Pattern 2

Hot-Swappable Prompts

The problem: Your loop is running overnight. You notice Claude is making games with tiny fonts. You want to fix the instructions without killing the loop and losing your place.

The pattern: Re-read the prompt file every iteration, not once at startup. This means you can edit prompt.md while the loop is running, and the next iteration picks up your changes immediately.

# THE ANTI-PATTERN: reading once at startup
PROMPT="$(cat prompt.md)"      # read once
while true; do
    claude -p "$PROMPT" ...    # same stale prompt forever
done

# THE PATTERN: re-reading every iteration
while true; do
    PROMPT="$(cat prompt.md)"  # fresh read each time
    claude -p "$PROMPT" ...    # picks up any edits
    sleep 3
done

This is a one-line change that fundamentally changes how you operate. With hot-swapping, the loop becomes steerable. Without it, you're launching a rocket and hoping the trajectory is right.

A real example: during a games marathon, Claude was building games that worked but had text too small to read on mobile. I opened prompt.md in another terminal and added one line:

Ensure all text is at least 18px and all touch targets are at least 44px.

The next game — and every game after it — came out perfectly responsive. No restart, no lost progress, no rebuilding context. The fix took effect on the very next cycle.

Validation matters too. If someone accidentally empties the prompt file, you don't want Claude invoked with a blank string:

PROMPT="$(cat "$PROMPT_FILE")"
if [[ -z "$PROMPT" ]]; then
    echo "WARNING: prompt.md is empty — waiting 5s..."
    sleep 5
    continue  # skip this iteration, try again
fi
Pattern 3

Permission Management

The problem: Claude Code asks for permission before running shell commands, writing files, and using tools. In interactive mode, you click "allow." In headless mode, nobody is there to click.

The pattern: Use .claude/settings.local.json to pre-approve specific tools and commands. This is the allowlist approach — only give Claude the exact permissions it needs.

{
  "permissions": {
    "allow": [
      "Bash(chmod:*)",
      "Bash(claude:*)",
      "Bash(git add:*)",
      "Bash(git commit:*)",
      "Bash(git push:*)",
      "Bash(python3:*)",
      "Bash(npm:*)",
      "mcp__plugin_playwright_playwright__browser_navigate",
      "mcp__plugin_playwright_playwright__browser_snapshot",
      "mcp__plugin_playwright_playwright__browser_click"
    ]
  }
}

Each entry follows a pattern: ToolName(command:arguments). The wildcard * means "any arguments for this command." Let's break down what each does:

Notice what's not on the list: Bash(rm:*), Bash(curl:*), Bash(sudo:*). The principle is least privilege — give Claude what it needs and nothing more.

For unattended overnight runs, there's also the nuclear option: --dangerously-skip-permissions. This flag bypasses all permission checks entirely. It's appropriate when:

It is not appropriate on your development machine with access to production credentials. The flag is named dangerously for a reason.

Pattern 4

Phase-Based State Management

The problem: You want Claude to build something complex — a blog post, a game, a feature — but doing it all in one session overflows the context window and produces half-finished work.

The pattern: Split every task into phases. Execute one phase per loop iteration. Track state in a STATUS.md file that Claude reads at the start of each session.

# My Feature — Status

## Current Phase: WRITE complete

## Outline
### Section 1: Setup (~200 words)
- Install dependencies
- Configure environment

### Section 2: Implementation (~400 words)
- Core logic
- Error handling

## Notes
- Use the existing auth module, don't reinvent
- Target audience: intermediate developers

The phases form a pipeline. For blog posts on this site, it's:

  1. RESEARCH — Study the topic, create a detailed outline in STATUS.md
  2. WRITE — Draft the complete article following the outline
  3. POLISH — Re-read, fix errors, verify code correctness, run a validation checklist
  4. INTEGRATE — Add the blog card to the index page, update the task list

For games, a slightly different set: BUILD → VALIDATE → POLISH → INTEGRATE.

Why does this work? Three reasons:

Context containment. Each phase starts with a clean context window. Claude only needs to hold the current phase's work in memory, not the entire history. A WRITE phase that would choke trying to also VALIDATE and POLISH runs cleanly on its own.

Crash recovery. If Claude crashes mid-WRITE, the next iteration reads STATUS.md, sees "Current Phase: RESEARCH complete," and starts WRITE again from scratch. No work is duplicated because the previous phase's output (the outline) is already on disk.

Rollback points. Every phase ends with a git commit. If POLISH introduces bugs, you git checkout the WRITE commit and try again. Each phase boundary is a known-good state.

The three-layer resilience stack: STATUS.md tracks task state. Git commits track code state. The watchdog tracks process state. Together, they make an autonomous loop fault-tolerant without any complex state management.
Pattern 5

Stream JSON for Real-Time Visibility

The problem: Claude is running headlessly. You want to know what it's doing right now — is it writing code? Is it stuck in a loop? Did it hit an error?

The pattern: Use --output-format stream-json to get newline-delimited JSON events, then parse them into a human-readable feed.

parse_stream() {
    while IFS= read -r line; do
        echo "$line" >> "$RAW_LOG"  # flight recorder

        type=$(echo "$line" | jq -r '.type // empty' 2>/dev/null) || continue

        case "$type" in
            assistant)
                text=$(echo "$line" | jq -r '.message.content[]? |
                    select(.type == "text") | .text // empty' 2>/dev/null)
                [[ -n "$text" ]] && echo "Claude: $text"
                ;;
            tool_use)
                tool=$(echo "$line" | jq -r '.tool_name' 2>/dev/null)
                echo "Tool: $tool"
                ;;
            tool_result)
                is_err=$(echo "$line" | jq -r '.is_error // false' 2>/dev/null)
                [[ "$is_err" == "true" ]] && echo "ERROR in tool result"
                ;;
            result)
                cost=$(echo "$line" | jq -r '.cost_usd // "?"' 2>/dev/null)
                dur=$(echo "$line" | jq -r '.duration_ms // "?"' 2>/dev/null)
                echo "Done — Cost: \$$cost | Time: ${dur}ms"
                ;;
        esac
    done
}

The four event types you'll see:

The flight recorder pattern is the key insight here. Every raw JSON line gets appended to a JSONL file. You now have a complete, machine-readable record of everything Claude did — every tool call, every file it read, every command it ran. This is invaluable for debugging stuck loops and post-hoc analysis.

Pipe it to your loop like this:

claude -p "$PROMPT" \
    --output-format stream-json \
    --dangerously-skip-permissions \
    2>&1 | parse_stream
Pattern 6

The Watchdog

The problem: Agents get stuck. They hang on API calls, enter infinite retry loops, or write code that deadlocks. If you leave a loop running overnight without a supervisor, you'll wake up to find it frozen at 3 AM.

The pattern: A separate process that monitors the loop and restarts it when something goes wrong. Three checks: Is the process alive? Is the log file growing? Has a stop file appeared?

STUCK_TIMEOUT=1800  # 30 minutes
CHECK_INTERVAL=30
LAST_LOG_SIZE=0
LAST_LOG_CHANGE="$(date +%s)"

while true; do
    sleep "$CHECK_INTERVAL"

    # Check 1: Is the loop process alive?
    PID="$(cat loop.pid 2>/dev/null)"
    if ! kill -0 "$PID" 2>/dev/null; then
        echo "Loop died. Restarting..."
        start_loop
        continue
    fi

    # Check 2: Is the log file growing?
    CURRENT_SIZE="$(stat -c%s loop.log 2>/dev/null || echo 0)"
    NOW="$(date +%s)"

    if [[ "$CURRENT_SIZE" -ne "$LAST_LOG_SIZE" ]]; then
        LAST_LOG_SIZE="$CURRENT_SIZE"
        LAST_LOG_CHANGE="$NOW"
    else
        ELAPSED=$(( NOW - LAST_LOG_CHANGE ))
        if [[ "$ELAPSED" -ge "$STUCK_TIMEOUT" ]]; then
            echo "STUCK for ${ELAPSED}s. Killing and restarting."
            kill "$PID" 2>/dev/null
            start_loop
            LAST_LOG_SIZE=0
            LAST_LOG_CHANGE="$(date +%s)"
        fi
    fi

    # Check 3: Has the stop file appeared?
    [[ -f "loop_stop.md" ]] && echo "Stop file found. Shutting down." && exit 0
done

The stuck detection logic is simple: if the log file hasn't grown in 30 minutes, something is wrong. Kill the process and restart. The assumption is that if Claude is working, it's producing output. Silence means trouble.

Two lessons from production:

Don't set the timeout too low. The first version used 10 minutes. It turned out that some legitimate POLISH phases — especially reviewing long articles — take longer than that. The watchdog kept killing healthy sessions. 30 minutes eliminated the false positives.

Use tmux for the restart. The production watchdog runs both the loop and itself in named tmux windows. When it restarts the loop, it sends the command to a tmux pane rather than spawning a background process. This means you can tmux attach at any time and see live output from both the loop and the watchdog. It's the difference between flying blind and having a cockpit.

Use when: Any unattended run longer than an hour. Overnight builds. Multi-task pipelines.
Don't use when: Interactive sessions where you're at the keyboard. Quick one-off tasks.
Pattern 7

Structured Commit Messages as Audit Trail

The problem: Your autonomous loop has been running for hours. You want to know what it accomplished, what phase each task is in, and whether anything went wrong.

The pattern: Use a consistent commit message format that makes git log your progress report.

# The format:
# [task-name] Phase N PHASE complete — summary

# Real examples from production:
git log --oneline

# 7839cac [claude-code-patterns] Phase 1 RESEARCH complete — outline with 7 patterns
# 41d9676 [ralph-loop-explained] Phase 4 INTEGRATE complete — blog card added
# 46e3ee8 [ralph-loop-explained] Phase 3 POLISH complete — validated rendering
# 29382ae [ralph-loop-explained] Phase 2 WRITE complete — full meta post
# fbf7830 [ralph-loop-explained] Phase 1 RESEARCH complete — outline with 10 sections
# f5e7e07 [attention-from-scratch] Phase 4 INTEGRATE complete — blog card added
# ce0bcb4 [attention-from-scratch] Phase 3 POLISH complete — fixed dot product values
# 45813bd [attention-from-scratch] Phase 2 WRITE complete — full blog post

Every commit message has three parts:

This gives you instant visibility with a single command:

# See all phase completions
git log --oneline | grep "Phase"

# See all work on a specific task
git log --oneline | grep "attention-from-scratch"

# See just the POLISH phases (where bugs get fixed)
git log --oneline | grep "POLISH"

The implicit benefit: every phase boundary is a rollback point. If Claude writes a bad POLISH, you don't have to redo the whole task. You git checkout the WRITE commit and run POLISH again with a fresh context. The commit history is your undo stack.

Instruct Claude to commit with this format in your prompt. The prompt used for this site includes:

After completing the phase, commit with the format: [task-name] Phase N PHASE complete — one-line summary. Then push to remote.

Putting It All Together

The seven patterns form a dependency chain. Each one solves a specific problem, and they compose into a complete autonomous system:

  1. The Headless Loop gives you unattended execution
  2. Hot-Swappable Prompts make it steerable without restarts
  3. Permission Management makes it safe to run unattended
  4. Phase-Based State makes it crash-recoverable
  5. Stream JSON makes it observable
  6. The Watchdog makes it self-healing
  7. Structured Commits make it auditable

You don't need all seven on day one. Start with Pattern 1 — a bare while true loop calling claude -p. Run it interactively. When you notice the prompt needs tweaking mid-run, add Pattern 2. When you leave it unattended for the first time, add Pattern 6. Build up as you hit real problems.

The progression looks like this:

The fully autonomous version is what built this site: 21 games and 5 blog posts, running overnight while I slept. The three files that make it work — the loop script, the watchdog, and the prompt — are each under 200 lines of bash. No framework. No orchestration platform. Just patterns that compose.

References & Further Reading