The interactive terminal is where most people meet Claude Code, but it is not the only way to run it. The same engine that drives the chat loop also runs unattended: in a shell script, inside your own program, or as a step in a CI pipeline. There are three entry points worth knowing, and they sit on top of the same core. The headless CLI (claude -p) is the fastest way to script the tool. The Agent SDK embeds that same agent loop inside a TypeScript or Python program. The official GitHub Action wires it into pull requests and issues. Pick the one that matches where your automation lives, and the mental model you already have from interactive use carries over.
The thing to internalize before any of this is that running an agent unattended changes the stakes around two things: permissions and cost. Interactively, you are the safety check, where every risky tool call waits for you to approve it and you can watch the token meter. In CI nobody is watching, so the guardrails have to be configured in advance. The good news is that the same permission and cost controls exist across all three entry points, because they share one engine. Get them right once and the knowledge transfers.
Headless mode: claude -p
Headless mode is the interactive tool with the prompt loop replaced by a single shot. You pass a prompt, the agent works the task to completion using its tools, and it prints a result. The flag is -p (or --print):
claude -p "Summarize what changed in the last commit"
You can pipe input in, which is how most CI usage works because the interesting context is usually the output of another command:
git diff main...HEAD | claude -p "Review this diff and list any bugs as file:line followed by the issue"
For anything programmatic you want structured output rather than prose, which is what --output-format is for. The three values are text (the default, just the response), json (a single object with the result plus metadata), and stream-json (newline-delimited events as they happen, for live progress). The JSON object is the one worth knowing, because it carries the fields you need to act on a run: the result text, a session_id you can resume from, and the cost of the run in total_cost_usd. A typical scripted call pulls the result out with jq:
result=$(claude -p "Generate release notes from git log since the last tag" \
--output-format json)
echo "$result" | jq -r '.result'
echo "$result" | jq -r '.total_cost_usd'
Sessions persist on disk per project, so a multi-step pipeline can keep context across calls. --continue resumes the most recent session in the working directory, and --resume <session-id> resumes a specific one by the id you captured from the JSON output. This lets you split a long job into stages without the second stage losing what the first one learned:
sid=$(claude -p "Read the failing test and explain the root cause" \
--output-format json | jq -r '.session_id')
claude -p "Now write the fix" --resume "$sid"
Permissions in a pipeline
This is the part that bites people. In interactive use the tool asks before it runs a risky command. In -p mode there is nobody to ask, so an unconfigured run will refuse the actions it would normally prompt for, and the task stalls. You fix this by deciding in advance what the agent may do. The cleanest lever is --allowedTools, which takes the same permission-rule syntax as your settings file, so you can allow whole tools or scope a tool down to specific commands:
claude -p "Run the test suite and fix any failures" \
--allowedTools "Read,Edit,Bash(npm test),Bash(npm run *)" \
--max-turns 6
--disallowedTools is the inverse for carving exceptions out of a broad allow. --permission-mode sets the overall posture: acceptEdits auto-approves file edits while still gating other actions, and bypassPermissions approves everything. There is also --dangerously-skip-permissions, which is the blunt "approve everything" switch. The name is a warning, not a joke. It belongs only in a throwaway sandbox where the agent cannot touch anything you care about, never against a checkout with real credentials in the environment. The right default for CI is the narrowest --allowedTools list that lets the task finish, because least privilege is the whole game once no human is in the loop.
Auth and the other useful flags
Headless runs authenticate with the ANTHROPIC_API_KEY environment variable. If you route through a cloud provider instead, set CLAUDE_CODE_USE_BEDROCK=1 for Amazon Bedrock or CLAUDE_CODE_USE_VERTEX=1 for Google Vertex AI and supply that provider's normal credentials. A handful of other flags earn their place in CI: --max-turns caps the agentic loop so a confused run cannot spin forever and burn tokens; --model pins the model (for example claude-sonnet-4-6 for cheaper bulk work or claude-opus-4-8 for hard reasoning); --append-system-prompt adds a house instruction without replacing the default system prompt; --add-dir grants read access to a directory outside the working tree; and --verbose turns on the debug logging you will want the first time a workflow misbehaves.
The Agent SDK
When the automation is a program rather than a shell script, reach for the Agent SDK. It embeds the exact agent loop the CLI uses, so you get the same tools, the same permission model, and the same session handling, but driven from your own code with results you can branch on. One naming note that trips people up: this used to be called the "Claude Code SDK" and was renamed to the Claude Agent SDK. The packages are @anthropic-ai/claude-agent-sdk on npm and claude-agent-sdk on PyPI. If you find a tutorial referencing the old name, the concepts still apply; the package moved.
The core of both SDKs is a single query() function. You give it a prompt and an options object, and it returns an async stream of messages: the assistant's turns, the tool calls it makes, and a final result message carrying the cost. You iterate that stream the way the CLI prints turns. The TypeScript shape:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({
prompt: "Find and fix the failing test in this repo",
options: {
allowedTools: ["Read", "Edit", "Bash"],
permissionMode: "acceptEdits",
maxTurns: 8,
model: "claude-sonnet-4-6",
},
})) {
if (message.type === "result") {
console.log("done, cost was", message.total_cost_usd);
}
}
The same thing in Python reads almost identically, which is deliberate:
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
async for message in query(
prompt="Find and fix the failing test in this repo",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Bash"],
permission_mode="acceptEdits",
max_turns=8,
model="claude-sonnet-4-6",
),
):
print(message)
asyncio.run(main())
The options map onto the CLI flags you already know: allowedTools, permissionMode, maxTurns, model, a cwd for the working directory, mcpServers to connect MCP servers, and resume to pick up a prior session. Auth is the same ANTHROPIC_API_KEY environment variable. The decision between the SDK and the headless CLI comes down to where the logic lives: if your automation is mostly shell glue around one agent call, claude -p with jq is less ceremony; if you are fanning out over many inputs, branching on results, or embedding the agent inside a larger service, the SDK gives you real control flow instead of string parsing.
GitHub Actions
The most common place teams want Claude Code unattended is on their pull requests and issues, and that is what the official anthropics/claude-code-action covers. There are two ways it runs. In its default mode it watches for an @claude mention in an issue, a PR, or a comment, and responds to whatever you asked: implement the issue, fix the failing test, review the diff. In automation mode you give it a fixed prompt and a trigger of your choosing, which is how you build an unattended reviewer or a scheduled report.
The fastest setup path is to run /install-github-app from inside an interactive Claude Code session. It installs the GitHub App on your repo, adds the ANTHROPIC_API_KEY repository secret, and drops a starter workflow into .github/workflows/. If you would rather wire it by hand, install the app, add the secret under Settings then Secrets and variables then Actions, and add a workflow. A mention-triggered workflow looks like this:
name: Claude
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
claude:
if: contains(github.event.comment.body, '@claude')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: "--max-turns 10 --model claude-sonnet-4-6"
Notice that the CLI flags you learned for headless mode pass straight through the claude_args input, because the action is running the same binary underneath. Automation mode swaps the mention trigger for a fixed prompt. A workflow that reviews every pull request as it opens:
name: Claude review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: "Review this pull request for correctness bugs and security issues. Comment inline on specific lines."
claude_args: "--max-turns 8"
If your account does not call the Claude API directly, the action also runs against Amazon Bedrock or Google Vertex AI, and it supports OIDC-based authentication so you can avoid storing a long-lived key as a secret at all. Those setups are documented in the action's repo and the platform docs linked at the end. Whichever auth you pick, two costs are in play on every run: the GitHub Actions minutes the job consumes on the runner, and the Claude tokens the agent spends, which bill to whichever account owns the key or cloud credentials.
Cost and safety in CI
The discipline that keeps unattended runs from surprising you is the same three habits in every entry point. Scope the tools as tightly as the task allows, because an agent that can only read and run your test command cannot do much damage even if it goes off the rails. Cap the loop with --max-turns so a confused run ends instead of grinding, since each turn is real money. And capture total_cost_usd from the JSON output (or the result message in the SDK) and log it, so the cost of your automation is a number you watch rather than a line item you discover on the invoice. Treat --dangerously-skip-permissions and bypassPermissions as sandbox-only, reserved for environments where the agent has nothing valuable within reach. The interactive tool lets you be loose because you are the backstop. In CI the configuration is the backstop, so spend a few minutes making it the narrowest thing that still gets the job done.
