deployment

Headless Mode: Claude Code Without a Terminal

Run Claude Code as a service. Cron jobs, webhooks, async tasks. The patterns that turn Claude into infrastructure.

11 min read·

Claude Code · Hidden Feature #003 · Infrastructure Pattern

Claude Code without a human.

The -p flag turns Claude Code into a Unix tool you can pipe, chain, and schedule. No terminal interaction. Structured JSON output. Runs inside CI/CD without a TTY. This is how Claude becomes infrastructure.

claude -p

the flag

JSON output

schema-constrained

CI/CD ready

no TTY required

Core flags

Everything that makes headless mode powerful

Pass these flags to claude -p to control output format, tool access, and session continuity.

Flag
What it does
-p "prompt"
Non-interactive: runs the prompt, prints response, exits. Pipe-friendly.
--output-format json
Returns structured JSON with session metadata, token count, and cost.
--output-format stream-json
Streams newline-delimited JSON for real-time processing with jq.
--json-schema '{…}'
Enforces an exact JSON schema on the response. Claude must match it.
--continue
Resume the last conversation. Keeps full context.
--resume <id>
Resume a specific previous conversation by session ID.
--allowedTools "X,Y"
Auto-approve specific tools without prompting. E.g. "Read,Edit,Bash(git *)"
--append-system-prompt
Add extra instructions on top of CLAUDE.md for this specific run.
--add-dir ../shared
Give Claude access to a directory outside your current project.

Key insight

Combine --output-format json with --json-schema to guarantee Claude's response fits an exact schema. No prompt engineering needed.

Pipeline patterns

Real commands you can copy-paste today

Five patterns that cover 90% of headless use cases. Click a tab to see the command.

Monitor logs in real time

Pipe your log tail directly into Claude. It reads the stream, detects anomalies, and messages you on Slack via MCP.

# Stream logs → Claude watches → Slacks you if anomaly detected
tail -f app.log | claude -p "Slack me immediately if you see any 5xx errors,
OOM warnings, or database connection failures.
Otherwise stay silent." \
  --allowedTools "mcp__slack__conversations_add_message"

Deployment patterns

4 ways to run Claude as infrastructure

From simple cron jobs to horizontally-scaled queue workers.

Pattern 01

Cron job

Schedule Claude to run at a fixed cadence. No human needs to be present. Works with crontab, launchd, or any job scheduler.

# crontab - run every morning at 7am
0 7 * * * cd /your/project && \
  claude -p "Summarize last 24h of git activity.
Post a bullet-point digest to #dev-updates on Slack." \
  --allowedTools "Bash(git *),mcp__slack__conversations_add_message" \
  >> /var/log/claude-cron.log 2>&1
  • Always redirect stdout and stderr to a log file
  • Use --allowedTools to lock down what Claude can touch
  • Test with CLAUDE_SKIP_AUTH=1 locally before scheduling
Pattern 02

Webhook handler

Trigger Claude via HTTP webhook: Stripe events, GitHub push hooks, Zapier, or any POST from an external service.

// pages/api/webhooks/github.ts
import { exec } from "child_process"
import { promisify } from "util"
const execAsync = promisify(exec)

export async function POST(req: Request) {
  const body = await req.json()
  if (body.action !== "opened") return Response.json({ ok: true })

  const pr = body.pull_request
  const { stdout } = await execAsync(
    `claude -p "Review PR #${pr.number}: ${pr.title}.
Check for: missing tests, N+1 queries, auth gaps.
Output JSON: {summary, issues[], verdict}" \
      --output-format json \
      --allowedTools "Read,Bash(git *)"`
  )
  const result = JSON.parse(stdout)
  // post result.summary back to the PR via GitHub API
  return Response.json(result)
}
  • Validate webhook signatures before invoking Claude
  • Wrap exec in try/catch; Claude can exit non-zero on errors
  • Keep prompts short; pipe heavy context via stdin instead
Pattern 03

Queue worker

Pull jobs from a queue (Redis, SQS, BullMQ) and run Claude per job. Scales horizontally. Handles retries automatically.

