· 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:
ANTHROPIC_API_KEY— an API console key (sk-ant-...). Pay-per-token against your Anthropic API account. No cap, no subscription involvement. This is what every "headless Claude in CI" tutorial hands you.CLAUDE_CODE_OAUTH_TOKEN— a long-lived OAuth token tied to your Claude subscription (Pro/Max). Usage counts against your plan's included allowance and limits.
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:
- Don't run
--dangerously-skip-permissionsas root. The CLI refuses it. Either run the job as a non-root user (good practice anyway) or use--permission-mode acceptEdits, which works fine as root. --output-format stream-jsonrequires--verbose. If you're not parsing the stream, just leave the flag off and let it print normally.
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:
- Low-volume, personal automation — nightly housekeeping, the occasional "turn this issue into an MR," a repo you own. Use the subscription token. You're already paying for the plan; let CI sip from it.
- High-volume, bursty, or anything shared — per-commit agent runs, parallel matrix jobs, a runner other people can trigger, a product. Use an API key, ideally scoped per project, so cost is explicit and nobody's personal plan is the bottleneck.
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.