Module 7: Integrations: Git, SDK, and Remote Control

Headless mode CLI: automation without interaction

Headless mode CLI: automation without interaction

Overview

The SDK gives you programmatic access to Claude Code from Python or TypeScript. But sometimes you don't need all that machinery. Sometimes what you want is a bash command that sends a prompt to Claude Code, gets the answer back, and that's it. No scripts, no async/await, no imports — a one-liner in the terminal.

That's what headless mode is for: the -p flag that turns Claude Code into a non-interactive command-line tool. You hand it a prompt, it hands you a response, and the process exits. No conversation, no approvals, no interaction. It's Claude Code as one more pipe in your Unix pipeline.

This capsule covers how to use headless mode, what output formats are available, how to combine it with other flags, and how to build practical automation scripts that run with no human watching.


What headless mode is

Headless mode is Claude Code running non-interactively. Instead of the normal flow (prompt → response → prompt → response), headless mode is:

Input (prompt) → Processing → Output (response) → Process exits

The -p flag

claude -p "your prompt here"

That's it. Claude Code takes the prompt, performs whatever actions it needs (read files, analyze code, etc.), writes the answer to stdout, and the process ends.

Basic example

claude -p "What framework does this project use?"

Output:

This project uses FastAPI (Python) with SQLAlchemy as the ORM 
and Pydantic for data validation.

Intermediate example

claude -p "List every API endpoint with its HTTP method and path"

Output:

Endpoints found:

GET  /api/health          - Health check
POST /api/auth/login      - User login
POST /api/auth/register   - User registration
GET  /api/users           - List users
GET  /api/users/:id       - Get user by ID
PUT  /api/users/:id       - Update user
DELETE /api/users/:id     - Delete user
GET  /api/products        - List products
POST /api/products        - Create product

Output formats

Headless mode supports three output formats, which you control with --output-format:

text (default)

claude -p "Explain the main function" --output-format text

Returns plain text. It's the default format — you don't have to specify it.

When to use it: simple scripts, human-readable output, piping into other text commands.

json

claude -p "Explain the main function" --output-format json

Returns a JSON object with the complete response:

{
  "type": "result",
  "subtype": "success",
  "cost_usd": 0.003,
  "is_error": false,
  "duration_ms": 2340,
  "duration_api_ms": 1850,
  "num_turns": 1,
  "result": "The main() function is the application's entry point...",
  "session_id": "abc123"
}

When to use it: when you need to parse the response programmatically, check costs, or integrate with other systems that expect JSON.

stream-json

claude -p "Explain the main function" --output-format stream-json

Returns JSON messages line by line as Claude Code produces output:

{"type":"system","subtype":"init","session_id":"abc123"}
{"type":"assistant","message":{"type":"text","text":"The main() function"}}
{"type":"assistant","message":{"type":"text","text":" is the entry point"}}
{"type":"result","subtype":"success","cost_usd":0.003,"result":"..."}

When to use it: when you need real-time streaming, want to show progress, or want to process the response as it's generated.

Format comparison

FormatReal-time progressParseableMetadataUse case
text❌❌❌Simple scripts, human reading
json❌✅✅System integration, CI/CD
stream-json✅✅✅Streaming, UIs, partial processing

Complementary flags

⚠️ Flags evolve: Claude Code updates frequently. Check the available flags with claude --help or claude -p --help before using them in production scripts.

Headless mode combines with other flags for fine-grained control:

--model: pick the model

claude -p "Review this code" --model opus
claude -p "Quick fix" --model sonnet

--max-turns: cap the iterations

claude -p "Analyze this project" --max-turns 3

Caps how many times Claude Code can iterate (read files, run tools, etc.) before it gives the final answer. Useful for controlling cost and time.

--dangerously-skip-permissions

claude -p "Fix the linting errors" --dangerously-skip-permissions

Skips every permission check. Claude Code can read, write, and execute without asking for approval.

⚠️ EXTREME CAUTION: This flag should be used ONLY in controlled, isolated environments (containers, CI/CD sandboxes, ephemeral environments). Never on your development machine with production code. With --no-permissions, Claude Code can run any command without supervision, including destructive operations like deleting files or changing critical configurations. If you need automation, prefer --allowedTools to pre-approve only the specific tools you need.

--output-file: save output to a file

claude -p "Generate API documentation" --output-file docs/api.md

--system-prompt: a custom system prompt

claude -p "Review auth.py" --system-prompt "You are a security expert. Focus only on security vulnerabilities."

--append-system-prompt: add to the existing system prompt

claude -p "Review auth.py" --append-system-prompt "Focus on SQL injection and XSS."

