Headless mode and scripting

Claude Code · Advanced · 14 min

Non-interactive mode turns Claude Code into a building block for scripts, git hooks and CI jobs.

claude -p "prompt" (long form --print) runs one task without the interactive screen, prints the result and exits. Exit code 0 means success, so scripts can branch on it. It reads standard input, so you can pipe data in: cat build.log | claude -p "explain the root cause".

Output formats with --output-format:

  • text (default): just the answer.
  • json: one object with result, session_id, cost and usage; add --json-schema '<schema>' to get validated data in structured_output.
  • stream-json: one JSON event per line as work happens (use with --verbose).

In a script or a CI job (continuous integration: automated checks that run on every push) nobody is there to approve prompts, so decide permissions up front:

  • --allowedTools "Read,Edit,Bash(npm test *)" pre-approves exactly what the task needs,
  • --permission-mode dontAsk denies anything else that would have asked; good for locked-down CI,
  • --permission-mode acceptEdits allows file edits as a baseline.

For reproducible runs, add --bare: it skips auto-discovery of hooks, skills, plugins, MCP servers, auto memory and CLAUDE.md from the machine and repo, so a teammate's personal hook can't change the result. Bare mode needs an API key (ANTHROPIC_API_KEY), and you pass anything required explicitly (--settings, --mcp-config, --append-system-prompt).

Conversations continue with --continue or --resume <session_id>. --max-turns caps how many steps a run may take.

Important: without --bare, a -p run in an untrusted repository loads that repo's hooks and MCP servers without the usual trust prompt. Only run headless Claude in repositories you trust.

Example

# explain a failure from a log file
cat build-error.txt | claude -p "concisely explain the root cause" > why.txt

# structured output for a script
claude -p "List the HTTP routes in src/api" --output-format json \
  --json-schema '{"type":"object","properties":{"routes":{"type":"array","items":{"type":"string"}}},"required":["routes"]}' \
  | jq '.structured_output.routes'

# locked-down CI run: only these tools, everything else denied
claude --bare -p "Run the unit tests and fix failures in src/" \
  --allowedTools "Read,Edit,Bash(npm test *)" \
  --permission-mode dontAsk --max-turns 20

# continue a specific session
sid=$(claude -p "Start a review of src/auth" --output-format json | jq -r .session_id)
claude -p "Now list only the high-severity issues" --resume "$sid"

Remember

  • claude -p runs one task, reads stdin and exits with a status code
  • --output-format text|json|stream-json; --json-schema for validated data
  • Pre-approve with --allowedTools, deny the rest with dontAsk; --bare for reproducible runs

Watch out for

  • Running claude -p in CI without limiting tools or turns, so an unexpected path can run arbitrary commands for a long time.

Check yourself

  1. In a CI job you want Claude to edit files and run only `npm test`, refusing everything else without hanging. Which combination?
  2. What does --bare change?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: code.claude.com/docs/en/headless