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.
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.
Key insight
--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.
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
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
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
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
fiExit codes
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
--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.
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."
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)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.