How do I run Claude Code headless?
Run claude -p (headless mode) with the prompt, the tools it may use, and a permission mode that never waits for a person. It prints the result and exits 0 on success, non-zero on failure, so any scheduler can run it.
Cap every run. --max-turns and --max-budget-usd stop a run that loops or wanders before it becomes a surprise on the bill; the numbers in the examples are placeholders to size from your own attended runs.
The flags below come from Anthropic's headless and CLI reference docs. In -p mode with nobody attached, a permission prompt cannot be answered, so anything not allowed up front is denied. That is the behaviour you want in a batch job: unknown means no.
| Flag | What it does |
|---|---|
| -p "prompt" | Runs once without the interactive UI and exits |
| --allowedTools "Read,Edit,Bash(npm test *)" | Pre-approves exactly these tools and command patterns |
| --permission-mode dontAsk | Denies anything not pre-approved instead of asking |
| --permission-prompts none | Turns prompts off entirely for unattended runs (recent versions) |
| --max-turns 40 | Stops after that many agentic turns and exits with an error. No limit by default |
| --max-budget-usd 5 | Stops once API spend for the run reaches that amount |
| --output-format json | Machine-readable result, including total_cost_usd |
| --json-schema | Returns structured output that matches your JSON Schema |
| --bare | Skips CLAUDE.md, hooks, skills and MCP for a predictable script run |
| --mcp-config file.json | Loads MCP servers from a file |
| --resume <session_id> | Continues an earlier session |
claude -p "Read TRIAGE.md and follow it. Write the result to out/report.md." \
--permission-mode dontAsk \
--allowedTools "Read,Grep,Glob,Edit" \
--max-turns 40 \
--max-budget-usd 5 \
--output-format jsonsourcesClaude Code docs: headless modeClaude Code docs: CLI referenceClaude Code docs: permissions
Which credentials does an unattended run use?
An API key or a long-lived token, injected as a secret, never a login in the image. ANTHROPIC_API_KEY works everywhere. A subscriber can run claude setup-token to get a one-year OAuth token for CI, exposed as CLAUDE_CODE_OAUTH_TOKEN.
Two catches from Anthropic's authentication and headless docs. Bare mode needs ANTHROPIC_API_KEY or an apiKeyHelper and does not read CLAUDE_CODE_OAUTH_TOKEN. And cloud provider credentials take precedence over both, so check which one actually authenticates the run.
| Credential | Works with --bare | Notes |
|---|---|---|
| ANTHROPIC_API_KEY | Yes | Console API key, billed per token |
| apiKeyHelper | Yes | A script that prints a key, for rotation |
| CLAUDE_CODE_OAUTH_TOKEN | No | From claude setup-token, model requests only |
| Cloud provider (Bedrock, Vertex) | Provider-specific | Takes precedence over the others |
sourcesClaude Code docs: authenticationClaude Code docs: headless mode
What are the options for running Claude Code unattended?
Five common shapes, as of October 2026. They differ in where the run lives, who picks the work, and whether a person can sign off mid-way.
| Option | Where it runs | Picks the work | Sign-off mid-run |
|---|---|---|---|
| CI job (GitHub Actions, GitLab CI) | Your CI runners | The pipeline trigger | Only between jobs |
| Claude Code Routines (research preview) | Anthropic cloud or a self-hosted environment | Schedule (at most hourly), API call or GitHub event | No. Anthropic says the run continues without stopping for approval |
| Devcontainer on a VM | One container | You, by hand | In the live session only |
| Your own Kubernetes Job or CronJob | Your cluster | Your prompt or script | No, unless you build it |
| ConvOps isolated run | One pod per run on your Kubernetes | Pools and schedules | Yes, gates hold the work for a person |
sourcesClaude Code docs: RoutinesClaude Code docs: devcontainer
How do I run Claude Code as a Kubernetes Job?
Build a pinned image, store the key as a Secret, and run one Job per run with a locked-down pod. The manifests below are a working starting point; adjust the image, prompt and tools to your task.
- 1
Build the image
Pin the Claude Code version you tested and turn off auto-update. Run as the non-root node user.
FROM node:22-slim RUN apt-get update \ && apt-get install -y --no-install-recommends git ca-certificates jq \ && rm -rf /var/lib/apt/lists/* # Pin the version you tested. Never let a batch job auto-update. RUN npm install -g @anthropic-ai/claude-code@X.Y.Z ENV DISABLE_AUTOUPDATER=1 USER node WORKDIR /home/node/work - 2
Create the namespace and the secret
Use a dedicated namespace for agent runs. Prefer one key per purpose so you can revoke it alone.
kubectl create namespace agents kubectl -n agents create secret generic claude-api \ --from-literal=api-key="$ANTHROPIC_API_KEY" - 3
Write the Job
No service-account token, non-root, every capability dropped, no retries, a hard deadline and automatic cleanup. An init container that clones the repo into the work volume is left out for brevity.
apiVersion: batch/v1 kind: Job metadata: name: claude-triage namespace: agents spec: backoffLimit: 0 activeDeadlineSeconds: 3600 ttlSecondsAfterFinished: 86400 template: spec: restartPolicy: Never automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 1000 seccompProfile: { type: RuntimeDefault } containers: - name: claude image: registry.example.com/claude-runner:1.0.0 args: - claude - -p - "Read TRIAGE.md and follow it. Write the result to out/report.md." - --permission-mode - dontAsk - --allowedTools - "Read,Grep,Glob,Edit" - --max-turns - "40" - --max-budget-usd - "5" - --output-format - json env: - name: ANTHROPIC_API_KEY valueFrom: secretKeyRef: { name: claude-api, key: api-key } securityContext: allowPrivilegeEscalation: false capabilities: { drop: ["ALL"] } volumeMounts: - { name: work, mountPath: /home/node/work } volumes: - name: work emptyDir: {} - 4
Lock down the network
Deny all ingress and allow only DNS and HTTPS out. A plain NetworkPolicy cannot filter by hostname; to allow only api.anthropic.com and your git host, use an egress proxy or a CNI with FQDN policies.
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: claude-runs namespace: agents spec: podSelector: {} policyTypes: [Ingress, Egress] ingress: [] egress: - ports: - { protocol: UDP, port: 53 } - { protocol: TCP, port: 53 } - ports: - { protocol: TCP, port: 443 } - 5
Schedule it
Wrap the same spec in a CronJob. concurrencyPolicy Forbid stops a slow run from overlapping the next one.
apiVersion: batch/v1 kind: CronJob metadata: name: claude-nightly namespace: agents spec: schedule: "0 3 * * *" concurrencyPolicy: Forbid jobTemplate: spec: # same spec as the Job above - 6
Read the result
With --output-format json the pod log ends with one JSON object: the result, the session id and the cost. Ship logs to your log stack before the TTL removes the pod.
kubectl -n agents logs job/claude-triage | tail -n 1 | jq '.result, .total_cost_usd'
sourcesClaude Code docs: setupClaude Code docs: network accessKubernetes docs: JobsKubernetes docs: network policies
How do I keep an unattended Claude Code run safe?
Treat the pod as untrusted: it runs code an AI chose. Anthropic's devcontainer docs warn that a container is not full protection, that credentials in ~/.claude can be exfiltrated, and that --dangerously-skip-permissions is rejected when the CLI runs as root.
- Allow tools explicitly. Prefer dontAsk with a short allow list over bypassPermissions, which Anthropic says to use only in isolated containers or VMs.
- No service-account token: automountServiceAccountToken: false.
- Non-root, all capabilities dropped, no privilege escalation, Pod Security restricted on the namespace.
- A per-run or per-purpose secret, never your personal login.
- Egress only to the model API, your git host and package registries the task needs.
- A deadline on every run, and no automatic retries of a half-done change.
- Push to a branch for review, not straight to main.
sourcesClaude Code docs: devcontainerClaude Code docs: permissions
What does a plain Kubernetes Job not give you?
The pod is the easy part. A Job runs one prompt; it does not know which task is next (a work pool answers that), what process the task follows, where a person must decide, or how to pick up where a failed run stopped.
| Question | Plain Job | What you would build |
|---|---|---|
| Which work runs next? | Whatever the prompt says | A queue and selection rules |
| Which steps, in which order? | Text in the prompt or CLAUDE.md | A process the agent cannot skip |
| Where does a person decide? | Nowhere; prompts are denied | A gate that holds the work and waits |
| What happened, at what cost? | Pod logs until the TTL | A run record that outlives the pod |
| The run failed half way | Start again | Keep the work and resume |
How does ConvOps run Claude Code on Kubernetes?
Every isolated ConvOps run is its own Kubernetes Job: a fresh pod with the task volume, a secret made for this run and masked in logs, and the MCP servers the environment names as native sidecars. The pod has no service-account token, runs non-root with capabilities dropped under Pod Security restricted, and sits behind a NetworkPolicy with no ingress and limited egress.
The run follows the task's workflow one step at a time. A gate that needs a person holds the work until a person approves. Each executor has a time limit (4 hours by default) and the work resumes on the same volume. You can Stop any run. The work is pushed as a fast-forward or to a task branch, and a failed run keeps its commits on a rescue branch. When the pod is gone, the run record stays: tokens, model split, cost, turns, duration and the error class. Claude Code and OpenCode are supported today. Enterprise customers self-host the whole system on their own cluster with one Helm chart.
- 1
Self-host on your cluster (Enterprise)
One chart, one values file for the run namespace, posture, network policy, time limit and model credentials.
helm install convops convops/convops -n convops -f values.yaml - 2
Or bridge your own Job to ConvOps
Keep the Job above and let it take work from a ConvOps pool. Give it an API key through a variable whose name is not a credential name; Claude Code expands those in .mcp.json, and reads credential-named ones as empty.
{ "mcpServers": { "convops": { "type": "http", "url": "https://mcp.convops.app/", "headers": { "Authorization": "Bearer ${CONVOPS_TOKEN}" } } } } - 3
Prompt the run to follow the workflow
The agent claims the next task, works the current step and advances. When a step needs judgment it marks the task blocked and stops, and a person picks it up.
claude -p "Call pools_next for the pool nightly-fixes. Work the task by following its workflow: read the current step, do it, call workflow_advance. If a step needs human judgment, set the task to blocked with the reason and stop." \ --mcp-config .mcp.json \ --permission-mode dontAsk \ --allowedTools "Read,Grep,Glob,Edit,Bash(npm test *),mcp__convops"
sourcesClaude Code docs: MCP
When should Claude Code run unattended at all?
When the process has been run attended enough times that you trust it, and the steps that still need a person are gates rather than habits. Start with approvals on every step. As trust grows, let the work run on its own.
| Stage | How it runs | Where people stay |
|---|---|---|
| Attended | Claude Code in your terminal, one step at a time | Every advance |
| Gated | Unattended runs that stop at named gates | Approvals and blocked tasks |
| Autonomous | Schedules and pools pick the work | Only where consequence is high |
Frequently asked questions
Can Claude Code run without a human?
Yes. claude -p runs headless and exits with a status code. Pre-approve the tools it needs with --allowedTools and use --permission-mode dontAsk, so anything else is denied instead of waiting for an answer nobody will give.
Should I use --bare in Kubernetes?
Use it when the run should ignore CLAUDE.md, hooks, skills and MCP and behave the same everywhere. Leave it off when the run must follow the repo's CLAUDE.md or hooks. Bare mode needs ANTHROPIC_API_KEY or an apiKeyHelper.
Is --dangerously-skip-permissions safe in a container?
Anthropic says bypass mode belongs only in isolated containers or VMs, and the CLI rejects it as root. A short allow list with dontAsk is safer, because a mistaken or injected instruction cannot reach tools you did not list.
How do I stop a run that goes wrong?
Set activeDeadlineSeconds on the Job so Kubernetes ends it, and delete the Job to stop it by hand. Claude Code exits with 143 on SIGTERM. In ConvOps, every run has a time limit and a Stop button.
Can an unattended run wait for my approval?
Not inside a plain claude -p run: permission prompts are denied when nobody is attached. You need state outside the run. In ConvOps a gate holds the task, the run ends, and the work continues after a person approves.
terms in this guide
go deeper