// worker.ts - BullMQ example
import { Worker } from "bullmq"
import { execSync } from "child_process"

const worker = new Worker("ai-tasks", async (job) => {
  const { prompt, tools, outputFormat } = job.data

  const flags = [
    `-p "${prompt.replace(/"/g, '\\"')}"`,
    outputFormat ? `--output-format ${outputFormat}` : "",
    tools ? `--allowedTools "${tools}"` : "",
  ].filter(Boolean).join(" ")

  const result = execSync(`claude ${flags}`, {
    cwd: job.data.projectPath,
    timeout: 120_000,
  }).toString()

  return outputFormat === "json" ? JSON.parse(result) : result
})

worker.on("failed", (job, err) => {
  console.error(`Job ${job?.id} failed:`, err.message)
})
  • Set timeout; Claude on large repos can take 60-120s
  • Use job.data.projectPath to scope Claude to the right repo
  • Store structured JSON output directly in the job result
Pattern 04

Scheduled task (launchd / systemd)

Run Claude as a persistent background service or scheduled OS-level task. More reliable than cron for mission-critical jobs.

# launchd plist - runs every hour on macOS
# ~/Library/LaunchAgents/com.ayautomate.claude-monitor.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>com.ayautomate.claude-monitor</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/sh</string>
    <string>-c</string>
    <string>claude -p "Check /var/log/app.log for errors in
the last hour. Post summary to Slack if any found." \
  --allowedTools "Bash(tail *),mcp__slack__conversations_add_message"
    </string>
  </array>
  <key>StartInterval</key><integer>3600</integer>
  <key>StandardOutPath</key><string>/tmp/claude-monitor.log</string>
  <key>StandardErrorPath</key><string>/tmp/claude-monitor.err</string>
</dict>
</plist>

# Load it
launchctl load ~/Library/LaunchAgents/com.ayautomate.claude-monitor.plist
  • launchd is more reliable than cron: it survives reboots and sleeps
  • Use StandardOutPath/ErrorPath to capture all output
  • Test with launchctl start before enabling the schedule

Output parsing

Reading what Claude returns

How to capture stdout, parse JSON, stream responses, and handle exit codes in shell scripts.

bash - capture, parse, stream, exit codes

# Default text output - capture stdout
result=$(claude -p "What's the current git branch?")
echo "Branch: $result"

# JSON output - parse with jq
claude -p "List all TODO comments in the codebase" \
  --output-format json | jq '.result'

# Stream JSON - process line by line
claude -p "Analyse each file in src/ for complexity" \
  --output-format stream-json | while read line; do
  echo "$line" | jq -r '.content // empty'
done

# Capture exit code - Claude exits 1 on critical issues
if ! claude -p "Run tests and verify all pass" \
     --allowedTools "Bash(pnpm test)"; then
  echo "Claude flagged a critical issue - blocking deploy"
  exit 1
fi

Exit codes

Claude exits 0 on success and 1 when it encounters a critical issue it was told to escalate. Use this in CI pipelines to block deploys automatically.

Error handling

Make headless runs production-safe

Claude can fail: rate limits, context overflow, tool errors. Wrap your calls defensively.

bash - retry wrapper with Slack alerting

#!/bin/bash
# Robust wrapper for headless Claude invocations

run_claude() {
  local prompt="$1"
  local tools="${2:-Read}"
  local max_retries=3
  local attempt=0

  while [ $attempt -lt $max_retries ]; do
    set +e
    output=$(claude -p "$prompt" --allowedTools "$tools" 2>&1)
    exit_code=$?
    set -e

    if [ $exit_code -eq 0 ]; then
      echo "$output"
      return 0
    fi

    attempt=$((attempt + 1))
    echo "Attempt $attempt failed (exit $exit_code). Retrying in 5s..." >&2
    sleep 5
  done

  echo "ERROR: Claude failed after $max_retries attempts" >&2
  # Send alert - swap in your preferred channel
  claude -p "Send a Slack alert: headless task '$prompt' failed after retries." \
    --allowedTools "mcp__slack__conversations_add_message" || true
  return 1
}

