Skip to main content
InfraGap.com Logo
Home
Getting Started
Core Concept What is a CDE? How It Works Benefits CDE Assessment Getting Started Guide Inner Loop vs Outer Loop Environment Drift Local vs Cloud CDEs for Startups
AI & Automation
AI Coding Assistants Agentic AI AI-Native IDEs Agentic Engineering AI Agent Orchestration AI Governance AI-Assisted Architecture Shift-Left AI LLMOps Autonomous Development AI/ML Workloads CDEs for Data Science GPU Computing
Agent Infrastructure
Agent Experience (AX) Agent Egress Control Computer Use Agents Agent Evals Agent Runbooks Agent Client Protocol AGENTS.md MCP Servers Git Worktrees Kubernetes Agent Sandbox Agent Fleets Agent Identity Prompt Injection Defense Agent Observability Context Engineering AI Code Review Bottleneck Headless Agents in CI Spec-Driven Development Agent Readiness Code Provenance
Implementation
Architecture Patterns DevContainers Advanced DevContainers Language Quickstarts IDE Integration CI/CD Integration Platform Engineering Developer Portals Container Registry Multi-CDE Strategies Remote Dev Protocols Nix Environments Hermetic Builds OpenTofu for CDEs Kubernetes Development
Operations
Performance Optimization High Availability & DR Disaster Recovery Monitoring Capacity Planning Multi-Cluster Development Troubleshooting Runbooks Ephemeral Environments Sandbox Environments Workspace Snapshots Database Branching
Security
Security Deep Dive Zero Trust Architecture Secrets Management Vulnerability Management Network Security IAM Guide Supply Chain Security Air-Gapped Environments AI Agent Security MicroVM Isolation Compliance Guide EU AI Act Cyber Resilience Act Data Residency Governance
Planning
Pilot Program Design Stakeholder Communication Risk Management Migration Guide Cost Analysis FinOps GreenOps Vendor Evaluation Training Resources Developer Onboarding Team Structure Platform Maturity Model Open Source CDEs AI Productivity Paradox Build vs Buy DevEx Metrics Productivity Engineering Industry Guides CDEs for Healthcare CDEs for Financial Services CDEs for Government Edge Development WebAssembly in CDEs
Resources
Tools Comparison State of CDEs 2026 Isolation Decision Tool Template Library
Learning Paths
All Paths Platform Engineer Security and Compliance Engineering Manager
Vendor Reviews
GitHub Codespaces Coder Ona Google Workstations Microsoft Dev Box Okteto Eclipse Che DevPod Daytona E2B
Head to Head
Coder vs Codespaces Coder vs Ona Ona vs Codespaces Coder vs Okteto Self-Hosted vs Managed E2B vs Daytona CDE Market Guide CDE vs Alternatives Case Studies Lessons Learned Glossary FAQ Sources & Citations

Headless AI Agents in CI

Runner sizing, secret injection, timeouts, cost caps and exit codes for coding agents that run with nobody watching

What "Headless" Actually Means

Same agent loop, no TTY, no human to approve the next step

A headless agent is not a cut-down agent. It is the same tool-calling loop you drive interactively, started with a flag that says "do not wait for me." Claude Code documents this plainly: "Add the -p (or --print) flag to any claude command to run it non-interactively." OpenAI documents codex exec for the same purpose, explicitly for teams that want to "Run as part of a pipeline (CI, pre-merge checks, scheduled jobs)." AWS makes the same claim for its CLI: "Headless mode lets you run Kiro CLI as part of your CI/CD pipeline to automate code reviews, generate tests, or troubleshoot build failures - no interactive terminal required."

What changes is what happens at a decision point. Interactively, a permission prompt stops the loop and a person answers it. In a pipeline nobody is there, so every one of those decisions has to be pre-committed as configuration: which tools are allowed, how many turns are permitted, how much money may be spent, and what the process should do when it runs out of any of those.

Still a full loop

Print mode is not a single completion call. The agent still reads files, runs tools and iterates, so every budget control matters more, not less

No prompt to say no

Permission decisions move from a human keystroke to a flag. Decide the tool allowlist before the job starts, because nothing will ask you later

Output is the contract

Nobody reads the scrollback. Structured output is the only way the rest of the pipeline learns what happened

Non-Interactive Invocations Across Tools

The flag differs, the shape does not

# Claude Code - print mode runs the full agent loop with no TTY
claude -p "Summarise the failing tests" --output-format json --max-turns 20

# OpenAI Codex - the documented non-interactive form
codex exec "Summarise the failing tests"

