Claude Code GitLab CI Code Review Guide 2026: Self-Hosted MR Reviews

Claude Code GitLab CI code review pipeline illustration

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:

  1. GitLab CI runs only on merge requests.
  2. A script collects the MR diff, excluding generated files and lockfiles.
  3. Claude reviews the diff against a short review policy.
  4. The job posts a single GitLab MR note.
  5. 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:

VariableExampleWhy it exists
AI_API_KEYsk-...API key for your model provider or gateway
AI_BASE_URLhttps://api.kissapi.ai/v1OpenAI-compatible base URL
AI_REVIEW_MODELclaude-sonnet-4-6Review model
GITLAB_BOT_TOKENglpat-...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:

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 typeRecommended behavior
Docs onlySkip or use a cheap proofreading model
Generated client updateSkip generated files, review hand-written wrapper code
Migration touching payments/usersRun stronger model and require human approval
Huge refactorTruncate 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.