Claude Code in GitLab CI/CD: Jobs, OIDC and MRs

Add a masked ANTHROPIC_API_KEY variable under Settings then CI/CD then Variables, and add a job that installs the CLI with the install script and calls claude with the -p flag. Set the permission mode to acceptEdits and name the tools Claude may use, including the GitLab MCP tool, in the allowedTools flag.
The installer places the binary in ~/.local/bin, which is not on PATH in the node alpine image. Add an export line putting that directory on PATH in your before_script, after the install step. The error names the command rather than the path, which is why it sends people back to the install step that actually worked.
No. GitLab listens for the triggers you configure, and mention-driven runs need a piece you supply: a project webhook for comment events pointing at an event listener that calls the pipeline trigger API with the comment context as variables. If a mention appears to do nothing, the listener is usually what is missing.
Yes, on Amazon Bedrock and Google Cloud's Agent Platform. GitLab mints an OIDC token for the job from an id_tokens block; the job exchanges it for temporary AWS credentials with assume-role-with-web-identity, or uses Workload Identity Federation for Google Cloud. No long-lived keys are stored in either path.
Three independent brakes: the --max-turns flag caps back-and-forth iterations and bounds token spend, GitLab's job-level timeout caps runner minutes which are billed separately, and concurrency limits cap how many jobs run at once. Specific prompts also cost less than vague ones, because a vague run only stops when it exhausts its turn budget.

