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
| Format | Real-time progress | Parseable | Metadata | Use 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 --helporclaude -p --helpbefore 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--allowedToolsto 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:
- Grabs the staged changes in Git
- Pipes them to Claude Code to generate a commit message
- 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
| Aspect | Headless CLI (-p) | SDK (Python/TS) |
|---|---|---|
| Complexity | One bash line | A full script |
| Setup | None (you already have Claude Code) | Install a library |
| Error handling | Exit codes | Try/catch, types |
| Streaming | stream-json | Native async iterators |
| Composability | Unix pipes | Programmatic functions |
| Concurrency | Manual (background jobs) | asyncio.gather() / Promise.all() |
| Structured output | --output-format json | Parsing in code |
| Ideal for | Simple scripts, hooks, one-liners | Complex 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
| Situation | Interactive | Headless |
|---|---|---|
| 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 -pwith a prompt that asks about the project structure - Try all three output formats (
text,json,stream-json) - Redirect the
jsonoutput to a file and pull the result out withjq
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 difffor the current changes - Pipe the diff to Claude Code with
-p - Produce a
review.mdfile 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:
set -euo pipefailfor error handling--output-format jsonfor the security review (parseable)--output-format textfor human-readable reports--allowedTools Read Grep Globin every step (read-only)--max-turns 3-5to control costjqto 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
- Headless Mode — Full documentation for headless mode
- CLI Reference — Every CLI flag
- GitHub Actions — Headless mode in CI/CD
Automation
- Hooks — Hooks as a complement to headless mode
- Settings — Configuring permissions for automation
- Best Practices — Good practice for automated scripts
Complementary
- SDK Documentation — For when headless isn't enough
- jq Manual — Processing JSON on the command line