The difference from --system-prompt: --append-system-prompt keeps Claude Code's default system prompt (including CLAUDE.md) and adds your text. --system-prompt replaces it entirely.

--allowedTools: restrict the tools

claude -p "Analyze this code" --allowedTools Read Grep Glob

Limits which tools Claude Code can use. For a review, it only needs to read.

Combining flags

claude -p "Review the latest changes for security issues" \
  --model opus \
  --max-turns 5 \
  --output-format json \
  --allowedTools Read Grep Glob \
  --append-system-prompt "Focus on OWASP Top 10 vulnerabilities"

Piping: Claude Code in Unix pipelines

Headless mode follows the Unix philosophy: it can take input via stdin and send output to stdout. That makes it composable with other tools.

Input pipe

cat src/auth.py | claude -p "Review this code for security issues"
git diff | claude -p "Explain these changes"
git log --oneline -10 | claude -p "Summarize the recent development activity"

Output pipe

claude -p "Generate a .gitignore for a Python FastAPI project" > .gitignore
claude -p "List all TODO comments in the codebase" | grep "HIGH"

Full composition

git diff --staged | claude -p "Write a conventional commit message for these changes" | git commit -F -

This one-liner:

  1. Grabs the staged changes in Git
  2. Pipes them to Claude Code to generate a commit message
  3. Uses that message to make the commit

Another composition example

find src -name "*.py" -newer last_review.txt | \
  while read f; do
    echo "=== $f ===" >> review.md
    claude -p "Brief review of $f: bugs, issues, score 1-10" >> review.md
  done

Practical scripts

Script 1: Automated code review on a git diff

#!/bin/bash
# review-changes.sh - Review uncommitted changes

DIFF=$(git diff)

if [ -z "$DIFF" ]; then
    echo "No changes to review."
    exit 0
fi

echo "Reviewing changes..."
echo "$DIFF" | claude -p "Review this git diff. For each file changed:
1. What changed and why (your best guess)
2. Any bugs or issues
3. Suggestions

Format as markdown." --output-format text > review-output.md

echo "Review saved to review-output.md"
cat review-output.md

Script 2: PR description generator

#!/bin/bash
# pr-description.sh - Generate PR description from branch commits

BASE_BRANCH=${1:-main}
BRANCH=$(git branch --show-current)

echo "Generating PR description for $BRANCH (base: $BASE_BRANCH)..."

COMMITS=$(git log $BASE_BRANCH..$BRANCH --oneline)
DIFF=$(git diff $BASE_BRANCH...$BRANCH --stat)
FULL_DIFF=$(git diff $BASE_BRANCH...$BRANCH)

claude -p "Generate a GitHub PR description based on these commits and changes.

Branch: $BRANCH
Base: $BASE_BRANCH

Commits:
$COMMITS

Files changed:
$DIFF

Full diff (first 5000 chars):
${FULL_DIFF:0:5000}

Format:
## Summary
[2-3 sentences]

## Changes
[bullet points]

## Testing
[how to test]

## Notes
[any caveats or follow-ups]" --output-format text

Usage:

chmod +x pr-description.sh
./pr-description.sh main

Script 3: Test generator for new functions

#!/bin/bash
# generate-tests.sh - Generate tests for new/modified functions

FILES=$(git diff --name-only --diff-filter=AM | grep "\.py$")

if [ -z "$FILES" ]; then
    echo "No new or modified Python files."
    exit 0
fi

for FILE in $FILES; do
    TEST_FILE="tests/test_$(basename $FILE)"
    echo "Generating tests for $FILE..."

    claude -p "Read $FILE and generate pytest tests for any new or modified 
    functions. Include edge cases and error handling. Output ONLY the test 
    code, no explanations." \
    --allowedTools Read Grep \
    --max-turns 3 \
    --output-format text > "$TEST_FILE"

    echo "  Created: $TEST_FILE"
done

Script 4: Daily changelog of changes

#!/bin/bash
# daily-changelog.sh - Generate daily changelog

YESTERDAY=$(date -v-1d +%Y-%m-%d 2>/dev/null || date -d "yesterday" +%Y-%m-%d)
TODAY=$(date +%Y-%m-%d)

COMMITS=$(git log --after="$YESTERDAY" --before="$TODAY 23:59:59" --oneline)

if [ -z "$COMMITS" ]; then
    echo "No commits since yesterday."
    exit 0
fi

claude -p "Generate a changelog entry for today ($TODAY) based on these commits:

$COMMITS

Format:
## $TODAY

### Added
- [new features]

### Changed  
- [modifications]