# Aider - send one message, process the reply, exit
# note the flag is --yes-always, not --yes
aider --message "Summarise the failing tests" --yes-always
ToolNon-interactive formLicenseNotes
claude -pPrint mode, full agent loopProprietaryOfficial anthropics/claude-code-action@v1 for GitHub Actions
codex execPipeline, pre-merge, scheduled jobsApache-2.0OpenAI recommends openai/codex-action over installing the CLI by hand
aider --messageOne message, reply, exit; disables chat modeApache-2.0Pair with --yes-always to auto-answer confirmations
OpenHands headlessDocumented mode for scripting and CI/CDMIT apart from the enterprise/ directoryGitHub's license detector reports "Other"; read the LICENSE before assuming
Kiro CLI headlessCode review, test generation, build triageProprietaryKiro CLI itself implements the Agent Client Protocol

Two details are worth pinning down because they are widely mis-stated. First, the direction of the Agent Client Protocol relationship: Kiro's own documentation says "Kiro CLI implements the Agent Client Protocol (ACP), an open standard that enables AI agents to work with any compatible editor." The CLI is the agent that plugs into ACP-compatible editors, not a consumer of them - see Agent Client Protocol for what that buys you. Second, Continue is Apache-2.0 but its repository carries a verbatim notice that it "is no longer actively maintained and is read-only for all users." Do not plan a pipeline around it. Cline is Apache-2.0 and active.

If you build against the Claude SDK rather than the CLI, note the rename: "The Claude Code SDK has been renamed to the Claude Agent SDK and its documentation has been reorganized." At SDK v0.1.0 the npm package moved from @anthropic-ai/claude-code to @anthropic-ai/claude-agent-sdk, PyPI from claude-code-sdk to claude-agent-sdk, and ClaudeCodeOptions to ClaudeAgentOptions. Pin the official GitHub Action too: its docs warn that v1 "introduces breaking changes that require updating your workflow files in order to upgrade to v1.0 from the beta version."

Sizing the Runner

The right question is what your test suite needs, not what the agent needs

The most common sizing mistake is treating an agent like a compiler. Between tool calls, a headless agent is doing almost nothing: it has issued an HTTPS request to a model provider and is waiting for the response. That wait dominates wall-clock time on most jobs. Buying a 16 vCPU runner so the agent "thinks faster" buys you a more expensive idle process, because the thinking happens somewhere else.

What does consume real resources is everything the agent triggers. It clones the repo, installs dependencies, runs your build, runs your test suite, maybe spins up a database container. Those are the workloads to size for, and they are the same workloads your existing CI already sizes for. If your test suite needs 8 GB of RAM, the agent job needs 8 GB of RAM plus a little headroom for the CLI process and its file cache. If your dependency install is 3 GB on disk, size the disk for that and cache it, because an agent that re-downloads the world on every turn wastes both time and your provider's patience.

Concurrency is where this gets expensive. Several agent jobs on one host will share CPU happily while they are all waiting on the network, then all wake up and run the test suite at the same moment. Size for the concurrent peak of the triggered work, not the average of the agent process.

vCPU: modest

Match your existing build job. Extra cores only help if the agent's shell commands are themselves parallel

RAM: size for the tests

The agent's own footprint is small; the test runner, language server and containers it starts are not

Disk: cache aggressively

A warm dependency cache removes minutes of wall clock that you would otherwise pay for at agent rates

Injecting Secrets Without Poisoning the Context

Anything in the agent's context can leave with the agent's next network call

There are two very different kinds of secret in an agent pipeline and conflating them is how credentials end up in a transcript. The first is the model provider key. That belongs in the CLI process environment and nowhere else: set it through your CI secret store, let the process read it, and never reference it in the prompt. The second kind is application secrets - the database password, the API token, the signing key that the agent's code or tests actually need. Those are harder, and the wrong answer is to dump them into the prompt so the agent "has what it needs."

The better pattern is brokering. A proxy holds the real credential, the agent calls the proxy, and the proxy attaches the credential after making a policy decision about whether this caller may make this call. The agent never sees the secret, so the secret cannot be repeated back, logged, or extracted. Where a broker is overkill, issue a short-lived scoped token that expires before the artifact retention window does, so a leaked transcript is a leaked expired token.

Treat context minimization as a security control, not a cost optimization. Every file the agent reads and every command output it captures becomes material that a successful prompt injection can ask it to send somewhere. Do not mount a .env the job does not need, do not run env and paste the result, and do not pipe a secret into the prompt on the theory that the transcript is private - the transcript is a CI artifact, and CI artifacts get downloaded.

Environment, never prompt

Provider keys go in the process environment through the CI secret store. They are never interpolated into prompt text or a config file the agent reads

Broker or scope

Application credentials sit behind a proxy that decides per call, or are minted as short-lived scoped tokens that outlive nothing

CI Hands an Agent All Three Legs by Default