Retry with backoff

Rate limits and transient failures need exponential back-off. Start at 5s, double each attempt.

Capture stderr

Claude writes tool errors to stderr. Redirect 2>&1 or you'll miss them in logs.

Set timeouts

Large repos can take 90-120s. Always set a timeout so stuck jobs don't block queues.

Alert on failure

Use Claude itself to send a Slack alert when a headless run fails. No extra tooling needed.

State persistence

Keeping context across separate runs

Claude's context window doesn't survive process boundaries. Use --continue, --resume, or manual state files.

bash - continue, resume, and manual state

# --continue resumes the LAST session - keeps full context
claude -p "Run the linter and fix any errors"    --allowedTools "Read,Edit,Bash"
claude -p "Now run tests and confirm they pass"  --allowedTools "Bash" --continue
claude -p "Commit the changes with a good message" --allowedTools "Bash(git *)" --continue

# --resume <id> targets a specific past session
SESSION_ID=$(claude -p "Start analysis of payments module" \\
  --output-format json | jq -r '.session_id')
# ... hours later ...
claude -p "Continue the payments module analysis" --resume "$SESSION_ID"

# Persist state yourself between separate jobs
claude -p "Analyse the auth module. Output JSON:
{findings: <array>, next_steps: <array>}" \\
  --output-format json > /tmp/claude-state.json

# Next job picks up where the last left off
CONTEXT=$(cat /tmp/claude-state.json)
claude -p "Given this prior analysis: $CONTEXT
Now check the payments module for the same issues."

Rule of thumb

Use --continue for sequential same-session steps. Use --resume <id> to re-enter a specific past session. Write to a JSON file when tasks run hours apart and need structured handoff.

Production examples

3 real workflows you can deploy today

Copy, adapt, and ship. Each example is running in production at agencies using Claude Code.

01

Nightly code health report

Runs at 2am every night. Scans the whole repo, builds a quality report, and posts it to Slack before the team starts work.

# cron: 0 2 * * *
claude -p "Audit the entire codebase for:
1. TODO/FIXME comments (list file + line)
2. Functions longer than 50 lines
3. Missing error handling in async functions
4. Hardcoded secrets or API keys

Output a Slack-formatted summary and post it to #engineering." \
  --allowedTools "Read,Grep,mcp__slack__conversations_add_message" \
  --append-system-prompt "Be concise. Max 20 bullet points."
02

PR auto-review bot

Triggers on every PR open via GitHub webhook. Posts a structured review comment within 60 seconds. Blocks merge on critical issues.

// Webhook handler (Next.js route handler)
const prompt = `
Review PR: "${pr.title}"
Files changed: ${changedFiles.join(", ")}

Check for:
- Breaking changes to public API
- Missing input validation
- N+1 database queries
- Exposed secrets

Output JSON: {
  verdict: "approve" | "request_changes" | "comment",
  summary: string,
  issues: [{file, line, severity, message}]
}`

const { stdout } = await execAsync(
  `claude -p '${prompt}' --output-format json --allowedTools "Read"`
)
const review = JSON.parse(stdout)
await postGitHubReview(pr.number, review)
03

i18n auto-translator pipeline

Triggered whenever a PR adds new English strings. Detects missing translations, produces accurate i18n output, and opens a follow-up PR.

# Runs in CI after merge to main
claude -p "Compare src/i18n/en.json with fr.json, de.json, es.json.
For each missing key:
1. Produce a natural, contextually accurate translation
2. Update the respective JSON file
3. Open a single PR titled 'i18n: auto-translate new strings'
   with all three files changed." \
  --allowedTools "Read,Edit,Bash(git *)" \
  --append-system-prompt "Preserve JSON key order. Do not alter existing translations."

Bottom line

claude -p is the bridge between an interactive coding assistant and a piece of infrastructure. Once you start thinking of Claude as a CLI tool, something you pipe into, schedule, and chain, the automation surface area becomes enormous. Cron jobs, webhook handlers, queue workers, CI gates: all of them become AI-native with one flag.

Want this running in your stack?

AY Automate builds AI automation systems for production teams.