Recommended resources
The freely available resources this lesson will direct you to.
- Building headless automation with Claude Code | Code w/ Claudewww.youtube.com
- Claude Ralph - The Bizarre Anthropic Plugin That Every Developer Is Missingwww.youtube.com
- Collection of Claude Code skills for enhanced AI workflows - GitHubgithub.com
- Forcing Claude Code to TDD: An Agentic Red-Green-Refactor Loopalexop.dev
Hello! Welcome back to our advanced course on Claude Code.
In our last lesson, we mastered a crucial interactive workflow: debugging runtime errors in a FastAPI application by providing Claude with a structured package of evidence, including tracebacks and logs. This honed your ability to guide the AI through complex problems in a live environment.
Today, we take a significant leap from interactive guidance to full automation. We will move beyond the interactive Claude Code terminal and learn how to run Claude programmatically in "headless" mode. Your goal for this lesson is to create a shell script that uses headless Claude Code to automate a complete Test-Driven Development (TDD) cycle for a given function. This is the foundation for building powerful, automated developer tools and integrating AI into your CI/CD pipelines.
The TDD Cycle: Our Automation Blueprint
Before we script it, let's revisit the process we're automating. The TDD cycle is a disciplined loop of three distinct phases: Red, Green, and Refactor.

Headless Mode: The Engine of Automation
To automate this cycle, we need a way to call Claude from a script, pass it instructions, and parse its response. This is precisely what "headless" mode is for. The Claude Code SDK provides a command-line binary, claude-b, that allows for this non-interactive execution.
Let's watch a short video from Anthropic that introduces the SDK and its core capabilities.
Building headless automation with Claude Code | Code w/ Claude
This video, 'Building headless automation with Claude Code', provides an excellent overview of the SDK and how to use it from your terminal.
Please watch from 01:36 to 04:38. Pay close attention to: The fundamental concept of the SDK as a 'Unix tool'. Given your backend experience, you'll recognize how this enables piping and chaining commands. The syntax for basic calls: claude-b "<your prompt>". The crucial --output-format json flag, which makes Claude's responses machine-readable. The --allow-tools write flag, which grants permission for the AI to modify your filesystem—essential for our TDD script.
As you saw, the --output-format json flag is the key to scripting. When used, claude-b returns a JSON object containing the AI's response, any tool usage, and session information. You can then use a command-line JSON processor like jq to parse this output and extract the information you need, such as the content of a file Claude has written.
For example, a simple headless call might look like this:
# Ask Claude to create a file and pipe the JSON output to jq to extract the final chat message
claude-b "write a python function that adds two numbers into a file called math_utils.py" --allow-tools write --output-format json | jq '.chat.messages[-1].content'
This command executes the prompt and then cleanly extracts only the final text response from Claude, demonstrating how you can integrate it into a larger script.
Scripting the Red-Green-Refactor Flow
A single, monolithic prompt to "do TDD" often fails due to a phenomenon called "context pollution." The AI, seeing the entire task at once, might write a test that is subconsciously tailored to the implementation it's already planning. This defeats the purpose of TDD.
To enforce a true TDD process, we must orchestrate it with a script that makes separate, isolated calls to headless Claude for each phase. This mirrors the advanced "multi-agent" architectures used in sophisticated Claude Code skills, but we can implement it with a straightforward shell script.
Let's break down the script's logic, phase by phase.
1. The RED Phase: Generate a Failing Test
The first step is to ask Claude to write a test that is guaranteed to fail.
- Prompt Claude: The script will call
claude-bwith a prompt asking it to write a failingpytesttest for a specified function signature. - Permissions: It will use
--allow-tools writeso Claude can create the test file. - Execution: The script then runs
pytestand checks for a failing exit code to confirm the "Red" state.
# --- tdd_script.sh (RED Phase) ---
# Define the RED phase prompt
RED_PROMPT="You are a Python TDD expert. Write a single failing pytest test for a function is_valid_email(email: str) in 'app/utils.py'. The function doesn't exist yet. Place the test in 'tests/test_utils.py'. The test should assert that is_valid_email('test@example.com') is True. Create the necessary files and directories."
# Call Claude headlessly
echo "🔴 Starting RED phase..."
claude-b "$RED_PROMPT" --allow-tools write,bash
# Verify the test fails
echo " Running tests to confirm failure..."
if ! pytest; then
echo " ✅ RED phase successful: Test failed as expected."
else
echo " ❌ RED phase failed: Test passed unexpectedly."
exit 1
fi
2. The GREEN Phase: Write Code to Pass the Test
Now, with a failing test and its error output, the script instructs Claude to write the minimum code necessary to make it pass.
- Gather Context: The script prepares a new prompt that includes the path to the failing test file (
@tests/test_utils.py), the error message from the previouspytestrun, and the goal. - Prompt Claude: The
claude-bcall asks Claude to implement the function to resolve the test failure. - Execution: The script runs
pytestagain, this time asserting that the test passes.
# --- tdd_script.sh (GREEN Phase) ---
# Capture the failing test output
FAILING_OUTPUT=$(pytest 2>&1)
# Define the GREEN phase prompt
GREEN_PROMPT="The test at @tests/test_utils.py is failing. Here is the output from pytest:
\`\`\`
$FAILING_OUTPUT
\`\`\`
Write the minimal implementation for the is_valid_email function in 'app/utils.py' to make the test pass."
# Call Claude headlessly
echo "🟢 Starting GREEN phase..."
claude-b "$GREEN_PROMPT" --allow-tools write,edit
# Verify the test passes
echo " Running tests to confirm pass..."
if pytest; then
echo " ✅ GREEN phase successful: Test passed."
else
echo " ❌ GREEN phase failed: Test still fails."
exit 1
fi
A Note on Robustness: For more complex tasks, you could even create a loop for the GREEN phase, similar to the logic in the Ralph plugin, which re-runs a prompt until a condition is met. The script could check the pytest exit code and, if it's still failing, ask Claude to try again, providing the new error as context.
3. The REFACTOR Phase: Improve the Code
With a passing test, the final step is to clean up the code and tests.
- Gather Context: The script provides Claude with both the implementation file (
@app/utils.py) and the test file (@tests/test_utils.py). - Prompt Claude: The prompt asks Claude to refactor the code for clarity, efficiency, and to add more comprehensive test cases (e.g., for an invalid email), while ensuring all tests remain green.
- Execution: A final
pytestrun verifies that the refactoring didn't break anything.
# --- tdd_script.sh (REFACTOR Phase) ---
REFACTOR_PROMPT="Review the function in @app/utils.py and its test in @tests/test_utils.py. Refactor both for improved readability and best practices. Also, add a new test case for an invalid email address. Ensure all tests continue to pass."
# Call Claude headlessly
echo "🔵 Starting REFACTOR phase..."
claude-b "$REFACTOR_PROMPT" --allow-tools edit
# Final verification
echo " Running tests for final verification..."
if pytest; then
echo " ✅ REFACTOR phase successful: All tests pass."
echo "🎉 TDD cycle complete!"
else
echo " ❌ REFACTOR phase failed: Refactoring broke the tests."
exit 1
fi
Your Automated TDD Script
Here is the complete shell script. It automates the entire TDD cycle for creating a simple email validation function. As someone experienced with scripting and backend systems, you can see how this orchestrates the separate AI calls to enforce a disciplined workflow.
#!/bin/bash
# A script to automate a single TDD cycle using headless Claude Code.
# Ensure the environment is clean
rm -f app/utils.py tests/test_utils.py
mkdir -p app tests
# --- 1. RED PHASE ---
echo "🔴 Starting RED phase..."
RED_PROMPT="You are a Python TDD expert. Write a single failing pytest test for a function is_valid_email(email: str) in 'app/utils.py'. The function doesn't exist yet. Place the test in 'tests/test_utils.py'. The test should assert that is_valid_email('test@example.com') is True. Create the necessary files and directories."
claude-b "$RED_PROMPT" --allow-tools write,bash > /dev/null 2>&1
echo " Running tests to confirm failure..."
if ! pytest >/dev/null 2>&1; then
echo " ✅ RED phase successful: Test failed as expected."
FAILING_OUTPUT=$(pytest 2>&1)
else
echo " ❌ RED phase failed: Test passed unexpectedly."
exit 1
fi
# --- 2. GREEN PHASE ---
echo "🟢 Starting GREEN phase..."
GREEN_PROMPT="The test at @tests/test_utils.py is failing. Here is the output from pytest:
\`\`\`
$FAILING_OUTPUT
\`\`\`
Write the minimal implementation for the is_valid_email function in 'app/utils.py' to make the test pass."
claude-b "$GREEN_PROMPT" --allow-tools write,edit > /dev/null 2>&1
echo " Running tests to confirm pass..."
if pytest >/dev/null 2>&1; then
echo " ✅ GREEN phase successful: Test passed."
else
echo " ❌ GREEN phase failed: Test still fails."
exit 1
fi
# --- 3. REFACTOR PHASE ---
echo "🔵 Starting REFACTOR phase..."
REFACTOR_PROMPT="Review the function in @app/utils.py and its test in @tests/test_utils.py. Refactor both for improved readability, adherence to PEP 8, and add a test case for an invalid email like 'not-an-email'. Ensure all tests continue to pass."
claude-b "$REFACTOR_PROMPT" --allow-tools edit > /dev/null 2>&1
echo " Running tests for final verification..."
if pytest >/dev/null 2>&1; then
echo " ✅ REFACTOR phase successful: All tests pass."
echo "🎉 TDD cycle complete!"
else
echo " ❌ REFACTOR phase failed: Refactoring broke the tests."
exit 1
fi
# Display final files
echo -e "\n--- Final implementation (app/utils.py) ---"
cat app/utils.py
echo -e "\n--- Final tests (tests/test_utils.py) ---"
cat tests/test_utils.py
You can save this script as tdd_script.sh, make it executable with chmod +x tdd_script.sh, and run it with ./tdd_script.sh.
Conclusion
In this lesson, you have moved from an interactive user of Claude Code to an automator. You've learned how to leverage headless mode to build a script that enforces a sophisticated development methodology like TDD. This is a powerful paradigm shift that unlocks a new level of productivity and workflow automation.
Key Takeaways:
- Headless for Automation: The
claude-bcommand-line tool is the gateway to scripting and automating Claude Code tasks. - Structured Output is Key: Using
--output-format jsonallows your scripts to parse Claude's responses and make decisions. - Isolate for Purity: Automating a TDD cycle requires orchestrating multiple, isolated headless calls to prevent context pollution and enforce the Red-Green-Refactor sequence.
- From Prompting to Scripting: You've transformed a series of well-crafted prompts into a single, executable, and automated workflow.
Next Up:
We've successfully created a standalone automation script. The next step is to integrate this capability into a professional development pipeline. In our next module, we will learn how to set up and run headless Claude Code within a GitHub Actions workflow, enabling automated code reviews, documentation generation, and more on every pull request.