The lethal trifecta is not a hypothetical in a pipeline; it is the default configuration

Simon Willison named the pattern in The lethal trifecta for AI agents (June 16, 2025). The three legs are "Access to your private data", "Exposure to untrusted content", and "The ability to externally communicate in a way that could be used to steal your data". His conclusion is blunt: "If your agent combines these three features, an attacker can easily trick it into accessing your private data and sending it to that attacker." Note that he does not call the third leg exfiltration - in the post he demotes that word to a parenthetical, saying he is "not confident that term is widely understood."

Now map that onto a pull-request review job. Private data: the entire repository, plus whatever is in the runner's environment. Untrusted content: the PR title, the issue body, the branch name, commit messages, dependency metadata, and the README of any transitive dependency the agent happens to open. External communication: the runner has outbound internet by default, and the agent can run curl if you let it use a shell. A stock CI agent has all three legs before you have written a line of prompt.

Private data

The checkout, the secret store, the runner environment - all reachable by a process you started on purpose

Untrusted content

PR titles, issue bodies, branch names and dependency READMEs are all attacker-writable in an open repository

External communication

Egress is open unless you close it. An allowlisted proxy is the difference between a finding and an incident

The controls belong on their own pages rather than duplicated here: see Prompt Injection Defense for content-boundary and egress patterns, and Agent Identity for giving the pipeline agent its own scoped principal instead of borrowing a human's. The one thing to do in the workflow file itself is narrow the tool allowlist. Claude Code exposes --allowed-tools (also spelled --allowedTools, with the docs now steering restriction toward --tools) and a --permission-mode that accepts default, acceptEdits, plan, auto, dontAsk, bypassPermissions and manual. A review job that only needs to read should be given read tools only.

Timeouts Live in Two Different Places

Turn limits come from the agent; wall clock has to come from the platform

This is the detail that surprises people. Claude Code has no general --timeout flag. If you write one into a workflow you will get an argument error, and if you assume one exists you will ship a job with no wall-clock ceiling at all. The in-tool budget controls are turn-based and money-based: --max-turns, documented as "Limit the number of agentic turns (print mode only). Exits with an error when the limit is reached. No limit by default", and --max-budget-usd. A wall-clock limit has to come from somewhere else: timeout-minutes in GitHub Actions, the equivalent in your platform, or coreutils timeout wrapped around the command.

Layer both, because they fail differently. Hitting --max-turns is a graceful exit: the process ends with an error status, having already flushed whatever it produced. A platform timeout is a kill: the job dies mid-write and you lose the tail of the log, which is precisely the part explaining why the agent was still going. If the only limit you have is the platform one, you will spend your debugging time reconstructing what happened from a truncated file. Stream the transcript to disk as it is produced so a hard kill still leaves you something to read.

name: Agent Code Review

on:
  pull_request:
    types: [opened, synchronize]

concurrency:
  group: agent-review-${{ github.ref }}
  cancel-in-progress: true

jobs:
  review:
    runs-on: ubuntu-latest
    # The only real wall clock. The CLI has no --timeout flag.
    timeout-minutes: 20
    permissions:
      contents: read

    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false

      - name: Install agent CLI
        run: npm install -g @anthropic-ai/claude-code

      - name: Run agent (non-interactive)
        env:
          # Pipeline-only key, separate billing from interactive use.
          ANTHROPIC_API_KEY: ${{ secrets.CI_AGENT_API_KEY }}
        run: |
          claude -p "Review this checkout against .ci/review-rules.md.
                     Write findings as JSON to review-verdict.json.
                     Do not edit files." \
            --output-format json \
            --max-turns 25 \
            --max-budget-usd 2.00 \
            --allowed-tools "Read,Grep,Glob" \
            > agent-transcript.json

      - name: Upload transcript
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: agent-transcript
          path: |
            agent-transcript.json
            review-verdict.json
          retention-days: 14

Cost Caps and Budget Circuit Breakers

Three layers, because any one of them can be bypassed by a loop you did not anticipate

An agent with no budget will keep going as long as its loop keeps producing tool calls. The nearest control is --max-budget-usd, which stops the run at a spend ceiling you choose per invocation. Set it to something a little above your observed p95 for that job rather than a round number you guessed, and revisit it when the prompt changes, because a prompt that starts asking the agent to read more files will move the distribution.

Above that sits the workflow ceiling. A per-invocation cap does nothing about a matrix job that runs 40 times, so the ceiling that matters organizationally is per workflow per day. And above both sits the control most teams skip: give the pipeline its own billing key, separate from the one your engineers use interactively. That is not bookkeeping. It means a runaway scheduled job cannot drain the budget developers rely on to get work done, and the spend graph stays legible about which of the two is growing.