Key Takeaway
Claude Code runs in GitLab CI/CD as an ordinary job that installs the CLI and calls it in non-interactive mode. It is a beta integration maintained by GitLab. Setup is one job in the pipeline file plus a masked variable, and it supports the Claude API, Amazon Bedrock via OIDC, and Google Cloud's Agent Platform via Workload Identity Federation.
My first GitLab job installed Claude Code successfully and then failed with claude: not found. The install had worked perfectly. The installer puts the binary in a directory that is not on the path in the node alpine image, and one missing export line turned a working setup into fifteen minutes of reading a log that said the install succeeded.
That is representative of this integration: the concept is simple and the friction is entirely in the plumbing. This post covers what the integration actually is, the minimal job, the mention-driven trigger you have to build yourself, the two keyless authentication paths for enterprise providers, and the flags that keep a runaway job from costing more than it saves.
There is no GitLab application to install and no service to authorise. The integration is built on the Claude Code CLI and Agent SDK: a job installs the CLI, runs it with a prompt in non-interactive mode, and lets it commit results back through a merge request. Every change flows through an MR, so reviewers see the diff and your existing approvals and branch protection still apply.
Two things follow from that. First, this is currently in beta and maintained by GitLab rather than Anthropic, with support tracked in a GitLab issue rather than through Anthropic. Second, anything you can do with the CLI you can do here — the constraints are the ones you put in the job, not ones the integration imposes.
This is the whole setup: add a masked variable for your API key under the project's CI/CD settings, then add a job. Note the export line in the before-script — it is the difference between a working pipeline and a confusing one.
# .gitlab-ci.yml — the smallest job that works.
stages:
- ai
claude:
stage: ai
image: node:24-alpine3.21
rules:
- if: '$CI_PIPELINE_SOURCE == "web"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
variables:
GIT_STRATEGY: fetch
before_script:
- apk update
- apk add --no-cache git curl bash
- curl -fsSL https://claude.ai/install.sh | bash
# The installer puts claude in ~/.local/bin, which is NOT on
# PATH in this image. Miss this line and the job fails with
# "claude: not found" AFTER a successful install.
- export PATH="$HOME/.local/bin:$PATH"
script:
- /bin/gitlab-mcp-server || true
- >
claude
-p "Review this MR and implement the requested changes"
--permission-mode acceptEdits
--allowedTools "Bash Read Edit Write mcp__gitlab"
--max-turns 25
timeout: 30mThe installer places the binary in a directory that is not on the path in this image, so without that export line the job installs Claude Code successfully and then fails to find it. The error names the command rather than the path, which sends you looking at the install step that worked rather than the environment that did not. It costs an afternoon exactly once.
Every write-up of this integration shows someone typing an at-mention in an MR comment and Claude responding, which makes it look like comment handling is built in. It is not. GitLab listens for whatever triggers you configure, and mention-driven runs need a piece you supply: a project webhook for comment events pointing at an event listener, which calls the pipeline trigger API with the comment context as variables when it sees the mention.
The job then reads those variables — the input, the context and the event — and passes the input to the CLI as the prompt, falling back to a default when the job was started some other way. If your comment mention appears to do nothing, the listener is almost always what is missing, not the job. It is also worth checking the comment uses the at-sign form rather than a slash, which is a surprisingly common typo.
For enterprise environments the interesting part is that neither provider needs a stored key. GitLab mints an OIDC token for the job from an id-tokens block and exposes it to the script, which exchanges it for temporary credentials at runtime. The audience value has to match what you configured on the identity provider — usually your GitLab instance URL.
# Amazon Bedrock via OIDC — no static AWS keys anywhere.
claude-bedrock:
stage: ai
image: node:24-alpine3.21
rules:
- if: '$CI_PIPELINE_SOURCE == "web"'
id_tokens:
GITLAB_OIDC_TOKEN:
# Must match the audience configured on the IAM OIDC
# identity provider in AWS — usually your GitLab URL.
aud: https://gitlab.example.com
before_script:
- apk add --no-cache bash curl jq git aws-cli
- curl -fsSL https://claude.ai/install.sh | bash
- export PATH="$HOME/.local/bin:$PATH"
- export AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/oidc_token"
- printf "%s" "$GITLAB_OIDC_TOKEN" > "$AWS_WEB_IDENTITY_TOKEN_FILE"
- >
aws sts assume-role-with-web-identity
--role-arn "$AWS_ROLE_TO_ASSUME"
--role-session-name "gitlab-claude-run"
--web-identity-token "file://$AWS_WEB_IDENTITY_TOKEN_FILE"
--duration-seconds 3600 > /tmp/aws_creds.json
- export AWS_ACCESS_KEY_ID="$(jq -r .Credentials.AccessKeyId /tmp/aws_creds.json)"
- export AWS_SECRET_ACCESS_KEY="$(jq -r .Credentials.SecretAccessKey /tmp/aws_creds.json)"
- export AWS_SESSION_TOKEN="$(jq -r .Credentials.SessionToken /tmp/aws_creds.json)"
variables:
AWS_REGION: "us-west-2"
CLAUDE_CODE_USE_BEDROCK: "1"Google Cloud's Agent Platform follows the same shape through Workload Identity Federation: the job writes the OIDC token to a file, writes a credential configuration whose credential source points at that file, and exposes it through the application default credentials variable. No service account keys are downloaded at any point. Both paths are strictly better than a long-lived secret in a CI variable, and both take about twenty minutes to set up once.
Two separate permission systems apply here and confusing them produces the most common failure, which is a job that runs happily and then cannot comment or open an MR:
Put your conventions in a CLAUDE.md at the repository root rather than in the job's prompt. Claude reads it during every run, so review criteria, coding standards and the things you never want touched live in one reviewed file instead of being duplicated across a review job, an implement job and a refactor job that will drift apart within a month.
The useful framing is that a CI job is a place for work with a clear input and a reviewable output. Anything needing judgement about product direction belongs in a session where you can steer it.
Three shapes that work well, and what triggers each:
| Task | Trigger | Output |
|---|---|---|
| Turn an issue into a merge request | A mention in an issue comment | A branch and an MR for review |
| Fix a named bug | A mention naming the error and component | A commit on the existing branch, or a new MR |
| Respond to review feedback | A follow-up comment on the MR | Further commits on the same branch |
A CI job that can loop is a CI job that can spend, and there are three independent brakes worth setting from the first run rather than the first invoice:
Specific mentions cost less than vague ones. Asking Claude to fix the type error in the user dashboard component gives it a target; asking it to improve the code gives it a search. The turns flag will eventually stop a vague run, but it stops it by running out of budget rather than by finishing, and you pay for the difference either way.
The GitLab integration is worth setting up if you already live in merge requests, and the setup is genuinely small — one job, one variable, and a listener if you want mentions. What decides whether it earns its keep is not the pipeline configuration but the CLAUDE.md next to it: a job with clear conventions produces MRs you merge, and a job without them produces MRs you close. Get that file right before you tune anything in the YAML.
Sources & further reading