### Fixed
- [bug fixes]

Skip sections with no entries." --output-format text >> CHANGELOG.md

echo "Changelog updated."

Script 5: Pre-commit hook with Claude Code

#!/bin/bash
# .git/hooks/pre-commit - Claude Code pre-commit review

STAGED=$(git diff --staged --name-only)

if [ -z "$STAGED" ]; then
    exit 0
fi

DIFF=$(git diff --staged)

RESULT=$(echo "$DIFF" | claude -p "Quick review of these staged changes. 
If there are critical bugs or security issues, respond with BLOCK and explain why. 
If changes look OK, respond with PASS.
Only respond BLOCK for actual bugs, not style issues." \
--max-turns 2 --output-format text)

if echo "$RESULT" | grep -q "BLOCK"; then
    echo "❌ Pre-commit review found issues:"
    echo "$RESULT"
    echo ""
    echo "Fix the issues or use 'git commit --no-verify' to skip."
    exit 1
fi

echo "✅ Pre-commit review passed."
exit 0

Install it:

cp pre-commit.sh .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Comparisons and decisions

Headless CLI vs SDK: when to use which

AspectHeadless CLI (-p)SDK (Python/TS)
ComplexityOne bash lineA full script
SetupNone (you already have Claude Code)Install a library
Error handlingExit codesTry/catch, types
Streamingstream-jsonNative async iterators
ComposabilityUnix pipesProgrammatic functions
ConcurrencyManual (background jobs)asyncio.gather() / Promise.all()
Structured output--output-format jsonParsing in code
Ideal forSimple scripts, hooks, one-linersComplex pipelines, apps

The decision rule

Can you solve it with a bash pipe?
├── Yes → Headless CLI
└── No → Do you need conditional logic, loops, or error handling?
    ├── Yes → SDK
    └── No → Headless CLI with a bash script

Headless CLI vs interactive Claude Code

SituationInteractiveHeadless
Active development✅❌
Code review in CI/CD❌✅
Pre-commit hook❌✅
Exploring a new codebase✅❌
Generating docs automatically❌✅
Debugging✅❌
Daily cron job❌✅
Pair programming✅❌

Common patterns

Pattern: a reusable script template

#!/bin/bash
# claude-task.sh - Template for headless Claude Code tasks

set -euo pipefail

PROMPT="$1"
FORMAT="${2:-text}"
MAX_TURNS="${3:-5}"

if [ -z "$PROMPT" ]; then
    echo "Usage: ./claude-task.sh \"prompt\" [format] [max_turns]"
    exit 1
fi

claude -p "$PROMPT" \
    --output-format "$FORMAT" \
    --max-turns "$MAX_TURNS" \
    --allowedTools Read Grep Glob

Pattern: batch processing with xargs

find src -name "*.py" | \
    xargs -I {} -P 3 bash -c \
    'claude -p "Brief quality score (1-10) for {}: " --max-turns 2'

Processes files in parallel (3 at a time) with xargs -P.

Pattern: saving results with a timestamp

OUTPUT_DIR="reports/$(date +%Y-%m-%d)"
mkdir -p "$OUTPUT_DIR"

claude -p "Full project analysis" \
    --output-format json > "$OUTPUT_DIR/analysis.json"

Pattern: branching on the output

RESULT=$(claude -p "Are there any security vulnerabilities in src/auth.py? Answer YES or NO only." --max-turns 3)

if echo "$RESULT" | grep -qi "YES"; then
    echo "⚠️ Security issues detected!"
    claude -p "Detail the security vulnerabilities in src/auth.py" > security-report.md
    exit 1
fi

echo "✅ No security issues found."

Pitfalls and edge cases

Pitfall 1: long prompts on the command line

Bash has a limit on argument length. For long prompts, use a heredoc or a file:

claude -p "$(cat <<'EOF'
Analyze this project with the following criteria:
1. Code quality and readability
2. Test coverage
3. Security vulnerabilities
4. Performance bottlenecks
5. Documentation completeness

For each criterion, provide a score from 1-10 and specific examples.
EOF
)"

Or from a file:

claude -p "$(cat prompts/review-template.txt)"

Pitfall 2: special characters in prompts

Quotes, backticks, and $ inside the prompt can cause trouble:

# BAD: the $ gets interpreted as a variable
claude -p "Explain the $HOME variable in bash"

# GOOD: use single quotes
claude -p 'Explain the $HOME variable in bash'

Pitfall 3: timeouts in long scripts

Headless mode has no default timeout. A complex prompt can take minutes:

timeout 120 claude -p "Analyze this large codebase" --max-turns 3

