Skip to content

Headless Mode and Scripting

The -p flag turns Claude into a scriptable tool. Pipe input, capture output, chain with other commands. Automation starts here.

Goodbye Interactive, Hello Scriptable

Everything so far assumes you're sitting at a terminal, typing prompts, reading responses, approving tool calls. That's fine for development. But it's only half of what Claude Code can do.

The -p flag (short for --print) flips the whole model. It turns Claude Code into a non-interactive, scriptable CLI tool. Input in, result out, exit. No human in the loop. No approval prompts. No conversation state lingering around.

This flag matters more than any other you'll learn. Once you internalize -p, Claude Code stops being "an AI assistant you chat with" and becomes a programmable tool you call from scripts, CI pipelines, git hooks, build systems, cron jobs, whatever.

The Basics of Print Mode

Simplest usage. Pass a prompt as a string:

claude -p "Explain what this project does"

It reads your project context (CLAUDE.md included), processes the prompt, prints to stdout, exits.

Default output is plain text. You can get JSON instead:

claude -p "List all exported functions in src/utils.ts" --output-format json

JSON output includes the response text plus metadata: cost, token usage, session ID. There's also stream-json for real-time token streaming when you need progressive output.

Piping

Claude Code reads from stdin, so you can feed it anything:

cat error.log | claude -p "explain this error and suggest a fix"
gh pr diff "$1" | claude -p "review this diff for security issues"
cat src/api/routes.ts | claude -p "extract all route paths as a JSON array" --output-format json

Pipe output forward too. Full stdin-to-stdout pipeline, just like you would expect:

cat schema.sql | claude -p "generate TypeScript interfaces for these tables" > types.ts

Unix philosophy. Claude Code is just another tool in your pipeline, sitting between cat and > like grep or awk would.

Controlling Tool Access and Spending

In interactive mode, Claude asks before running commands or editing files. In headless mode, nobody's there to ask. You need to be explicit about permissions:

claude -p "Run the test suite and fix any failures" --allowedTools "Bash,Read,Edit"

--allowedTools takes a comma-separated list. Want Claude to only analyze code without touching anything? Leave it out and it defaults to read-only.

Two more flags you'll want for automation. --max-turns caps how many agentic steps Claude takes before stopping. Without it, Claude in CI might loop forever trying to fix a flaky test. --max-budget-usd does the same thing but in dollars. Hard spending limit per invocation.

claude -p "Refactor this module" --allowedTools "Read,Edit" --max-turns 10 --max-budget-usd 0.50

Structured Output with JSON Schema

Sometimes you don't just want JSON, you want JSON in a specific shape. The --json-schema flag paired with --output-format json guarantees structure:

claude -p "Analyze this codebase for security issues" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"issues":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string"},"description":{"type":"string"},"file":{"type":"string"}}}}}}'

The response will match your schema. You can parse it reliably in downstream scripts instead of hoping Claude happened to format things the way you wanted.

Real Scripting Patterns

A few patterns that keep coming up:

Fan-out processing. Loop through files, process each independently:

for file in src/components/*.tsx; do
  claude -p "Review this React component for accessibility issues: $(cat $file)" >> a11y-report.txt
done

Build script integration. Add Claude Code as a linter in your package.json:

{
  "scripts": {
    "lint:claude": "claude -p 'review staged changes vs main for code quality issues'"
  }
}

npm run lint:claude works like any other lint step. Pre-commit hooks, CI checks, whatever.

Continuing conversations. --continue picks up the most recent conversation. --resume with a session ID returns to a specific one. Useful for multi-pass scripts:

SESSION=$(claude -p "Analyze the test coverage gaps" --output-format json | jq -r '.session_id')
claude -p "Now write tests for the top 3 gaps you found" --resume "$SESSION" --allowedTools "Read,Edit"

Adding system instructions. --append-system-prompt injects additional instructions while keeping all defaults (including CLAUDE.md) intact:

claude -p "Review this PR" --append-system-prompt "Focus only on performance. Ignore style issues."

Try It

Pick any file in your project:

cat README.md | claude -p "Rate this README from 1-10 on clarity, completeness, and accuracy. Output as JSON." --output-format json

Chain it. Pipe the output to jq to extract just the scores, or redirect to a file. The point is to feel Claude Code as a pipeline tool, not a conversation partner.

Now try the fan-out pattern. Pick a directory with a few files:

for f in src/lib/*.ts; do
  echo "=== $f ===" >> review.txt
  cat "$f" | claude -p "One-paragraph code review" >> review.txt
done

What Doesn't Work

Slash commands and skills aren't available in -p mode. Those depend on the interactive UI. In headless mode you have the raw tool, the prompt, and the flags. Honestly that's a feature, not a limitation. Scripts should be explicit about what they want.

The Bigger Picture

CI/CD integration, automated code review, custom toolchains, multi-agent orchestration. All of that builds on -p. Once Claude Code can be called from a script, it can be called from anywhere: a GitHub Action, a cron job, a Makefile, a pre-commit hook, another AI agent.

You use the interactive terminal to figure things out. Headless mode is how you ship what you figured out.

Mastering Claude Code

Lesson 59 of 70