← all posts

· Agents · 7 min read

Run Claude Code in CI on your subscription, not the metered API

I run Claude Code non-interactively in CI now — a pipeline job that clones a repo, hands Claude a prompt, lets it edit files, and opens a merge request. It works well. But the first time I wired it up I did the obvious thing: dropped an ANTHROPIC_API_KEY into a masked CI variable and moved on.

Then I remembered I'm already paying for a Max subscription. And that API key doesn't touch it. The API key bills a completely separate, metered, pay-per-token account. Every pipeline run was quietly racking up API charges next to a subscription that was sitting right there with usage included.

There's a second way to authenticate Claude Code, and it draws on the subscription. It's not obvious, it's one environment variable, and the switch is worth understanding — along with the edges, because "run my personal plan inside CI" has a couple of them.

Two auth modes, two bills

Claude Code can authenticate two ways, and which env var you set decides who pays:

Same CLI, same claude -p, same everything else. The only difference is the credential in the environment — and the account it draws down. If you have a subscription and your CI volume fits inside it, the OAuth token is free money you're otherwise leaving on the table.

Getting the token

The API key you copy from a console. The OAuth token you generate, once, from a machine where you're already logged into the subscription:

claude setup-token

It runs an interactive browser flow and prints a long-lived token — CLAUDE_CODE_OAUTH_TOKEN. Copy it. That's the thing your pipeline will use; you never have to repeat the browser dance in CI.

Then store it as a masked CI/CD variable. In GitLab: Settings → CI/CD → Variables → Add, key CLAUDE_CODE_OAUTH_TOKEN, tick Masked, and — this matters — Protected only if your pipeline runs on protected branches. Treat it exactly like the API key: it's a bearer credential for your account.

The one trap: don't set both

Here's the part that'll bite you. If ANTHROPIC_API_KEY is also present in the job's environment, Claude Code will happily use it — and you're back to metered billing without noticing, because nothing errors. The behaviour is silent.

So the rule for a subscription job is: CLAUDE_CODE_OAUTH_TOKEN set, ANTHROPIC_API_KEY absent. If you have the API key defined at the group or project level for other jobs, explicitly unset it in this one:

variables:
  ANTHROPIC_API_KEY: ""   # force the OAuth/subscription path

Empty-string it, or scope the API key so it never reaches this job. Don't rely on remembering — an inherited variable is invisible until the invoice shows up.

A minimal GitLab pipeline

The image needs Claude Code installed — npm i -g @anthropic-ai/claude-code on any Node base, or a prebuilt image that already has it. Then the job is small:

claude:
  image: node:22-bookworm            # or your image with claude preinstalled
  variables:
    ANTHROPIC_API_KEY: ""            # subscription path — see above
  rules:
    - if: '$PROMPT'                  # only run when triggered with a prompt
  script:
    - command -v claude || npm i -g @anthropic-ai/claude-code
    - claude -p "$PROMPT"
        --permission-mode acceptEdits
        --allowedTools "Read,Edit,Write,Bash"
    # ...then commit/push/open an MR with the results

claude -p is print (headless) mode: one prompt in, the agent works autonomously, output out. --permission-mode acceptEdits lets it write files without a human at a prompt. --allowedTools scopes what it may reach for — worth setting explicitly in an unattended context.

Two flag notes I learned the annoying way:

CLAUDE_CODE_OAUTH_TOKEN comes in from the masked CI variable automatically — it's in the environment, Claude Code reads it, done. You don't reference it in the script at all.

The edges of billing CI to a personal plan

This is the part the tutorials skip, and it's the reason "just use the subscription" isn't always the right call.

Your plan's limits are now shared with a robot. A Max subscription has usage limits — generous, but finite, and they reset on a window. A pipeline that fires on every push, or a fleet of parallel jobs, can chew through that allowance and then you hit the wall mid-afternoon on your own laptop because CI ate the budget. The API key has no such ceiling (it just costs money). So the trade is real: subscription = free but capped and shared with your interactive use; API = unbounded but metered.

It's genuinely your personal usage. setup-token is the documented, supported way to run Claude Code headless, so this isn't a hack. But the token authenticates as you, on your plan. For your own automation, fine. For anything multi-tenant — a shared runner other people trigger, a SaaS you're building on top — don't put your personal subscription token behind it. That's what API keys (and per-customer keys) are for.

The token is long-lived — so treat it like one. It doesn't expire on the timescale a session token would, which is exactly why it's convenient in CI and exactly why a leak is worse. Masked variable, minimum scope, and rotate it (claude setup-token again) if a job log ever looks suspicious. Never echo it, and remember that anything the agent's shell can reach, it can potentially exfiltrate — scope --allowedTools and the job's network accordingly.

So which should you use?

My rule of thumb after running both:

The nice thing is switching is a one-variable change. Start on the subscription, watch your plan's usage, and if CI starts crowding out your own work, flip that job to an API key. Same pipeline, same image, same claude -p — you're only ever changing which credential is in the environment, and therefore which account picks up the tab.