Use timeout to cap the execution.

Pitfall 4: cost piling up in loops

A loop that runs Claude Code 100 times can get expensive:

# ⚠️ This runs Claude Code 100 times
for f in $(find src -name "*.py"); do
    claude -p "Review $f" >> reviews.md
done

Consider grouping files into a single prompt, or using the SDK with batch processing.

Pitfall 5: not using --allowedTools in automation

Without tool restrictions, Claude Code can run arbitrary commands in headless mode. Always limit the tools:

# ⚠️ Claude can run any command
claude -p "Fix the code" --no-permissions

# ✅ It can only read files
claude -p "Review the code" --allowedTools Read Grep Glob

Pitfall 6: mixed output with json

When you use --output-format json, the entire output is JSON. If your script expects plain text, it breaks:

# Extract only the result from the JSON response
claude -p "Hello" --output-format json | jq -r '.result'

Use jq to pull specific fields out of the JSON.


Complete worked example

A CI/CD pipeline that uses headless mode for several validations:

#!/bin/bash
# ci-claude-pipeline.sh - Claude Code CI pipeline

set -euo pipefail

echo "🔍 Claude Code CI Pipeline"
echo "=========================="

REPORT_DIR="ci-reports"
mkdir -p "$REPORT_DIR"
EXIT_CODE=0