Per invocation

--max-budget-usd stops one run. Tune it from measured spend, not from a guess

Per workflow

A daily ceiling for the whole pipeline catches the fan-out case a per-run cap cannot see

Separate billing key

A pipeline-only key means automation can never starve interactive work, and the spend graph stays legible

Exit Codes Have to Mean Something

"The agent broke" and "the agent found a problem" are not the same red X

This is the hardest part to get right and the easiest to get wrong quietly. A non-zero exit from an agent CLI can mean the process crashed, the API returned an error, the turn limit was reached, the budget was exhausted - or nothing went wrong at all and the agent is correctly reporting that your code has problems. If your pipeline collapses all of those into "failed", the check stops carrying information and people start ignoring it.

The fix is to stop letting the agent decide the build status. Have it emit a structured verdict - JSON written to a file, or --output-format json captured and parsed - and have a thin wrapper script map that verdict to an exit code. The wrapper owns the semantics, so the CI status means what you documented it to mean, and you can change the policy (fail on high severity only, warn on medium) without touching the prompt.

Exit codeMeaningCI should
0Agent ran to completion, no findingsPass the check
1Agent ran fine and reported findings in the codeFail the check, post the findings, do not retry
2Agent error: crash, API failure, unparseable verdict, turn or budget limitMark infrastructure failure, alert the pipeline owner, retry is reasonable
124Wall-clock timeout, the coreutils conventionMark timeout distinctly; repeated hits mean the budget is wrong, not the code
#!/usr/bin/env bash
# review.sh - run the agent, then decide the exit code ourselves.
set -uo pipefail

TRANSCRIPT="agent-transcript.json"
VERDICT="review-verdict.json"
TIMEOUT_SECS="${AGENT_TIMEOUT_SECS:-900}"

# coreutils timeout supplies the wall clock the CLI does not have.
timeout --signal=TERM --kill-after=30s "$TIMEOUT_SECS" \
  claude -p "$(cat .ci/review-prompt.md)" \
    --output-format json \
    --max-turns 25 \
    --max-budget-usd 2.00 \
    --allowed-tools "Read,Grep,Glob" \
  | tee "$TRANSCRIPT"
status="${PIPESTATUS[0]}"

if [ "$status" -eq 124 ]; then
  echo "agent: wall-clock timeout after ${TIMEOUT_SECS}s"
  exit 124
fi

if [ "$status" -ne 0 ]; then
  echo "agent: exited $status - turn limit, budget limit or crash"
  exit 2
fi

findings="$(jq -r '.findings // "error"' "$VERDICT" 2>/dev/null || echo error)"

if [ "$findings" = "error" ]; then
  echo "agent: completed but produced no parseable verdict"
  exit 2
fi

if [ "$findings" -gt 0 ]; then
  echo "agent: $findings finding(s) - failing the check"
  exit 1
fi

echo "agent: clean"
exit 0

Note the tee and PIPESTATUS pairing. Redirecting straight to a file is simpler, but piping through tee means the transcript lands on disk incrementally and the live job log shows progress, which matters when someone is watching a twenty-minute job wondering whether it is stuck.

Artifacts, Logs and Rate Limits

Keep enough to answer "why did it do that" a week later, and do not fan out into a 429 wall

Capture three things separately, because they answer different questions. The transcript - --output-format stream-json streamed to a file - answers what the agent did and in what order. The diff, saved on its own, answers what changed. The commentary, the human-readable summary, is what goes on the pull request. Merging them into one blob means that reviewing the change requires reading the reasoning, and auditing the reasoning requires scrolling past the diff.

Upload all of it with an explicit retention window long enough to cover the gap between a change shipping and someone asking about it. Then treat those artifacts as repository content, because that is what they are: a transcript from a private repo quotes private source at length, and it inherits that repository's confidentiality classification. If the repo is not world-readable, its transcripts should not be either.

The last operational trap is fan-out. A matrix job running an agent across 30 repositories will hit your model provider's rate limits long before it hits any runner limit, and the failure looks like a flaky agent rather than a quota problem. Use a concurrency group to cap how many agent jobs run at once, stagger the start times rather than firing them all on the same cron minute, and handle HTTP 429 with exponential backoff and jitter instead of an immediate retry that arrives inside the same window. A pipeline that runs 5 agents reliably is worth more than one that launches 30 and completes 11.

Stream, do not buffer

stream-json to a file survives a hard kill. A buffered transcript that never flushes tells you nothing about why the job died

Transcripts are source

They quote your repository at length. Apply the same access controls and the same retention policy you apply to the code

Throttle the fan-out

Concurrency groups, staggered schedules and jittered backoff on 429. Provider quota is the real constraint, not runner count