Languages: EN RU

GitLab CI/CD and Headless Automation

If the previous article had a ready-made claude-code-action — a wrapper you simply add to a YAML file and everything works — GitLab is a different story. There is no pre-built "action" here, and that is actually an advantage: you work directly with the CLI via headless mode, giving you full control over every invocation.

Status: The official Claude Code integration with GitLab CI/CD is in beta and is maintained by the GitLab team. The current tracker is at gitlab.com/gitlab-org/gitlab/-/issues/573776.

Headless Mode: What Happens Under the Hood

The -p flag (or --print) turns Claude from an interactive assistant into a command-line tool: run it → the agent loop executes → it exits with code 0 or non-zero. That is headless mode.

# Simplest invocation: read and analyze
claude -p "Найди потенциальные баги в src/auth/" --allowedTools "Read,Bash"

# With JSON output — easy to parse in CI
claude -p "Проверь code style" --output-format json | jq -r '.result'

# Pass data via pipe
git diff origin/main | claude -p "Сделай ревью этого диффа" --output-format text

Three flags matter most in a pipeline:

  • --max-turns N — limit the number of steps in the agent loop; otherwise the agent may run indefinitely
  • --max-budget-usd N — a hard cost ceiling per invocation (available in print mode only)
  • --permission-mode acceptEdits — allow file writes without confirmation, required for automated edits
sequenceDiagram participant U as User / Trigger participant GL as GitLab CI/CD participant R as Runner (container) participant C as claude -p participant AG as Agent loop participant Repo as Repository U->>GL: Event (MR / schedule / web / @claude) GL->>R: Job start: node:24-alpine3.21 R->>R: apk add git curl bash R->>R: curl install.sh | bash R->>C: claude -p "$AI_FLOW_INPUT" --allowedTools ... loop Agent loop C->>AG: Step planning AG->>Repo: Read / Edit / Bash Repo-->>AG: Tool result AG->>C: Observation → next step end C-->>R: Output (text / json) R->>Repo: git commit + push Repo->>GL: New MR or comment
sequenceDiagram
    participant U as User / Trigger
    participant GL as GitLab CI/CD
    participant R as Runner (container)
    participant C as claude -p
    participant AG as Agent loop
    participant Repo as Repository

    U->>GL: Event (MR / schedule / web / @claude)
    GL->>R: Job start: node:24-alpine3.21
    R->>R: apk add git curl bash
    R->>R: curl install.sh | bash
    R->>C: claude -p "$AI_FLOW_INPUT" --allowedTools ...
    loop Agent loop
        C->>AG: Step planning
        AG->>Repo: Read / Edit / Bash
        Repo-->>AG: Tool result
        AG->>C: Observation → next step
    end
    C-->>R: Output (text / json)
    R->>Repo: git commit + push
    Repo->>GL: New MR or comment
The lifecycle of a Claude Code invocation in GitLab CI/CD: from event to result in the repository

Basic Setup in .gitlab-ci.yml

GitLab CI/CD cannot "download an action" — you install Claude yourself in before_script. The recommended base template:

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
  script:
    # GitLab MCP Server (if configured) — provides access to the GitLab API
    - /bin/gitlab-mcp-server || true
    - >
      claude
      -p "${AI_FLOW_INPUT:-'Проанализируй изменения и предложи правки'}"
      --permission-mode acceptEdits
      --allowedTools "Bash Read Edit Write mcp__gitlab"
      --max-turns 10
      --max-budget-usd 3.00

Authentication. Go to Settings → CI/CD → Variables, add ANTHROPIC_API_KEY with the "Masked" and "Protected" flags. Claude Code picks up the variable automatically — no explicit --api-key is needed.

Image. node:24-alpine3.21 is the officially recommended image. Alpine requires explicit installation of git and bash via apk; otherwise the agent will fail when it tries to work with the repository.

Check yourself
In the `before_script` of the `node:24-alpine3.21` image, `curl` was added and Claude was installed. The pipeline was triggered — the agent crashed with an error when attempting to read repository files. What was forgotten to install?

AI_FLOW_* Variables: Context from Threads

When a pipeline is triggered via a webhook (for example, someone wrote @claude in an MR comment), context is passed through special variables:

  • AI_FLOW_INPUT — the request text (what the user wrote)
  • AI_FLOW_CONTEXT — the MR, issue, or thread identifier
  • AI_FLOW_EVENT — the event type (note, merge_request, etc.)

In the script block this looks like:

# Log context for debugging
echo "Запрос: $AI_FLOW_INPUT"
echo "Контекст: $AI_FLOW_CONTEXT, событие: $AI_FLOW_EVENT"

claude -p "$AI_FLOW_INPUT" \
  --allowedTools "Bash Read Edit Write mcp__gitlab" \
  --permission-mode acceptEdits

If AI_FLOW_INPUT is not set (manual run via the UI), use a bash default substitution: ${AI_FLOW_INPUT:-'Default task'}.

Three Automation Patterns

Pattern 1: Automatic review when an MR is opened.

Triggered on the merge_request_event event, passes diffs via stdin:

mr-review:
  stage: ai
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: always
  image: node:24-alpine3.21
  before_script:
    - apk add --no-cache git curl bash
    - curl -fsSL https://claude.ai/install.sh | bash
  script:
    - >
      git diff origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}...HEAD
      | claude -p
      "Это diff Merge Request. Найди баги, проблемы безопасности и
       нарушения code style. Прокомментируй конкретные строки."
      --output-format json
      --max-turns 5
      --max-budget-usd 2.00
      | tee review.json
    - jq -r '.result' review.json
  artifacts:
    paths: [review.json]
    expire_in: 7 days