# Step 1: Security review
echo ""
echo "Step 1/4: Security review..."
SECURITY=$(git diff main...HEAD | claude -p "Review this diff for security 
vulnerabilities. Respond with JSON: 
{\"critical\": 0, \"high\": 0, \"medium\": 0, \"issues\": [...]}" \
    --output-format json \
    --max-turns 3 \
    --allowedTools Read Grep Glob)

echo "$SECURITY" | jq -r '.result' > "$REPORT_DIR/security.json"

CRITICAL=$(echo "$SECURITY" | jq -r '.result' | jq -r '.critical // 0')
if [ "$CRITICAL" -gt 0 ] 2>/dev/null; then
    echo "  ❌ Critical security issues found!"
    EXIT_CODE=1
else
    echo "  ✅ No critical security issues"
fi

# Step 2: Code quality
echo ""
echo "Step 2/4: Code quality review..."
claude -p "Rate the code quality of the files changed in this branch 
(git diff main...HEAD --name-only). Score each file 1-10.
Format as markdown table: | File | Score | Key Issue |" \
    --max-turns 5 \
    --allowedTools Read Grep Glob \
    --output-format text > "$REPORT_DIR/quality.md"
echo "  ✅ Quality report generated"

# Step 3: Documentation check
echo ""
echo "Step 3/4: Documentation check..."
claude -p "Check if the changed files (git diff main...HEAD --name-only) 
have adequate documentation: docstrings, comments for complex logic, 
README updates if needed. Answer PASS or FAIL with details." \
    --max-turns 3 \
    --allowedTools Read Grep Glob \
    --output-format text > "$REPORT_DIR/docs.md"
echo "  ✅ Documentation check complete"

# Step 4: Generate PR description
echo ""
echo "Step 4/4: Generate PR description..."
COMMITS=$(git log main..HEAD --oneline)
DIFF_STAT=$(git diff main...HEAD --stat)

claude -p "Generate a PR description based on:
Commits: $COMMITS
Files changed: $DIFF_STAT
Format: ## Summary, ## Changes, ## Testing" \
    --max-turns 3 \
    --output-format text > "$REPORT_DIR/pr-description.md"
echo "  ✅ PR description generated"

# Summary
echo ""
echo "=========================="
echo "Reports saved to $REPORT_DIR/"
ls -la "$REPORT_DIR/"

exit $EXIT_CODE

Practice exercises

Exercise 1: Basic — Your first headless prompt

Run Claude Code in headless mode to get information about your project.

Requirements:

  • Use claude -p with a prompt that asks about the project structure
  • Try all three output formats (text, json, stream-json)
  • Redirect the json output to a file and pull the result out with jq
Solution
# Plain text
claude -p "What framework does this project use?"

# JSON
claude -p "What framework does this project use?" --output-format json > analysis.json
cat analysis.json | jq -r '.result'

# Stream JSON
claude -p "What framework does this project use?" --output-format stream-json

Exercise 2: Intermediate — A review script

Write a bash script that reviews the uncommitted changes and produces a report.

Requirements:

  • Read git diff for the current changes
  • Pipe the diff to Claude Code with -p
  • Produce a review.md file with the result
  • If there are no changes, print a message and exit with code 0
Solution
#!/bin/bash
# review.sh

DIFF=$(git diff)

if [ -z "$DIFF" ]; then
    echo "No changes to review."
    exit 0
fi

echo "$DIFF" | claude -p "Review these code changes. Format as markdown:
## Summary
## Issues Found (if any)
## Suggestions" \
    --max-turns 3 \
    --allowedTools Read Grep Glob \
    --output-format text > review.md

echo "Review saved to review.md"
chmod +x review.sh
./review.sh

Exercise 3: Intermediate — A commit message generator

Write a script that generates a commit message based on the staged changes.

Requirements:

  • Read git diff --staged
  • Generate a conventional commit message
  • Show the message and ask whether you want to use it
  • If you accept, run the commit
Solution
#!/bin/bash
# smart-commit.sh

DIFF=$(git diff --staged)

if [ -z "$DIFF" ]; then
    echo "No staged changes. Run 'git add' first."
    exit 1
fi

MESSAGE=$(echo "$DIFF" | claude -p "Generate a conventional commit message 
for these changes. Format: type(scope): description. 
Add a body if the changes are complex. Output ONLY the commit message." \
    --max-turns 2 \
    --output-format text)

echo "Proposed commit message:"
echo "---"
echo "$MESSAGE"
echo "---"
echo ""
read -p "Use this message? (y/n) " CONFIRM

if [ "$CONFIRM" = "y" ]; then
    git commit -m "$MESSAGE"
    echo "✅ Committed!"
else
    echo "Cancelled."
fi

Exercise 4: Advanced — A pre-commit hook

Implement a pre-commit hook that uses Claude Code to validate changes.

Requirements:

  • Installs at .git/hooks/pre-commit
  • Reviews staged changes for critical bugs
  • If it finds bugs, blocks the commit with a clear message
  • 60-second timeout
  • Uses read-only tools only
Solution
#!/bin/bash
# .git/hooks/pre-commit

DIFF=$(git diff --staged)

if [ -z "$DIFF" ]; then
    exit 0
fi

RESULT=$(timeout 60 bash -c "echo '$DIFF' | claude -p 'Quick security and bug check. 
If critical issues found, say BLOCK: [reason]. 
If OK, say PASS. Be concise.' \
    --max-turns 2 \
    --allowedTools Read Grep" 2>/dev/null)

if [ $? -ne 0 ]; then
    echo "⚠️ Claude Code review timed out. Proceeding with commit."
    exit 0
fi

if echo "$RESULT" | grep -q "BLOCK"; then
    echo "❌ Pre-commit review blocked the commit:"
    echo "$RESULT"
    echo ""
    echo "Use 'git commit --no-verify' to skip this check."
    exit 1
fi

echo "✅ Pre-commit check passed."
exit 0
cp pre-commit.sh .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Exercise 5: Challenge — A complete CI pipeline

Write a CI script that runs several validations with Claude Code.

Requirements:

  • Step 1: Security review of the diff
  • Step 2: Quality score for the changed files
  • Step 3: Check the documentation
  • Step 4: Generate a PR description
  • Save every report into one directory
  • Exit code 1 if there are critical security problems
Guide

Use this capsule's complete worked example as your base. Adapt the prompts to your project. Key points:

  1. set -euo pipefail for error handling
  2. --output-format json for the security review (parseable)
  3. --output-format text for human-readable reports
  4. --allowedTools Read Grep Glob in every step (read-only)
  5. --max-turns 3-5 to control cost
  6. jq to pull data out of the JSON

Check that the script works with bash -x ci-pipeline.sh to see each command as it runs.


Summary

What you learned in this capsule:

  • Headless mode (-p) runs Claude Code without interaction: one prompt, one response, process exits
  • Three output formats: text (default, readable), json (parseable, with metadata), stream-json (real-time streaming)
  • Complementary flags: --model, --max-turns, --no-permissions, --output-file, --system-prompt, --allowedTools
  • Unix piping: Claude Code can take input via stdin and send output to stdout, which makes it composable with pipes
  • Practical scripts: automated code review, PR descriptions, test generation, pre-commit hooks, daily changelogs
  • Headless vs SDK: headless for simple scripts and one-liners; the SDK for complex logic, error handling, and streaming
  • Pitfalls: long prompts, special characters, timeouts, cost piling up, unrestricted permissions

Next capsule: 05 - MCP and Remote Control — how to connect Claude Code to external data sources and drive it remotely.


Additional resources

Official documentation

Automation

  • Hooks — Hooks as a complement to headless mode
  • Settings — Configuring permissions for automation
  • Best Practices — Good practice for automated scripts

Complementary