Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to the course.
In our last lesson, we mastered the fundamentals of running Claude Code in headless mode. You learned how to execute non-interactive prompts, pipe data via stdin, andβmost criticallyβparse structured JSON output to make Claude's responses usable in scripts.
Today, we will assemble those building blocks into a powerful, automated workflow. You will learn how to build a shell script that orchestrates a sequence of related tasks with Claude Code. We'll introduce two crucial concepts for this: maintaining context across multiple headless calls and controlling tool access with precision using flags. By the end of this lesson, you'll be able to create scripts that automate complex, multi-step development tasks, moving you closer to building professional-grade automations.
Chaining Commands: The Essence of Shell Scripting
First, let's quickly reinforce the core idea of using Claude Code as a command-line tool that can be part of a larger chain.
Claude Code - 47 PRO TIPS in 9 minutes
This short clip from 'Claude Code - 47 PRO TIPS in 9 minutes' by Greg Baugues provides a great, high-level overview of treating Claude Code as a standard CLI tool.
Watch the segment from 00:21 to 01:05. Notice the emphasis on chaining tools, piping data, and running in headless mode with the -p flag. These are the foundational ideas for our script today.
Maintaining Context in a Multi-Step Workflow
A key challenge in creating a multi-step workflow is ensuring context is not lost between steps. If you make two separate claude -p calls, they are independent by default. The second call has no memory of the first. To build a coherent workflow, we need to link them.
This is achieved using session management flags:
--session-id <ID>: Starts a new headless session with a specific, unique identifier.--resume <ID>: Resumes a previously created session, loading its entire conversation history.
The following resource demonstrates this concept beautifully by building a script that performs an initial check and then "escalates" to a second step if needed, all while preserving context.
Bridge Automated and Human-in-the-Loop AI Workflows ... - Dev.build
The article 'Bridge Automated and Human-in-the-Loop AI Workflows' by John Lindquist provides the best explanation of session management for automation.
Please read the sections 'Workflow demonstrated in this lesson', 'Terminal Commands', and 'Code Snippets'. \nFocus on how uuidgen is used to create a unique ID, which is then passed to --session-id on the first call and --resume on the second. The check.sh script is a perfect, minimal example of a multi-step workflow.
As you saw in the check.sh script, a session ID acts as a handle to a persistent conversation, allowing your script to build context incrementally across multiple, independent claude invocations.
Controlling Tool Access for Safety and Intent
When you automate tasks, especially those that modify your filesystem, you need precise control over what the AI is allowed to do. Granting unrestricted access is risky. Claude Code allows you to grant specific permissions for each call using the --allow-tools flag.
This next video provides a clear explanation of how to use this flag.
Building headless automation with Claude Code | Code w/ Claude
This segment from Anthropic's 'Building headless automation with Claude Code' video explains the --allow-tools flag in detail.
Watch from 09:38 to 10:38. Pay close attention to the syntax for allowing specific bash commands (e.g., npm test) and built-in tools like write. This granularity is key to building safe and robust automations.
The syntax is straightforward and powerful:
--allow-tools write: Allows Claude to create or overwrite files.--allow-tools edit: Allows Claude to apply changes to existing files.--allow-tools read: Allows Claude to read files. (Often implicitly available, but good to be explicit).--allow-tools bash:"<command>": Allows Claude to run a specific shell command, likebash:"pytest"orbash:"npm run build".--allow-tools <mcp-tool-name>: Allows access to a configured MCP tool.
You can provide a comma-separated list to allow multiple tools: --allow-tools write,bash:"pytest".
This image shows a practical example of this flag in a command line.

