Building Headless Agents
A headless agent runs the SDK with no human in the loop, so permissions, turn limits, and budgets must be set up front rather than answered interactively.
Why it matters
In CI, cron jobs, webhooks, and queue workers there is no terminal to approve a tool call — an agent that pauses on a permission prompt simply hangs the pipeline. Headless design is also where cost and safety bite hardest: an unbounded agent in a loop can burn dollars and touch production files unattended.
How it works
The shift from interactive to headless is mostly about replacing prompts with policy, and a TTY with structured I/O.
- Permissions become non-interactive. Use
permissionMode: 'acceptEdits'for write-capable jobs, or supply acanUseTool/can_use_toolcallback to decide programmatically (allow/deny with a reason).bypassPermissionsskips all checks — convenient and dangerous; see security-safe-tool-use. - Bound the run. Set
maxTurnsto cap agentic round-trips andmaxBudgetUsd/max_budget_usdto stop at a dollar ceiling; theresultmessage’ssubtypereportserror_max_turnsorerror_max_budget_usd. - Isolate from local config. Pass
settingSources: []so a developer’s~/.claudeor strayCLAUDE.mdcan’t leak into a deployed/multi-tenant agent. - Scope the toolset. Prefer an explicit
allowedToolsallow-list overbypassPermissions. Read-only jobs get["Read", "Glob", "Grep"]. - Observe outcomes by consuming the terminal message (
total_cost_usd,num_turns,is_error) and logging it. This parallels the CLI’s one-shot-print-mode-p and headless-ci-usage.
Example
A bounded, write-capable, isolated CI agent (TypeScript):
const q = query({
prompt: "Apply lint fixes and commit",
options: {
permissionMode: "acceptEdits",
allowedTools: ["Read", "Edit", "Bash"],
settingSources: [], // ignore local ~/.claude config
maxTurns: 12,
maxBudgetUsd: 0.50,
}
});
for await (const m of q)
if (m.type === "result")
process.exit(m.subtype === "success" ? 0 : 1);Pitfalls
- Default mode blocks silently. With
permissionMode: 'default'a write tool waits for approval that never comes; the job appears to hang. Set an explicit mode. - No turn/budget cap = runaway. A confused agent can loop until it exhausts the context or your balance. Always set
maxTurnsand a budget for unattended runs. bypassPermissionsin CI is a liability. Combined withBash, a prompt-injected instruction can run arbitrary commands. Allow-list specific tools instead.- Leaking local settings. Forgetting
settingSources: []means CI behavior depends on whatever happens to be in the runner’s home dir — non-reproducible and a footgun.