Headless Claude Code: Agent SDK Automation Patterns
The Claude Agent SDK is the programmable core of Claude Code: the same agent loop that runs in your terminal, exposed as a library so you can run it headless, with no UI and no human in the loop. If you want Claude Code to review pull requests in CI, triage issues on a schedule, or refactor a hundred files overnight, this is the layer you build on. This guide covers both headless entry points, the claude -p print mode and the Claude Agent SDK for TypeScript and Python, then works through the automation patterns that actually survive contact with production.
A quick calibration for 2026: the SDK was renamed from "Claude Code SDK" to "Claude Agent SDK" in late 2025 when it outgrew coding-only use, the workhorse models for automation are the Sonnet tier (claude-sonnet-4-6 and claude-sonnet-5) with claude-opus-4-8 reserved for the hardest jobs, and print mode is stable enough to sit inside CI pipelines that gate real deployments.
What headless Claude Code actually is
Claude Code has two faces. The interactive CLI is what most engineers know: a terminal session where you type, watch the agent work, and approve actions as they come up. Headless mode strips all of that away. The agent still gets the same tools (Read, Write, Edit, Bash, Grep, Glob, WebFetch, plus any MCP servers you attach), still runs the same loop of reading files, executing commands, and deciding what to do next. The difference is that it runs to completion on its own and hands you a machine-readable result instead of a conversation.
There are two ways to run it headless:
claude -p(print mode): the CLI runs one prompt to completion and exits. Zero extra dependencies, perfect for shell scripts and CI steps.- The Claude Agent SDK: TypeScript and Python libraries that expose the same engine as an async message stream, plus programmatic control over permissions, tools, hooks, subagents, and sessions.
A useful rule of thumb: start with claude -p and jq. Move to the SDK the moment you need custom tools, permission callbacks, or multi-turn sessions, or when your bash one-liner grows its second if-statement.
The 60-second version: claude -p
Print mode is the fastest way to prove the concept. Set an API key, pick an allowlist of tools, and capture JSON:
export ANTHROPIC_API_KEY=sk-ant-...
claude -p "Find every TODO older than six months in this repo and list file, line, and a one-line summary" \
--output-format json \
--allowedTools "Read,Grep,Glob,Bash(git log:*)" \
--max-turns 25 > result.json
jq -r '.result' result.json
jq -r '.total_cost_usd' result.jsonThe --output-format flag has three settings. text prints only the final answer, which is fine for humans reading logs. json wraps the whole run in a single object with result, total_cost_usd, num_turns, duration_ms, session_id, and is_error, which is what your pipeline should parse. stream-json emits one JSON message per line as the agent works, so every tool call becomes a log line you can ship to your observability stack.
--allowedTools is the safety story in CLI form. The agent gets exactly the listed tools without prompting, and anything else it asks for is denied, because there is no human present to approve it. The Bash(git log:*) syntax scopes shell access to a command prefix; a bare Bash would allow arbitrary commands, which you almost never want in automation.
Two more flags worth memorizing:
--append-system-prompt "..."adds your automation-specific instructions on top of Claude Code's tuned system prompt instead of replacing it.--permission-mode acceptEditsauto-approves file edits while other permission rules keep applying, which is the usual setting for jobs that are supposed to change code.
Installing the Claude Agent SDK
Both SDKs drive the same Claude Code engine under the hood:
npm install @anthropic-ai/claude-agent-sdk # TypeScript, Node 18+
pip install claude-agent-sdk # Python 3.10+Current releases bundle the agent runtime. If you are pinned to an older version and hit a CLINotFoundError, install the CLI next to it with npm install -g @anthropic-ai/claude-code. Authentication for unattended runs is a plain ANTHROPIC_API_KEY environment variable, and the same provider switches the CLI supports (CLAUDE_CODE_USE_BEDROCK=1, CLAUDE_CODE_USE_VERTEX=1) work headless if your tokens live with a cloud provider.
One migration gotcha bites almost everyone coming from the old Claude Code SDK: the Claude Agent SDK does not load your filesystem configuration by default. CLAUDE.md files, settings.json, and custom slash commands are opt-in through settingSources, and Claude Code's own system prompt is opt-in through a preset. Empty defaults are actually what you want for automation, because a stray CLAUDE.md edit can no longer silently change CI behavior. But if your workflow depends on project context, turn it back on explicitly:
const options = {
systemPrompt: { type: "preset", preset: "claude_code" },
settingSources: ["project"],
};Pin your SDK version in package.json or requirements files like you would any other dependency that talks to production. Reproducibility is the whole point of running headless.
The core loop: query() and the message stream
The single most important function in the SDK is query(). It spawns the agent, streams messages back as they happen, and finishes with a result message containing everything your pipeline needs.
TypeScript:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Fix the flaky test in tests/checkout.spec.ts and explain the root cause",
options: {
model: "claude-sonnet-4-6",
cwd: "/srv/repos/shop",
allowedTools: ["Read", "Grep", "Glob", "Edit", "Bash(npm test:*)"],
permissionMode: "acceptEdits",
maxTurns: 40,
},
})) {
if (message.type === "result") {
if (message.subtype === "success") {
console.log(message.result);
} else {
console.error(`Run failed: ${message.subtype}`);
process.exitCode = 1;
}
console.log(`turns=${message.num_turns} cost_usd=${message.total_cost_usd}`);
}
}Python mirrors the same API with snake_case options:
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def main() -> None:
options = ClaudeAgentOptions(
model="claude-sonnet-4-6",
cwd="/srv/repos/shop",
allowed_tools=["Read", "Grep", "Glob", "Edit", "Bash(npm test:*)"],
permission_mode="acceptEdits",
max_turns=40,
)
async for message in query(
prompt="Fix the flaky test in tests/checkout.spec.ts",
options=options,
):
if isinstance(message, ResultMessage):
print(message.result)
print(f"cost_usd={message.total_cost_usd}")
asyncio.run(main())Three message types matter in practice. An init system message arrives first and carries the session_id. Assistant messages stream in as the agent thinks and calls tools, useful when you want to log intermediate steps. Exactly one result message arrives last, with result, num_turns, duration_ms, total_cost_usd, and a subtype of success, error_max_turns, or error_during_execution. Treat error_max_turns as a legitimate outcome rather than an exception: the agent ran out of its turn budget, and the right response, decided by your code, is usually a retry with a tighter prompt or a bigger budget.
Claude Agent SDK vs the raw Messages API
Engineers who already call the Claude API sometimes ask why they should carry the extra dependency. The distinction is who owns the loop.
The Messages API gives you one model call. You define tools, you execute them, you feed results back, you manage the context window, you decide when the job is done. That is exactly right for classification, extraction, summarization, and any workflow where your code orchestrates the steps.
The Claude Agent SDK gives you the loop Anthropic already tuned for Claude Code: tool execution with permission gating, automatic context compaction when the transcript gets long, prompt caching that just works because the harness keeps prefixes stable, file system awareness, MCP integration, subagents, and session persistence. When the job is "operate on this repository until the tests pass," reimplementing that harness on the raw API is months of work you do not need to spend.
The decision table is short:
- Single-shot text or structured extraction: call the Messages API directly.
- Multi-step work against a filesystem, a repo, or a shell: use the Claude Agent SDK.
- You want Anthropic to host the sandbox and the loop as a service: look at Anthropic's Managed Agents API, which is the hosted sibling of the self-hosted pattern this article covers.
Permissions: the part that decides whether automation is safe
Headless means nobody is watching, so the permission design is not a detail. You get four layers, and serious deployments use all of them.
Layer 1: tool allowlists. allowedTools and disallowedTools define the hard boundary. Grant read tools freely, grant Edit when the job changes code, and scope Bash to command prefixes like Bash(git diff:*) or Bash(npm test:*).
Layer 2: permission mode. default asks for approval it can never get in headless runs, so unlisted tools fail closed. acceptEdits auto-approves file changes. plan lets the agent read and plan but not act, which is a great dry-run mode. bypassPermissions approves everything and belongs only inside a disposable container that holds nothing you would miss.
Layer 3: programmatic approval. The canUseTool callback runs on every tool call that would otherwise need a human, and your code decides:
import { query } from "@anthropic-ai/claude-agent-sdk";
const run = query({
prompt: "Remove unused dependencies and update the lockfile",
options: {
allowedTools: ["Read", "Grep", "Glob", "Edit"],
canUseTool: async (toolName, input) => {
if (toolName === "Bash") {
const cmd = String(input.command ?? "");
const banned = ["rm -rf", "git push --force", "npm publish"];
if (banned.some((b) => cmd.includes(b))) {
return { behavior: "deny", message: `Blocked by policy: ${cmd}` };
}
}
return { behavior: "allow", updatedInput: input };
},
},
});If you are on an early SDK version and the callback never fires, switch the prompt to streaming input mode; the first releases only supported canUseTool there.
Layer 4: the environment itself. Run agents in throwaway containers, mount only the repo they need, inject only the credentials the job requires, and make them work on branches. Git is your undo button and the pull request is your human checkpoint. Hooks (PreToolUse and PostToolUse) round this out when you need an audit log of every command with the ability to veto by pattern, and they are configurable both in settings and directly in SDK options.
Pattern 1: a CI code reviewer in GitHub Actions
The highest-value first deployment for most teams. The official claude-code-action on the GitHub Marketplace is the batteries-included route; the hand-rolled version below shows the mechanics and is easier to bend to house rules:
name: ai-review
on:
pull_request:
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: npm install -g @anthropic-ai/claude-code
- name: Review the diff
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude -p "Review the diff between origin/main and HEAD. Report every bug, security issue, and missing test you find, with file and line references. Output markdown." \
--output-format json \
--allowedTools "Read,Grep,Glob,Bash(git diff:*),Bash(git log:*)" \
--max-turns 30 > review.json
jq -r '.result' review.json > review.md
- name: Post the review
env:
GH_TOKEN: ${{ github.token }}
run: gh pr comment ${{ github.event.pull_request.number }} --body-file review.mdThree details make this production-grade rather than a demo. First, the tool list is read-only: no Edit, no Write, and Bash scoped to git inspection, so the reviewer cannot "helpfully" fix things on the PR branch. Second, fetch-depth: 0 gives the agent real history to diff against. Third, one instruction in the prompt tells it to report everything and let humans filter; review agents given "only report serious issues" tend to investigate thoroughly and then say nothing.
Pattern 2: scheduled maintenance agents
Cron plus print mode covers a surprising amount of toil: nightly dependency audits, log triage, docs drift detection, test flake hunting. The shape that works is judgment inside the agent, control flow outside it:
#!/usr/bin/env bash
set -euo pipefail
cd /srv/repos/api
git fetch origin
git checkout -B chore/nightly-deps origin/main
claude -p "Run npm audit. Upgrade only patch-level vulnerable dependencies, run the test suite, and summarize exactly what changed and why." \
--output-format json \
--allowedTools "Read,Edit,Bash(npm:*),Bash(git diff:*)" \
--permission-mode acceptEdits \
--max-turns 50 > /tmp/deps.json
if [ "$(jq -r '.is_error' /tmp/deps.json)" = "false" ]; then
git add -A && git commit -m "chore: nightly dependency patch bumps"
git push -u origin chore/nightly-deps --force-with-lease
gh pr create --title "Nightly dependency patches" \
--body "$(jq -r '.result' /tmp/deps.json)"
fiThe agent never pushes and never opens the PR. Deterministic steps stay in bash where they are testable, and the destructive-adjacent operations happen on a branch that a human merges. If the run fails, nothing external changes and tomorrow's cron gets another shot. Log /tmp/deps.json somewhere durable; the day the agent does something surprising, the stream-json variant of this job is what tells you exactly which command did it.
Pattern 3: custom tools with an in-process MCP server
Real automation eventually needs your internal systems: deploy status, feature flags, ticket queues, customer metadata. Instead of running a separate MCP server process, the Claude Agent SDK lets you define tools in the same process as your script:
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const deployStatus = tool(
"get_deploy_status",
"Get the current deployment status for a service",
{ service: z.string().describe("Service name, e.g. checkout-api") },
async (args) => ({
content: [{ type: "text", text: await fetchStatusFromInternalApi(args.service) }],
})
);
const ops = createSdkMcpServer({
name: "ops",
version: "1.0.0",
tools: [deployStatus],
});
for await (const message of query({
prompt: "Why did the last checkout-api deploy fail? Check status, then read the relevant runbook in docs/.",
options: {
mcpServers: { ops },
allowedTools: ["Read", "Grep", "mcp__ops__get_deploy_status"],
},
})) {
// handle messages
}Two things trip people up. Custom tool names are namespaced as mcp__<server>__<tool>, and they must appear in allowedTools like any other tool, or the agent can see them but never call them. And because the handler runs in your process with your credentials, the security model from the permissions section applies double: the agent decides when to call the tool, so validate inputs inside the handler as if they came from an untrusted client. Python has the same helpers with a @tool decorator and create_sdk_mcp_server.
This pattern is also the cleanest way to get structured output from a run. Define a report_result tool with a strict schema, instruct the agent to finish by calling it, and read the typed input from the tool call instead of parsing prose.
Pattern 4: subagents for fan-out work
For jobs shaped like "do the same analysis across many targets," define subagents programmatically and let the orchestrator delegate. Each subagent gets its own context window, so the main transcript stays small while the work fans out:
const options = {
model: "claude-opus-4-8",
agents: {
"repo-auditor": {
description: "Audits a single repository for outdated CI patterns",
prompt: "You audit one repo at a time. Report findings as a terse checklist with file paths.",
tools: ["Read", "Grep", "Glob", "Bash(git log:*)"],
model: "sonnet",
},
},
allowedTools: ["Read", "Grep", "Glob", "Task"],
};The orchestrator runs on claude-opus-4-8 and delegates each repository to a Sonnet-tier subagent, which is the standard cost shape: expensive judgment at the top, cheap parallel legwork below. Keep subagent tool lists minimal and their prompts specific; a subagent with one job and four tools is dramatically more reliable than a general-purpose one with twelve.
Sessions, resumption, and long-lived services
Every headless run has a session_id, delivered in the init message and again in the result. Store it. It unlocks three follow-up moves:
- Resume in the CLI:
claude -p --resume "$SESSION_ID" "Now apply the same fix to the staging config"continues with full context from the earlier run. - Resume in the SDK: pass
resume: sessionIdin options. AddforkSession: truewhen you want to branch several follow-ups off one investigation without them contaminating each other. - Stay resident: for a Slack bot or queue worker, keep one process alive and use streaming input (the
ClaudeSDKClientclass in Python) to push new turns into the same conversation instead of respawning the agent per message.
Always set maxTurns. It is the circuit breaker that turns an agent stuck in a loop into a clean error_max_turns result instead of an infinite bill.
Cost control and observability
Headless agents are easy to leave running and easy to forget, so treat cost as a first-class output:
- Read
total_cost_usdfrom every result message and emit it as a metric tagged by job name. Alert on outliers, because a job that normally costs a few cents and suddenly costs dollars is telling you its prompt or its repo changed. - Match the model to the job. Sonnet-tier models (claude-sonnet-4-6, claude-sonnet-5) handle the bulk of review, triage, and maintenance work; claude-opus-4-8 earns its price on gnarly migrations where fewer retries beat a cheaper rate. Subagents let you mix tiers within one run.
- Keep prompt prefixes stable. The harness handles prompt caching for you, but only if you avoid injecting timestamps or random IDs into system prompts. Put volatile context at the end of the user prompt.
- Ship
stream-jsonoutput to your log pipeline so every tool call is queryable later, and enable the built-in OpenTelemetry export (CLAUDE_CODE_ENABLE_TELEMETRY=1) if your team already lives in OTEL dashboards. - Use
maxTurnsand scoped tool lists not just for safety but for spend: an agent that cannot wander cannot rack up wandering costs.
FAQ
Is the Claude Agent SDK the same thing as the Claude Code SDK?
Yes. Anthropic renamed it in late 2025 when the same harness proved useful far beyond coding. The packages are @anthropic-ai/claude-agent-sdk on npm and claude-agent-sdk on PyPI. If you find imports from the old package name in a codebase, that is your cue to check for the settings-loading behavior change described above.
Do I need an API key or a Claude subscription?
For unattended automation, use an API key from the Claude Console (or route through Bedrock or Vertex with the corresponding environment switches). Subscription OAuth is designed for interactive use on a developer machine; claude setup-token can mint a long-lived token for CI where your plan permits it, but the API key is the standard path for servers.
Which model should headless jobs default to?
Default the fleet to a Sonnet-tier model and promote individual jobs to claude-opus-4-8 when their failure rate says so. Watching num_turns and retry counts per job gives you the data: a hard job on a cheaper model often costs more in retries than the bigger model would have cost in one pass.
How do I stop an agent from wrecking a repository?
Layer the controls: read-mostly tool allowlists, scoped Bash prefixes, canUseTool deny rules for the commands you fear, branches instead of main, and containers with nothing valuable in them. Reserve bypassPermissions and the CLI's --dangerously-skip-permissions for sandboxes you could delete without a second thought.
Can the agent run fully offline?
No. The model runs in the cloud, so an air-gapped box cannot run it at all. Bedrock and Vertex deployments keep traffic inside your cloud perimeter, which satisfies most network-control requirements in practice.
How do I get structured output instead of prose?
Two reliable options: instruct the agent to output only JSON and parse the result field, or define a custom report_result tool with a typed schema and require the agent to call it as its final act. The tool approach is stricter because the schema is enforced at the tool boundary rather than by hope.
When should I use Managed Agents instead of the Agent SDK?
Use the Claude Agent SDK when you want to own the compute: your containers, your network, your secrets management. Use Anthropic's Managed Agents API when you want sessions, sandboxes, and the loop hosted for you. The mental model transfers almost one-to-one, so starting with the SDK does not lock you out of moving later.
Where to start on Monday
Pick one job with a clear artifact and a human checkpoint: the CI reviewer is the usual winner. Ship it with claude -p, JSON output, a read-only tool allowlist, and maxTurns set. Once it has run for two weeks and you trust the shape of its output, graduate to the Claude Agent SDK for the pieces bash cannot express: a canUseTool policy, a custom tool for your internal API, a subagent fan-out. Keep the blast radius small, keep the human merge button in the loop, and let the cost metrics tell you when a job has earned a bigger model.
AI CodingShip full-stack AI apps at conversation speed — specs, agents, deploys, all from the terminal.
Claude CodeGo from zero to confident with Claude Code, the terminal agent that reads, edits, runs, and verifies real code.