Practical Application: A FastAPI Scaffolding Script
Now, let's combine these concepts to build a practical, multi-step script that automates a common task from your world of FastAPI development: scaffolding a new API resource.
Our script, create_resource.sh, will:
- Accept a resource name (e.g., "product").
- Step 1: Generate a Pydantic model for the resource and save it to
models/{resource_name}.py. - Step 2: Generate a FastAPI router with basic CRUD endpoints for that model, saving it to
routers/{resource_name}.py. - Step 3: Automatically import and include the new router in your main
main.pyapplication file.
This workflow requires session management to link the steps and precise tool control (write for new files, edit for modifying main.py).
Before we dive into our script, let's look at a well-structured example of a similar automation to see what a production-ready script might look like.
Hands-On with Claude Code: Automating Git Workflows and Legacy ...
The article 'Hands-On with Claude Code' contains an excellent example of a multi-step Git automation script.
Find the section 'Creating a Reusable Git Automation Workflow' and study the gcpr shell function. Note how it chains multiple claude -p calls, saves output to variables, and includes error handling (if [[ $EXIT_CODE -ne 0 ]]). This is a great model for robust scripting.
The create_resource.sh Script
Inspired by the best practices we've seen, here is the complete script for our FastAPI resource scaffolder. Create a file named create_resource.sh, make it executable (chmod +x create_resource.sh), and then you can run it with ./create_resource.sh <resource_name>.
#!/bin/bash
# Exit immediately if a command exits with a non-zero status.
set -e
# Check if a resource name was provided.
if [ -z "$1" ]; then
echo "Usage: ./create_resource.sh <resource_name>"
exit 1
fi
RESOURCE_NAME=$1
MODEL_NAME=$(echo "$RESOURCE_NAME" | awk '{print toupper(substr($0,1,1))substr($0,2)}') # Capitalize first letter
SESSION_ID=$(uuidgen)
echo "π Starting to scaffold resource: $RESOURCE_NAME"
# --- Step 1: Generate Pydantic Model ---
echo " 1. Generating Pydantic model..."
MODEL_PROMPT="Create a Pydantic model named '${MODEL_NAME}' for a '${RESOURCE_NAME}' resource. It should have an 'id' (UUID), 'name' (string), and 'created_at' (datetime). Save it to 'models/${RESOURCE_NAME}.py'."
claude -p "$MODEL_PROMPT" --session-id "$SESSION_ID" --allow-tools write --output-format json > /dev/null
echo " β
Model file 'models/${RESOURCE_NAME}.py' created."
# --- Step 2: Generate FastAPI Router ---
echo " 2. Generating FastAPI router..."
ROUTER_PROMPT="Now, create a FastAPI router for the '${MODEL_NAME}' model. Include basic GET (list and by ID), POST, and DELETE endpoints. Use in-memory storage for this example. Save the complete router code to 'routers/${RESOURCE_NAME}.py'."
claude -p "$ROUTER_PROMPT" --resume "$SESSION_ID" --allow-tools write,read --output-format json > /dev/null
echo " β
Router file 'routers/${RESOURCE_NAME}.py' created."
# --- Step 3: Update main.py to include the new router ---
echo " 3. Integrating router into main.py..."
INTEGRATION_PROMPT="Finally, edit the 'main.py' file. Import the new router from 'routers.${RESOURCE_NAME}' and include it in the FastAPI app using 'app.include_router'."
claude -p "$INTEGRATION_PROMPT" --resume "$SESSION_ID" --allow-tools edit --output-format json > /dev/null
echo " β
'main.py' updated to include the new router."
echo "π Scaffolding complete for resource: $RESOURCE_NAME"
Breakdown of the Script:
set -e: A standard shell practice to ensure the script fails immediately if any command fails.SESSION_ID=$(uuidgen): We generate a unique ID to link all subsequentclaudecalls.- Step 1 (Model): We use
--session-idto start the conversation and--allow-tools writebecause we are creating a new file. The JSON output is redirected to/dev/nullas we only care about the side effect (file creation). - Step 2 (Router): We use
--resumeto continue the conversation. Claude now has the context of the model it just created. We need--allow-tools read,writeso it can conceptually "read" the model definition for context while writing the new router file. - Step 3 (Integration): We again
--resumethe session. This time, we use--allow-tools editto grant permission to modify an existing file,main.py.
This script is a tangible example of how you can automate away boilerplate code generation, enforcing consistency and saving timeβa true "power-user" workflow.
Conclusion
In this lesson, you've bridged the gap from running single headless commands to orchestrating complex, automated workflows. You now have the tools to build scripts that mirror and accelerate your own development processes.
Key Takeaways:
- Multi-Step Workflows Need Context: Use
--session-idand--resumeto create a persistent conversation that links multipleclaudecalls together. - Control is Paramount: The
--allow-toolsflag is essential for creating safe and predictable automations. Grant permissions (likewrite,edit, orbash:<cmd>) on a per-call basis. - Structure Your Scripts: Model your scripts after robust examples by handling inputs, checking for errors, and providing clear feedback to the user.
Next Up:
You've successfully automated a workflow on your local machine. The next logical step is to take this automation into the cloud. In our next lesson, we will adapt what you've learned today to a CI/CD environment by setting up Claude Code in a GitHub Actions workflow to automate tasks directly within your development pipeline.