DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

Claude Code in Scripts: Headless Mode and Automation (2026)

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?"
Enter fullscreen mode Exit fullscreen mode

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")'
Enter fullscreen mode Exit fullscreen mode

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."
Enter fullscreen mode Exit fullscreen mode
./scripts/generate-changelog.sh              # since 1 week ago
./scripts/generate-changelog.sh "1 day ago"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# Preview
git diff --staged | ./scripts/commit-msg-gen.sh

# Use directly
git commit -m "$(git diff --staged | ./scripts/commit-msg-gen.sh)"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
chmod +x .git/hooks/prepare-commit-msg
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Error Handling

RESULT=$(cat src/main.ts | claude -p "Analyze" --output-format text)
[ $? -ne 0 ] && echo "Claude failed" >&2 && exit 1
echo "$RESULT"
Enter fullscreen mode Exit fullscreen mode

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)