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.
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:
-p— Non-interactive prompt mode. Takes a string, returns a result, exits. This is what makes headless operation possible.--output-format stream-json— Emits newline-delimited JSON events as Claude works. Pipe this to a parser for live monitoring (see Pattern 5).--dangerously-skip-permissions— Skips the interactive permission prompts that would block an unattended loop. Use this only in sandboxed environments you control.
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.
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
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:
Bash(git add:*),Bash(git commit:*),Bash(git push:*)— Version control. Claude commits after every phase.Bash(python3:*),Bash(npm:*)— Build and run project tools.Bash(claude:*)— Allows Claude to call itself recursively (used in some advanced patterns).mcp__plugin_playwright_*— Browser automation for visual testing during VALIDATE/POLISH phases.
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:
- You're running in a sandboxed environment (container, VM, disposable instance)
- The worst case is "Claude breaks the project and I git reset"
- You've already tested the prompt interactively and trust the workflow
It is not appropriate on your development machine with access to production credentials. The flag is named dangerously for a reason.
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:
- RESEARCH — Study the topic, create a detailed outline in STATUS.md
- WRITE — Draft the complete article following the outline
- POLISH — Re-read, fix errors, verify code correctness, run a validation checklist
- 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.
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:
assistant— Claude is generating text. Contains.message.content[]with the response.tool_use— Claude is calling a tool (file read, shell command, web fetch). Contains.tool_nameand.input.tool_result— The result of a tool call. Contains.is_errorand.outputor.content.result— The session is finished. Contains.cost_usd,.duration_ms, and token counts.
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
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.
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:
[task-name]— The scope. What's being worked on.Phase N PHASE complete— The progress marker. Grep-friendly.— summary— One line describing what was actually done.
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:
- The Headless Loop gives you unattended execution
- Hot-Swappable Prompts make it steerable without restarts
- Permission Management makes it safe to run unattended
- Phase-Based State makes it crash-recoverable
- Stream JSON makes it observable
- The Watchdog makes it self-healing
- 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:
- Manual: You type
claude -p "..."one invocation at a time - Headless: A bash loop runs invocations back to back (Pattern 1)
- Phase-based: Tasks split into phases with STATUS.md tracking (Pattern 4)
- Fully autonomous: Loop + watchdog + streaming + structured commits (all 7 patterns)
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
- Anthropic — Claude Code Documentation — The CLI tool that powers all of these patterns. Start with the
-pflag and--output-formatoptions. - Anthropic — "Claude Code: Best practices for agentic coding" — Official patterns and recommendations for autonomous workflows.
- Anthropic — "Effective Harnesses for Long-Running Agents" — Deep dive into harness design, retry logic, and supervisory patterns for agents.
- How Ralph Loop Works — The full story behind these patterns. If this post is the cookbook, that one is the memoir.