# Headless CI: your coding agent in GitHub Actions

Headless CI runs your coding agent in a CI job through the Context Mode gateway. You add the gateway's base URL and a few request headers to the workflow. The agent, its own install, the prompt and the model stay the same.

## Key facts

What it is

Two env lines for a CI job: `ANTHROPIC_BASE_URL` and `ANTHROPIC_CUSTOM_HEADERS`. `npx @context-mode/cli ci init` writes them into a GitHub Actions workflow for you.

Who it is for

Teams that run a coding agent on pull requests, CI failures, issues or releases.

Clients

Claude Code headless (`claude -p`), `anthropics/claude-code-action` and the Claude Agent SDK. Codex, OpenCode and Pi connect by base URL too: [their lines](#other-agents).

Measured result

24% to 34% lower cost per run on 4 CI jobs, Claude Code headless with Sonnet 5.5, 2026-11-01: [the table](#results) and [Benchmarks](https://contextmode.ai/docs/benchmarks).

Limits

Needs a CI key, which a signed-in terminal makes. GitHub gives no secrets to pull requests from forks, so the workflow skips them. The table's figures are for Claude Code with Sonnet 5.5.

## Turn it on

Run these in your repo. First sign your terminal in, once:

```
npx @context-mode/cli login
```

Make a CI key and save it as the repo secret `CONTEXT_MODE_KEY`. `--gh` saves it with the [GitHub CLI](https://cli.github.com). The repo comes from your git origin, or from `--repo owner/name`:

```
npx @context-mode/cli ci key create --gh
```

Write the workflow:

```
npx @context-mode/cli ci init
```

Add your model key as the secret `ANTHROPIC_API_KEY`. Then open a pull request: the review runs through Context Mode.

## What ci init writes

`.github/workflows/context-mode-review.yml`, word for word. The only Context Mode lines are in the job's `env` block.

```
# Context Mode + anthropics/claude-code-action: Claude reviews every pull request.
# Written by `npx @context-mode/cli ci init`. The only Context Mode lines are in the job "env" block below.
# Secrets: ANTHROPIC_API_KEY (your model key, as the action needs today) and CONTEXT_MODE_KEY (a CI key from
# `npx @context-mode/cli ci key create`). This file holds no key.
name: context-mode-review

on:
  pull_request:
    types: [opened, synchronize, ready_for_review]

permissions:
  contents: read
  pull-requests: write

concurrency:
  # One review per PR at a time; a new push cancels the old review.
  group: context-mode-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  review:
    # Fork PRs get no secrets (GitHub rule), so skip them instead of failing.
    if: github.event.pull_request.head.repo.full_name == github.repository && !github.event.pull_request.draft
    runs-on: ubuntu-latest
    timeout-minutes: 20
    env:
      # --- Context Mode: start ---------------------------------------------
      # 1. Send the agent's model requests through the gateway.
      ANTHROPIC_BASE_URL: https://gateway.contextmode.ai
      # 2. One header per line. The gateway strips every x-cm-* header before it
      #    forwards, so CONTEXT_MODE_KEY never reaches Anthropic.
      #    x-context-mode-banner: off keeps the savings line out of the review text.
      #    x-cm-ci-* tell the gateway which repository, pull request and run sent the request.
      ANTHROPIC_CUSTOM_HEADERS: |
        x-cm-device-key: ${{ secrets.CONTEXT_MODE_KEY }}
        x-context-mode-banner: off
        x-cm-ci-repo: ${{ github.repository }}
        x-cm-ci-ref: pr-${{ github.event.pull_request.number }}
        x-cm-ci-run: ${{ github.run_id }}
      # 3. Claude Code turns tool search off for a non-first-party base URL; turn it back on.
      ENABLE_TOOL_SEARCH: "true"
      # --- Context Mode: end -----------------------------------------------
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          github_token: ${{ secrets.GITHUB_TOKEN }}
          prompt: |
            Review pull request #${{ github.event.pull_request.number }} in ${{ github.repository }}.
            Compare the PR head with origin/${{ github.event.pull_request.base.ref }}.
            Report only real problems: bugs, missing tests, security issues, breaking changes.
            For each one give the file, the line and a one-line fix.
            Post the result as one PR comment with `gh pr comment`. If you find nothing, say so in one line.
          claude_args: |
            --max-turns 20
            --allowedTools "Bash(git diff:*),Bash(git log:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Read,Grep,Glob"
```

## claude -p in your own job

For a CI-failure fix, triage or release notes, put the same env block on the job. Claude Code's own installer comes first:

```
env:
  ANTHROPIC_BASE_URL: https://gateway.contextmode.ai
  ANTHROPIC_CUSTOM_HEADERS: |
    x-cm-device-key: ${{ secrets.CONTEXT_MODE_KEY }}
    x-context-mode-banner: off
    x-cm-ci-repo: ${{ github.repository }}
    x-cm-ci-ref: pr-${{ github.event.pull_request.number }}
    x-cm-ci-run: ${{ github.run_id }}
  ENABLE_TOOL_SEARCH: "true"
  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
steps:
  - run: |
      curl -fsSL https://claude.ai/install.sh | bash
      echo "$HOME/.local/bin" >> "$GITHUB_PATH"
  - run: claude -p "The tests fail. Find the cause and fix it." --allowedTools "Bash,Read,Edit"
```

Claude Code turns tool search off for a base URL that is not Anthropic's, so the block sets `ENABLE_TOOL_SEARCH: "true"`. The Claude Agent SDK reads the same env vars, with no code change.

## Measured result

24% to 34% lower cost per run on 4 CI jobs. Each task ran twice, back to back: direct to Anthropic, and through the gateway with the lines `ci init` writes. Same agent, same prompt, same model.

| CI job | Saved | Direct | Context Mode | Per 1,000 runs | Cheaper | Quality | Evidence |
| --- | --- | --- | --- | --- | --- | --- | --- |
| CI-failure fix | **33.9%** Sonnet 5.5, 95% range 25.1 to 43.6% | $0.0442 | $0.0281 | $16 | 6 of 6 | Tests pass 7 of 7 in both | [Workflow](https://github.com/mksglu/cm-ci-lab/blob/main/.github/workflows/triage.yml) · [Tasks](https://github.com/mksglu/cm-ci-lab/blob/main/reproduce/tasks/triage.json) |
| Issue triage and labels | **29.6%** Sonnet 5.5, 95% range 21.1 to 38.8% | $0.0262 | $0.0183 | $8 | 6 of 6 | Label F1 pass 5 of 7 direct, 4 of 7 with Context Mode | [Workflow](https://github.com/mksglu/cm-ci-lab/blob/main/.github/workflows/labels.yml) · [Tasks](https://github.com/mksglu/cm-ci-lab/blob/main/reproduce/tasks/labels.json) |
| Release notes | **25.7%** Sonnet 5.5, 95% range 7.2 to 42.7% | $0.1333 | $0.0987 | $35 | 4 of 5 | Written notes, no pass mark | [Workflow](https://github.com/mksglu/cm-ci-lab/blob/main/.github/workflows/release.yml) · [Tasks](https://github.com/mksglu/cm-ci-lab/blob/main/reproduce/tasks/release.json) |
| @claude fix (issue to PR) | **23.7%** Sonnet 5.5, 95% range 11.4 to 35.7% | $0.0902 | $0.0736 | $17 | 6 of 6 | Tests pass 5 of 7 in both | [Workflow](https://github.com/mksglu/cm-ci-lab/blob/main/.github/workflows/mention.yml) · [Tasks](https://github.com/mksglu/cm-ci-lab/blob/main/reproduce/tasks/mention.json) |

Claude Code headless (`claude -p`), Sonnet 5.5, 2026-11-01, the live gateway. Cost per run at list price, warm runs. Saved: each task gives a cost ratio, Context Mode over direct; the saving is 1 minus the geometric mean of those ratios, so it can differ from the two mean costs. Per 1,000 runs: the direct cost minus the Context Mode cost, times 1,000. Proof: [the round](https://github.com/mksglu/cm-ci-lab/blob/main/docs/ci-night-1101-r2.md) · [raw results](https://contextmode.ai/benchmarks/ci/results-r2.json) ([in the lab](https://github.com/mksglu/cm-ci-lab/blob/main/lab/ci-night/results-r2.json)) · [data file](https://contextmode.ai/benchmarks/data/ci-night-1101-r2.json) · [the plan](https://github.com/mksglu/cm-ci-lab/blob/main/docs/plan-ci-1031.md), written 2026-10-31 19:27 UTC, before the first run at 2026-11-01 04:22 UTC · [reproduce it](https://github.com/mksglu/cm-ci-lab/blob/main/reproduce/README.md).

Run the same jobs in your own repo: [make a copy of the lab](https://github.com/mksglu/cm-ci-lab/generate). Your key stays with you.

## Codex, OpenCode and Pi

Each agent installs its own way first. Then its gateway config adds the Context Mode lines.

### Codex (`codex exec`)

Job env: `CONTEXT_MODE_KEY: ${{ secrets.CONTEXT_MODE_KEY }}` and the agent's own model key, `OPENAI_API_KEY`. Then the agent's own install, its gateway config and the run:

```
npm i -g @openai/codex
cat >> ~/.codex/config.toml <<'TOML'
model_provider = "context-mode"
[model_providers.context-mode]
name = "context-mode"
base_url = "https://gateway.contextmode.ai"
wire_api = "responses"
env_key = "OPENAI_API_KEY"
env_http_headers = { "x-cm-device-key" = "CONTEXT_MODE_KEY", "x-cm-ci-repo" = "GITHUB_REPOSITORY", "x-cm-ci-run" = "GITHUB_RUN_ID" }
TOML
codex exec "Review this pull request ..."
```

### OpenCode (`opencode run`)

Job env: `CONTEXT_MODE_KEY: ${{ secrets.CONTEXT_MODE_KEY }}` and the agent's own model key, `ANTHROPIC_API_KEY`. Then the agent's own install, its gateway config and the run:

```
curl -fsSL https://opencode.ai/install | bash
opencode.json in the repo root:
{ "provider": { "anthropic": { "options": { "baseURL": "https://gateway.contextmode.ai/v1",
  "headers": { "x-cm-device-key": "{env:CONTEXT_MODE_KEY}", "x-context-mode-banner": "off" } } } } }
opencode run "Review this pull request ..."
```

### Pi (`pi -p`)

Job env: `CONTEXT_MODE_KEY: ${{ secrets.CONTEXT_MODE_KEY }}` and the agent's own model key, `ANTHROPIC_API_KEY`. Then the agent's own install, its gateway config and the run:

```
npm install -g @earendil-works/pi-coding-agent
~/.pi/agent/models.json:
{ "providers": { "anthropic": { "baseUrl": "https://gateway.contextmode.ai",
  "headers": { "x-cm-device-key": "${CONTEXT_MODE_KEY}", "x-cm-client": "pi" } } } }
pi -p "Review this pull request ..."
```

## What each header does

| Header | What it does |
| --- | --- |
| `x-cm-device-key` | Names your account, for usage and billing. |
| `x-cm-ci-repo` | Marks the request as CI: no personal memory, cost per repo. |
| `x-cm-ci-ref` | The pull request or the branch. |
| `x-cm-ci-run` | Cost per run, and the gap between runs that sets how long the cache lives. |
| `x-context-mode-banner: off` | Keeps the savings line out of the review text. |

The gateway removes every `x-cm-*` header before it forwards. None of them reaches the model provider.