Pattern 2: Scheduled codebase maintenance.

Once a week — scan for outdated TODOs and potential improvements:

weekly-audit:
  stage: ai
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
  image: node:24-alpine3.21
  before_script:
    - apk add --no-cache git curl bash
    - curl -fsSL https://claude.ai/install.sh | bash
  script:
    - >
      claude --bare
      -p "Найди все TODO-комментарии старше месяца, проанализируй
          их актуальность и создай список рекомендаций."
      --allowedTools "Read,Bash"
      --output-format json
      --max-turns 8
      --no-session-persistence
      | jq -r '.result' > weekly-report.md
  artifacts:
    paths: [weekly-report.md]

Pattern 3: Automated fixes on demand.

Via a web trigger with AI_FLOW_INPUT passed in:

auto-fix:
  stage: ai
  rules:
    - if: '$CI_PIPELINE_SOURCE == "web"'
  image: node:24-alpine3.21
  before_script:
    - apk add --no-cache git curl bash jq
    - curl -fsSL https://claude.ai/install.sh | bash
    # Configure git for agent commits
    - git config user.email "claude-bot@example.com"
    - git config user.name "Claude Bot"
  script:
    - >
      claude
      -p "${AI_FLOW_INPUT}"
      --permission-mode acceptEdits
      --allowedTools "Bash Read Edit Write"
      --max-turns 15
      --max-budget-usd 5.00
    # Commit and push results
    - git add -A && git commit -m "fix: automated changes by Claude" || echo "No changes"
    - git push origin HEAD:${CI_COMMIT_REF_NAME}
Check yourself
You run a scheduled pipeline once a week to analyze the codebase. The agent only needs to read files — no writes are required. Which two flags would you add to speed up the run and prevent accidental writes?

--bare: Faster Startup in CI

By default, claude -p reads CLAUDE.md, hooks, plugins, and MCP servers from the working directory. In CI this is often unnecessary and slows down startup.

The --bare flag disables all auto-discovery: CLAUDE.md, hooks, plugins, MCP servers, and auto memory. Only what you explicitly passed via flags remains. The result: reproducible, fast runs — behavior does not depend on whatever happens to be in the repository.

# Without --bare: reads everything from .claude/
claude -p "задача" --allowedTools "Read"

# With --bare: only what is explicitly passed
claude --bare -p "задача" --allowedTools "Read" --append-system-prompt "Ты лаконичный ревьюер."

The official documentation notes: --bare will become the default for -p in future versions. It is recommended to use it explicitly now.

There is one caveat: --bare also disables reading CLAUDE.md. If project context is needed, pass it via --append-system-prompt-file ./CLAUDE.md.

Enterprise: Bedrock and Vertex AI

For enterprise environments with data residency requirements — Claude Code supports the same providers as GitHub Actions. Instead of ANTHROPIC_API_KEY, OIDC authentication is used:

  • Amazon Bedrock: GitLab OIDC → IAM role via aws sts assume-role-with-web-identity, temporary credentials in environment variables. Models with a regional prefix: us.anthropic.claude-sonnet-4-6.
  • Google Vertex AI: Workload Identity Federation, no stored keys. Variables GCP_WORKLOAD_IDENTITY_PROVIDER and GCP_SERVICE_ACCOUNT.

Both options require manual configuration — the one-liner quickstart does not work. Detailed YAML examples are available in the official GitLab documentation.

Practical Considerations

Cost. Unlike GitHub Actions with a fixed review cost, in GitLab you pay for runner minutes and API tokens separately. --max-budget-usd is your primary safeguard. Running on every push to an MR accumulates costs quickly; limit triggers via rules.

Parallel pipelines. Set interruptible: true at the job level so that a new push cancels the previous agent run:

claude:
  interruptible: true
  # ...

Logs. The --debug flag in CI is your best friend when troubleshooting. Also add --output-format json and save artifacts: the total_cost_usd field in the JSON response lets you track spending per run without opening the dashboard.

CLAUDE.md in the repository. Without --bare, the agent reads it and follows its rules. This is the primary way to pass coding standards, forbidden patterns, and project-specific context — just as when running locally. See CLAUDE.md and the Memory System for details.

Security. ANTHROPIC_API_KEY must always be a masked variable. Agent changes go through MRs, so the team sees every diff and branch protection rules apply. For risks around bypassPermissions in public repositories, see The Permission Model, Security, and Trust.


Quick recall
Что отключает флаг `--bare` при запуске Claude в CI?
Quick recall
Как передать ANTHROPIC_API_KEY в GitLab CI/CD без явного флага?
Quick recall
Что происходит, когда запускается `claude -p "задача"` в CI/CD-пайплайне?

See also

  • GitHub Actions and Automated Code Review — an alternative approach using a ready-made action for GitHub
  • Headless Mode and CLI Scripting — a complete reference for the -p flag and CLI automation
  • CLAUDE.md and the Memory System — how to provide project context that the agent reads in CI
  • The Permission Model, Security, and Trust — permission modes and security in automated environments
  • Claude Agent SDK: Building Agents Programmatically — when you need programmatic control on top of the CLI
  • Cloud Agents: Web, Routines, and Background Tasks — a CI/CD alternative for tasks that are not tied to a pipeline
  • Practice: GitHub, Databases, and Web APIs via MCP — using MCP tools inside pipeline invocations

Sources

  1. Claude Code GitLab CI/CD — официальная документация
  2. Run Claude Code programmatically (headless mode) — официальная документация
  3. Claude Code CLI reference
  4. Claude Code in CI/CD: GitHub Actions, GitLab CI, and Automation