Claude Code GitLab CI Code Review Guide 2026: Self-Hosted MR Reviews
GitLab teams usually hit the same wall with AI code review: the demo works, then the pipeline becomes noisy, expensive, and slow. Every merge request gets a full-repo essay. Developers stop reading it. Finance starts reading the API bill.
The better pattern is smaller: run Claude Code only on merge request diffs, give it the project rules it needs, post one focused review comment, and skip the pipeline when the change is too large or too boring. This guide walks through a practical GitLab CI setup you can run on GitLab.com or self-hosted GitLab.
What we're building
The workflow has five pieces:
- GitLab CI runs only on merge requests.
- A script collects the MR diff, excluding generated files and lockfiles.
- Claude reviews the diff against a short review policy.
- The job posts a single GitLab MR note.
- Token and file limits stop surprise bills.
I'm using the OpenAI-compatible chat API in the examples because it's easy to route through one endpoint. If your Claude Code installation expects Anthropic-style environment variables, keep the same structure and swap the CLI command. The core ideas don't change.
Assumption: You already have a GitLab project, CI runners, and a model/API key. If you use a gateway like KissAPI, you can point the job at one base URL and switch between Claude, GPT, or cheaper review models without rewriting the pipeline.
GitLab variables to add
In GitLab, go to Settings → CI/CD → Variables and add these masked variables:
| Variable | Example | Why it exists |
|---|---|---|
AI_API_KEY | sk-... | API key for your model provider or gateway |
AI_BASE_URL | https://api.kissapi.ai/v1 | OpenAI-compatible base URL |
AI_REVIEW_MODEL | claude-sonnet-4-6 | Review model |
GITLAB_BOT_TOKEN | glpat-... | Token with permission to post MR notes |
Use a project access token for GITLAB_BOT_TOKEN, not a personal token from a senior engineer's account. Give it the least access that can create merge request notes. Rotate it like any other production secret.
The CI pipeline
Create .gitlab-ci.yml:
stages:
- review
ai_code_review:
image: python:3.12-slim
stage: review
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
variables:
GIT_DEPTH: "0"
before_script:
- apt-get update && apt-get install -y git curl
- pip install openai requests
script:
- python scripts/ai_review_gitlab.py
allow_failure: true
allow_failure: true is intentional. Early AI review should advise, not block. Once the signal is good, you can add a stricter job for security issues only. Don't turn a chat model into a flaky release gate on day one.
The review script
Add scripts/ai_review_gitlab.py:
import os
import subprocess
import requests
from openai import OpenAI
MAX_DIFF_CHARS = 45000
EXCLUDED_SUFFIXES = (
"package-lock.json", "pnpm-lock.yaml", "yarn.lock",
".min.js", ".map", ".svg"
)
project_id = os.environ["CI_PROJECT_ID"]
mr_iid = os.environ["CI_MERGE_REQUEST_IID"]
api_url = os.environ["CI_API_V4_URL"]
gitlab_token = os.environ["GITLAB_BOT_TOKEN"]
client = OpenAI(
api_key=os.environ["AI_API_KEY"],
base_url=os.environ.get("AI_BASE_URL", "https://api.openai.com/v1")
)
model = os.environ.get("AI_REVIEW_MODEL", "claude-sonnet-4-6")
def sh(cmd):
return subprocess.check_output(cmd, shell=True, text=True, stderr=subprocess.STDOUT)
def changed_files():
out = sh("git diff --name-only origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD")
files = [x.strip() for x in out.splitlines() if x.strip()]
return [f for f in files if not f.endswith(EXCLUDED_SUFFIXES)]
def collect_diff(files):
if not files:
return ""
safe = " ".join(subprocess.list2cmdline([f]) for f in files)
diff = sh(f"git diff --unified=80 origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD -- {safe}")
if len(diff) > MAX_DIFF_CHARS:
return diff[:MAX_DIFF_CHARS] + "\n\n[Diff truncated for budget control]"
return diff
files = changed_files()
diff = collect_diff(files)
if not diff.strip():
print("No reviewable diff found.")
raise SystemExit(0)
prompt = f"""
You are reviewing a GitLab merge request. Be direct and practical.
Focus on:
- correctness bugs
- security issues
- data loss or migration risk
- race conditions and flaky tests
- API compatibility breaks
Do not comment on style unless it hides a real bug.
If the patch looks good, say so briefly.
Return Markdown with sections: Summary, Must fix, Should consider, Tests.
Changed files:
{chr(10).join(files)}
Diff:
{diff}
"""
response = client.chat.completions.create(
model=model,
temperature=0.2,
max_tokens=1800,
messages=[
{"role": "system", "content": "You are a senior software engineer doing code review."},
{"role": "user", "content": prompt},
],
)
body = response.choices[0].message.content
note = "## AI Code Review\n\n" + body + "\n\n---\n_Generated by the GitLab CI AI review job._"
r = requests.post(
f"{api_url}/projects/{project_id}/merge_requests/{mr_iid}/notes",
headers={"PRIVATE-TOKEN": gitlab_token},
json={"body": note},
timeout=30,
)
r.raise_for_status()
print("Posted AI review note")
This is intentionally plain Python. No hidden service, no database, no webhook server. If the job breaks, any engineer can read the script and fix it.
Cost controls that actually matter
The expensive mistake is sending too much context. A code review model doesn't need your whole repository for every MR. It needs the diff, the changed filenames, and maybe a short project policy file.
Use these guardrails:
- Cap diff size. The example cuts the diff at 45,000 characters. Tune this based on your average MR size.
- Exclude generated files. Lockfiles, maps, built assets, and vendored code waste tokens.
- Limit output.
max_tokens=1800is enough for a useful review. More tokens often means more rambling. - Run on MR events only. Don't review every push, schedule, and branch pipeline.
- Start with Sonnet-class models. Use Opus-level models for high-risk repos or manual re-review, not every typo fix.
If you route through KissAPI, one practical setup is to default MR review to a strong mid-tier Claude model, then reserve a pricier model for a manual /deep-review style job. That's usually better than making every pipeline pay flagship prices.
Adding a project review policy
Once the basic job works, create AI_REVIEW.md at the repo root:
# AI Review Policy
Prioritize:
1. Security regressions
2. Billing, quota, and payment logic bugs
3. Backward-incompatible API changes
4. Data migrations and destructive operations
5. Missing tests for risky branches
Ignore:
- Formatting-only comments
- Naming preferences
- Suggestions that require large rewrites without a bug
Then load it in the script:
policy = ""
if os.path.exists("AI_REVIEW.md"):
with open("AI_REVIEW.md", "r", encoding="utf-8") as f:
policy = f.read()[:6000]
prompt = f"""
Project review policy:
{policy}
Review this merge request diff:
{diff}
"""
A short policy file beats a giant prompt. It also gives humans a clean place to argue about standards without editing CI code.
Posting one comment vs inline comments
Inline comments look nicer, but they're more fragile. You need exact file paths, line positions, old/new line mapping, and special handling for force-pushes. For the first version, post one MR note. Developers care more about accuracy than decoration.
When to skip the review
| Change type | Recommended behavior |
|---|---|
| Docs only | Skip or use a cheap proofreading model |
| Generated client update | Skip generated files, review hand-written wrapper code |
| Migration touching payments/users | Run stronger model and require human approval |
| Huge refactor | Truncate diff and ask for risk summary, not line-by-line review |
Testing the setup locally
Before burning CI minutes, export the same variables locally and run the script on a feature branch:
export AI_API_KEY="sk-your-key"
export AI_BASE_URL="https://api.kissapi.ai/v1"
export AI_REVIEW_MODEL="claude-sonnet-4-6"
export CI_MERGE_REQUEST_TARGET_BRANCH_NAME="main"
python scripts/ai_review_gitlab.py
For local dry runs, comment out the final requests.post call and print the note. Read it like a reviewer. If it catches one real bug and avoids five fake ones, you're on the right track.
Common failure modes
The job says the target branch doesn't exist
Set GIT_DEPTH: "0" and make sure the runner fetches enough history. Shallow clones break three-dot diffs more often than people expect.
The review is too generic
Add a repo-specific AI_REVIEW.md. Tell the model what matters in your project: billing logic, tenant isolation, queue retries, schema migrations, mobile compatibility, whatever can actually hurt you.
The bill is too high
Lower MAX_DIFF_CHARS, exclude more files, use a cheaper model for normal MRs, and run expensive reviews only when a label like ai-deep-review is present.
The bot repeats old comments
For version two, search existing MR notes from the bot and update the latest one instead of posting a new note. The first version can be simple; the second should be tidy.
Run Claude, GPT, and More Through One API
KissAPI gives developers an OpenAI-compatible endpoint for Claude, GPT, and other models, with pay-as-you-go credits and simple endpoint switching for CI jobs.
Start Free →Final take
AI code review is useful when it's boring infrastructure, not a magical oracle. Keep the prompt short. Review the diff, not the universe. Post one practical comment. Measure whether humans act on it.
GitLab CI is a good place to start because the workflow is already there: MR opens, pipeline runs, reviewer reads. Add Claude Code or a Claude API review job carefully, and you'll catch real bugs without turning every merge request into a token bonfire.