Most people use Claude Code as an interactive REPL. But there's a second mode — headless, non-interactive — that turns it into a scriptable tool you can wire into bash pipelines, git hooks, cron jobs, and CI.
The same model. None of the conversation. Just input and output.
The -p Flag
-p (or --print) runs Claude in print mode. Prompt goes in, result comes to stdout, Claude exits.
# One-shot
claude -p "Generate a TypeScript interface for a blog post"
# Pipe a file
cat src/lib/auth.ts | claude -p "Review this for security issues. Be specific."
# Multiple files
cat src/lib/actions/user.ts src/types/user.ts | claude -p "Are these types consistent?"
Output Formats
# Plain text (no markdown)
claude -p "List 3 improvements" --output-format text < src/lib/payments.ts
# JSON — pipe into jq
claude -p 'Analyze and return JSON: { "issues": [{"severity","description","line"}] }' \
--output-format json < src/lib/auth.ts \
| jq '.issues[] | select(.severity == "high")'
Real Script: Auto-Changelog
#!/bin/bash
# scripts/generate-changelog.sh
SINCE=${1:-"1 week ago"}
COMMITS=$(git log --oneline --since="$SINCE")
[ -z "$COMMITS" ] && echo "No commits since $SINCE" && exit 0
CHANGELOG=$(claude -p "$(cat <<EOF
Generate a changelog entry in markdown for these git commits.
Group by: Features, Bug Fixes, Improvements, Other.
Use bullet points. Be concise and developer-friendly.
Commits:
$COMMITS
EOF
)" --output-format text)
echo "## $(date +%Y-%m-%d)" > /tmp/entry.md
echo "" >> /tmp/entry.md
echo "$CHANGELOG" >> /tmp/entry.md
echo "" >> /tmp/entry.md
cat /tmp/entry.md CHANGELOG.md > /tmp/full.md
mv /tmp/full.md CHANGELOG.md
echo "Changelog updated."
./scripts/generate-changelog.sh # since 1 week ago
./scripts/generate-changelog.sh "1 day ago"
Real Script: Commit Message Generator
#!/bin/bash
# Usage: git diff --staged | ./scripts/commit-msg-gen.sh
DIFF=$(cat)
[ -z "$DIFF" ] && echo "No staged changes." && exit 1
claude -p "$(cat <<EOF
Generate a conventional commit message for this diff.
Format: <type>(<scope>): <description>
Types: feat, fix, refactor, docs, test, chore, perf
Keep under 72 characters. Output the message only.
Diff:
$DIFF
EOF
)" --output-format text
# Preview
git diff --staged | ./scripts/commit-msg-gen.sh
# Use directly
git commit -m "$(git diff --staged | ./scripts/commit-msg-gen.sh)"
Real Script: Batch PR Review
#!/bin/bash
# scripts/pr-review.sh
BASE=${1:-"main"}
FILES=$(git diff --name-only "$BASE"...HEAD)
echo "# PR Review Report — $(date)" > pr-review.md
for file in $FILES; do
case "$file" in *.md|*.json|*.lock|*.svg) continue ;; esac
[ ! -f "$file" ] && continue
echo "Reviewing $file..."
REVIEW=$(cat "$file" | claude -p "$(cat <<EOF
Review for: bugs, security issues, missing error handling, performance.
Be specific with line numbers. Say "Looks good." if nothing to flag.
File: $file
EOF
)" --output-format text)
echo "## $file" >> pr-review.md
echo "$REVIEW" >> pr-review.md
echo "" >> pr-review.md
done
echo "Done → pr-review.md"
Git Hook: Auto-suggest Commit Messages
# .git/hooks/prepare-commit-msg
#!/bin/bash
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
if [ "$COMMIT_SOURCE" = "" ]; then
STAGED=$(git diff --staged)
if [ -n "$STAGED" ]; then
SUGGESTION=$(echo "$STAGED" | claude -p \
"Write a conventional commit message. One line, under 72 chars." \
--output-format text 2>/dev/null)
if [ -n "$SUGGESTION" ]; then
echo "# Suggested: $SUGGESTION" > /tmp/msg
cat "$COMMIT_MSG_FILE" >> /tmp/msg
mv /tmp/msg "$COMMIT_MSG_FILE"
fi
fi
fi
chmod +x .git/hooks/prepare-commit-msg
In CI/CD
# .github/workflows/claude-review.yml
- name: Review changed files
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
FILES=$(git diff --name-only origin/main...HEAD)
for file in $FILES; do
case "$file" in
*.ts|*.tsx|*.js|*.jsx)
cat "$file" | claude -p "Flag security issues or bugs. Be specific." \
--output-format text \
--dangerously-skip-permissions \
>> review.txt
;;
esac
done
cat review.txt
Error Handling
RESULT=$(cat src/main.ts | claude -p "Analyze" --output-format text)
[ $? -ne 0 ] && echo "Claude failed" >&2 && exit 1
echo "$RESULT"
When to Use Headless vs Interactive
Use -p for: well-defined tasks, batch file processing, CI pipelines, automation with known input/output.
Use interactive for: exploratory work, refactoring sessions that need back-and-forth, anything where you'd naturally say "yes, do that" or "try this instead."
Don't force complex context-heavy tasks into a single -p call. The interactive session can read files, adjust course, and ask clarifying questions in ways one-shot prompts can't.
Full guide at stacknotice.com/blog/claude-code-headless-scripting-2026
Top comments (0)