claude code

How to run Claude Code unattended on Kubernetes

To run Claude Code unattended on Kubernetes, build an image with a pinned Claude Code version, run claude -p headless in a Job with explicit allowed tools, no service-account token, a non-root locked-down pod, a time limit and egress limited to what the run needs. Then decide what picks the work and where a person signs off.

the route

8 sections · 11 min

  1. Headless Claude Code
  2. Credentials
  3. The options
  4. Claude Code as a Job
  5. Safety checklist
  6. What a Job lacks
  7. How ConvOps runs it
  8. When to go unattended
01Headless Claude Code

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.

Claude Code flags that matter for unattended runs.
FlagWhat 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 dontAskDenies anything not pre-approved instead of asking
--permission-prompts noneTurns prompts off entirely for unattended runs (recent versions)
--max-turns 40Stops after that many agentic turns and exits with an error. No limit by default
--max-budget-usd 5Stops once API spend for the run reaches that amount
--output-format jsonMachine-readable result, including total_cost_usd
--json-schemaReturns structured output that matches your JSON Schema
--bareSkips CLAUDE.md, hooks, skills and MCP for a predictable script run
--mcp-config file.jsonLoads MCP servers from a file
--resume <session_id>Continues an earlier session
headless run
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

sourcesClaude Code docs: headless modeClaude Code docs: CLI referenceClaude Code docs: permissions

claude -pthe prompt
allowed toolslisted up front
deniednot on the list
exit 0JSON result
fig 1 Unknown means no. Anything not pre-approved is denied.
02Credentials

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.

CredentialWorks with --bareNotes
ANTHROPIC_API_KEYYesConsole API key, billed per token
apiKeyHelperYesA script that prints a key, for rotation
CLAUDE_CODE_OAUTH_TOKENNoFrom claude setup-token, model requests only
Cloud provider (Bedrock, Vertex)Provider-specificTakes precedence over the others

sourcesClaude Code docs: authenticationClaude Code docs: headless mode

API keyworks with --bare
OAuth tokenclaude setup-token
Kubernetes Secretone per purpose
fig 2 Injected as a secret. Never a login baked into the image.
03The options

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.

OptionWhere it runsPicks the workSign-off mid-run
CI job (GitHub Actions, GitLab CI)Your CI runnersThe pipeline triggerOnly between jobs
Claude Code Routines (research preview)Anthropic cloud or a self-hosted environmentSchedule (at most hourly), API call or GitHub eventNo. Anthropic says the run continues without stopping for approval
Devcontainer on a VMOne containerYou, by handIn the live session only
Your own Kubernetes Job or CronJobYour clusterYour prompt or scriptNo, unless you build it
ConvOps isolated runOne pod per run on your KubernetesPools and schedulesYes, gates hold the work for a person

sourcesClaude Code docs: RoutinesClaude Code docs: devcontainer

CI or Routinesno mid-run sign-off
Your own Jobyou build the rest
ConvOps rungates hold the work
fig 3 Where the run lives, and whether a person can sign off.
04Claude Code as a Job

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. 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. 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. 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. 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. 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. 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

pinned image
secret
locked-down podnon-root, no token
deadlineapproval
resultJSON in the log
fig 4 One Job per run. Example manifests below.
05Safety checklist

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.

  1. Allow tools explicitly. Prefer dontAsk with a short allow list over bypassPermissions, which Anthropic says to use only in isolated containers or VMs.
  2. No service-account token: automountServiceAccountToken: false.
  3. Non-root, all capabilities dropped, no privilege escalation, Pod Security restricted on the namespace.
  4. A per-run or per-purpose secret, never your personal login.
  5. Egress only to the model API, your git host and package registries the task needs.
  6. A deadline on every run, and no automatic retries of a half-done change.
  7. Push to a branch for review, not straight to main.

sourcesClaude Code docs: devcontainerClaude Code docs: permissions

No SA tokennon-root, caps dropped
Egress limitedno ingress
Short allow listeverything else denied
fig 5 The pod runs code an AI chose. Treat it that way.
06What a Job lacks

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.

QuestionPlain JobWhat you would build
Which work runs next?Whatever the prompt saysA queue and selection rules
Which steps, in which order?Text in the prompt or CLAUDE.mdA process the agent cannot skip
Where does a person decide?Nowhere; prompts are deniedA gate that holds the work and waits
What happened, at what cost?Pod logs until the TTLA run record that outlives the pod
The run failed half wayStart againKeep the work and resume
07How ConvOps runs it

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. 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. 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. 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

pool picks task
Job + secretone per run
stepsone at a time
gateapproval
run record
fig 6 One pod per run. The pod goes; the record stays.
08When to go unattended

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.

StageHow it runsWhere people stay
AttendedClaude Code in your terminal, one step at a timeEvery advance
GatedUnattended runs that stop at named gatesApprovals and blocked tasks
AutonomousSchedules and pools pick the workOnly where consequence is high
attendedevery advance
gatedapproval
autonomouspools and schedules
fig 7 Start with approvals on every step. As trust grows, let the work run on its own.

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.