# How to implement Cursor Agent CLI into GitLab for code review

Published 2025-02-02 · Architecture · 13 min read · https://www.marcellefter.com/log/cursor-code-review

> Step-by-step guide to automated AI-powered code review in GitLab CI using Cursor Agent CLI: setup, prompt design, GitLab API, and troubleshooting.

This article walks you through implementing automated code review in GitLab CI using Cursor Agent CLI. The goal is to share a repeatable, technical approach so other developers can get AI-powered MR comments (inline and summary) without changing their existing pipeline structure.

**Audience:** Front-end and full-stack developers, DevOps engineers using GitLab CI.

---

## Prerequisites: Cursor CLI and headless mode

Cursor Agent CLI lets you run the same AI agent from the terminal or in CI. For pipelines you need **non-interactive (headless) mode** so the job can pass a prompt and get a response without user input.

- **Install (macOS, Linux, WSL):**  
  `curl https://cursor.com/install -fsS | bash`

- **Non-interactive run:**  
  Use `-p` / `--print` with your prompt and `--output-format text` so the agent prints the final response to stdout. Example:  
  `cursor-agent --force --model auto --output-format=text --print "Your prompt here"`

In CI, the binary may be named `cursor-agent` (or `agent` after install). This project looks for `cursor-agent` in `~/.cursor/bin` and `~/.local/bin` and adds those to `PATH` after install.

**References:**

- [Cursor CLI Overview](https://cursor.com/docs/cli/overview)
- [Using Agent in CLI](https://cursor.com/docs/cli/using) (Non-interactive mode)

---

## What you need in GitLab

| Variable                   | Required    | Purpose                                                                                                                                                                                                     |
| -------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CURSOR_API_KEY`           | Yes         | Authenticates Cursor CLI in CI. Add as a **masked** CI/CD variable.                                                                                                                                         |
| `AI_CODE_REVIEW_JOB_TOKEN` | Recommended | Personal or project access token with `api` scope so the agent can **post** MR comments. If unset, the job uses `CI_JOB_TOKEN`, which has limited permissions and may not allow creating notes/discussions. |

**Where to set:** Project → **Settings** → **CI/CD** → **Variables**. Mark secrets as **masked** (and **protected** if you only want them on protected branches).

---

## How it fits in the pipeline

In this setup, code review runs in the same stage as other code-quality jobs (e.g. ESLint). The flow is:

1. A merge request is opened or updated.
2. GitLab runs the pipeline; the `code-check` stage runs.
3. The `code-review` job: installs dependencies and Cursor CLI, sets env vars, builds a single prompt with MR context and procedures, runs `cursor-agent --print "..."`.
4. The agent (following the prompt) calls the GitLab API to fetch MR discussions, MR details, and MR changes (diff), analyzes the diff, then posts inline comments (and optionally a summary note).
5. If **blocking** mode is on and the agent reported critical/security issues, a follow-up script fails the job.

**Pipeline flow (simplified):**

```mermaid
flowchart LR
  MR[MROpened]
  Pipeline[Pipeline]
  CodeCheck[code-check stage]
  Eslint[eslint job]
  CodeReview[code-review job]
  Install[Cursor install]
  Run[Cursor run]
  API[GitLab API]
  Comments[Post comments]

  MR --> Pipeline
  Pipeline --> CodeCheck
  CodeCheck --> Eslint
  CodeCheck --> CodeReview
  CodeReview --> Install
  Install --> Run
  Run --> API
  API --> Comments
```

**Sequence: code-review job and GitLab API:**

```mermaid
sequenceDiagram
  participant Dev as Developer
  participant GitLab as GitLab
  participant Runner as Runner
  participant Agent as Cursor_Agent

  Dev->>GitLab: Push / MR update
  GitLab->>Runner: Trigger pipeline
  Runner->>Runner: Install deps and Cursor CLI
  Runner->>Runner: Set env vars
  Runner->>Agent: Run cursor-agent with prompt
  Agent->>GitLab: GET discussions, MR, changes
  GitLab->>Agent: MR metadata and diff
  Agent->>Agent: Analyze diff
  Agent->>GitLab: POST discussions and notes
  GitLab->>Dev: Inline comments on MR
```

---

## Step-by-step implementation

### Job skeleton (code-review job definition)

The code-review job lives in its own file and is included from the main pipeline. Here is the top-level structure from `deployment/ci/code-review.yml`:

```yaml
# deployment/ci/code-review.yml
code-review:
  stage: code-check
  image: ubuntu:latest
  variables:
    MODEL: ${MODEL:auto}
    BLOCKING_REVIEW: ${BLOCKING_REVIEW:-false}
  script:
    - |
      # ... install, env, prompt, cursor-agent ...
    - |
      # ... optional blocking check ...
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
      exists:
        - .gitlab-ci.yml
```

### Step 1: Add a dedicated CI file and include it

Create a file for the code-review job (e.g. `deployment/ci/code-review.yml`) and include it from the main pipeline so the job runs only when you want (e.g. on merge requests).

In `.gitlab-ci.yml`:

```yaml
include:
  - local: 'deployment/ci/code-review.yml'
```

Define the job in `deployment/ci/code-review.yml` under a stage that already exists (e.g. `code-check`).

### Step 2: Choose image and install Cursor CLI

Use an image that can run bash and curl (e.g. `ubuntu:latest`). In the job script, install dependencies and Cursor CLI, then resolve the `cursor-agent` binary:

```bash
# Install system dependencies
apt-get update && apt-get install -y curl git jq bash

# Configure git identity (for any git ops in the prompt)
git config --global user.name "Cursor Agent"
git config --global user.email "cursoragent@cursor.com"

# Install Cursor CLI
echo "Installing Cursor CLI..."
curl https://cursor.com/install -fsS | bash

# Add to PATH immediately
export PATH="$HOME/.cursor/bin:$HOME/.local/bin:$PATH"

# Resolve cursor-agent path (check both common locations)
CURSOR_AGENT_PATH=""
if [ -f "$HOME/.cursor/bin/cursor-agent" ]; then
  CURSOR_AGENT_PATH="$HOME/.cursor/bin/cursor-agent"
  chmod +x "$CURSOR_AGENT_PATH" || true
elif [ -f "$HOME/.local/bin/cursor-agent" ]; then
  CURSOR_AGENT_PATH="$HOME/.local/bin/cursor-agent"
  chmod +x "$CURSOR_AGENT_PATH" || true
else
  CURSOR_AGENT_PATH=$(command -v cursor-agent || echo "")
fi

if [ -z "$CURSOR_AGENT_PATH" ] || [ ! -f "$CURSOR_AGENT_PATH" ]; then
  echo "Error: cursor-agent not found after installation"
  exit 1
fi
echo "Using cursor-agent at: $CURSOR_AGENT_PATH"
```

Optionally set `git config user.name` and `user.email` so any git operations in the prompt have an identity.

### Step 3: Set environment variables

The agent needs Cursor auth, optional tuning, GitLab context, and a token for the GitLab API. Example from the job:

```bash
# Cursor: required for CLI
if [ -z "$CURSOR_API_KEY" ]; then
  echo "Error: CURSOR_API_KEY environment variable is not set"
  exit 1
fi
export CURSOR_API_KEY

# Optional tuning
export MODEL
export BLOCKING_REVIEW

# GitLab context (predefined in CI)
export CI_PROJECT_ID
export CI_API_V4_URL
export CI_MERGE_REQUEST_IID
export CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
export CI_MERGE_REQUEST_TARGET_BRANCH_NAME
export CI_MERGE_REQUEST_SOURCE_BRANCH_SHA
export CI_MERGE_REQUEST_TARGET_BRANCH_SHA

# GitLab API auth: prefer AI_CODE_REVIEW_JOB_TOKEN for posting comments
if [ -n "${AI_CODE_REVIEW_JOB_TOKEN:-}" ]; then
  export GITLAB_API_TOKEN="$AI_CODE_REVIEW_JOB_TOKEN"
  export GITLAB_AUTH_HEADER="PRIVATE-TOKEN"
  echo "Using AI_CODE_REVIEW_JOB_TOKEN for API authentication"
else
  export GITLAB_API_TOKEN="$CI_JOB_TOKEN"
  export GITLAB_AUTH_HEADER="JOB-TOKEN"
  echo "Using CI_JOB_TOKEN (limited permissions; consider AI_CODE_REVIEW_JOB_TOKEN for comments)"
fi
```

The prompt then references `$GITLAB_API_TOKEN` and `$GITLAB_AUTH_HEADER` in curl commands so the agent can call the API.

### Step 4: Build the prompt

The prompt should be **self-contained and procedural** so the agent actually performs the steps. Include:

1. **Context:** Repo path, project ID, API URL, MR IID, source/target branches, head/base SHA, and whether review is blocking.
2. **Objectives:**
   - Re-check existing discussions (e.g. resolve or mark superseded).
   - Review the MR diff and only flag clear, high-severity issues.
   - Post short inline comments (1–2 sentences) on the changed lines and optionally a brief summary note.
3. **Procedure (steps the agent must execute):**
   - GET existing discussions (e.g. `GET .../merge_requests/:id/discussions`).
   - GET MR details to obtain `diff_refs` (base_sha, head_sha, start_sha).
   - GET MR changes (e.g. `GET .../merge_requests/:id/changes`) and parse the diff (file path, line numbers; `+` lines map to new_line, `-` to old_line).
   - For each finding: file path, line in new file (new_line), and whether the file is new (old_path null).
   - Build the **position** object for inline comments: base_sha, start_sha, head_sha, old_path, new_path, old_line, new_line.
4. **Scope of review:** Limit to high-impact issues (e.g. null/undefined dereferences, resource leaks, injection, concurrency, missing error handling, clear logic errors, performance anti-patterns, security). Cap the number of inline comments (e.g. 10) and avoid duplicate feedback.
5. **Commenting rules:** One issue per comment; comments must be inline (with position). Use a simple severity convention (e.g. emojis or tags) so a later script can detect critical/security issues if you use blocking mode.
6. **Submission:**
   - If there are no issues and no prior “no issues” comment, POST a single MR note (e.g. “No issues found”).
   - If there are issues, for each one POST a discussion with `position` (or the appropriate GitLab API) so comments appear on the correct line. Optionally POST a summary note.  
     Include the exact curl examples (with placeholders for token, project id, MR id, body, position) so the agent can run them.

In this project the prompt is stored in a **heredoc** in the YAML and passed to `cursor-agent`. Key parts of the prompt (context, objectives, procedure, submission) look like this:

```bash
read -r -d '' PROMPT_TEXT_TMP <<'PROMPT_EOF' || true
You are operating in a GitLab CI/CD runner performing automated code review. The GitLab API is available and authenticated via GITLAB_API_TOKEN (using $GITLAB_AUTH_HEADER header).

Context:
- Repo: $CI_PROJECT_PATH
- Project ID: $CI_PROJECT_ID
- API URL: $CI_API_V4_URL
- MR IID: $CI_MERGE_REQUEST_IID
- MR Source Branch: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
- MR Target Branch: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
- MR Head SHA: $CI_MERGE_REQUEST_SOURCE_BRANCH_SHA
- MR Base SHA: $CI_MERGE_REQUEST_TARGET_BRANCH_SHA
- Blocking Review: $BLOCKING_REVIEW

Objectives:
1) Re-check existing review comments and reply resolved when addressed.
2) Review the current MR diff and flag only clear, high-severity issues.
3) Leave very short inline comments (1-2 sentences) on changed lines only and a brief summary at the end.

Procedure (EXECUTE THESE COMMANDS):
- Step 1: Get existing discussions: EXISTING_DISCUSSIONS=$(curl -s --header "$GITLAB_AUTH_HEADER: $GITLAB_API_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/discussions")
- Step 2: Get MR details for SHA: MR_DATA=$(curl -s --header "$GITLAB_AUTH_HEADER: $GITLAB_API_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID") && BASE_SHA=$(echo "$MR_DATA" | jq -r '.diff_refs.base_sha') && HEAD_SHA=$(echo "$MR_DATA" | jq -r '.diff_refs.head_sha') && START_SHA=$(echo "$MR_DATA" | jq -r '.diff_refs.start_sha')
- Step 3: Get MR changes/diff: MR_CHANGES=$(curl -s --header "$GITLAB_AUTH_HEADER: $GITLAB_API_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/changes")
- Step 4: Parse the diff (old_path, new_path, diff; + lines = new_line, - lines = old_line). For each issue: file path, new_line, and whether file is new (old_path null).
- Step 5: Build position for inline comments: base_sha, start_sha, head_sha, old_path, new_path, old_line, new_line.

Analyze ONLY for: null/undefined dereferences, resource leaks, injection (SQL/XSS), concurrency issues, missing error handling, obvious logic errors, performance anti-patterns, security vulnerabilities. Max 10 inline comments. Use emojis: Critical Security Performance Logic Resolved Improvement.

Submission:
- If NO issues and no prior "no issues" comment: curl --request POST --header "$GITLAB_AUTH_HEADER: $GITLAB_API_TOKEN" --header "Content-Type: application/json" --data "{\"body\":\"✅ No issues found\"}" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes"
- If issues: for EACH issue run (replace <comment>, <file_path>, <line_number>): curl --request POST --header "$GITLAB_AUTH_HEADER: $GITLAB_API_TOKEN" --header "Content-Type: application/json" --data "{\"body\":\"<comment>\",\"position\":{\"base_sha\":\"$BASE_SHA\",\"start_sha\":\"$START_SHA\",\"head_sha\":\"$HEAD_SHA\",\"old_path\":\"<file_path>\",\"new_path\":\"<file_path>\",\"old_line\":null,\"new_line\":<line_number>}}" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/discussions"

Blocking: If BLOCKING_REVIEW is true and any Critical or Security issues were posted: echo "true" > /tmp/critical_issues_found.txt ; else echo "false" > /tmp/critical_issues_found.txt
PROMPT_EOF

# Strip leading spaces from YAML indentation
PROMPT_TEXT=$(echo "$PROMPT_TEXT_TMP" | sed 's/^      //')
```

You can strip leading spaces (e.g. from YAML indentation) before passing the prompt to the agent.

### Step 5: Run the agent

Invoke the CLI in non-interactive mode with the prepared prompt. Use the resolved path to the binary so the job does not depend on PATH order:

```bash
# Pass the prompt to cursor-agent (non-interactive / headless)
"$CURSOR_AGENT_PATH" --force --model "$MODEL" --output-format=text --print "$PROMPT_TEXT"
```

In the full job this runs inside the same script block as the heredoc and env setup; the agent receives the full prompt and can execute the curl commands described in it.

### Step 6: Blocking behavior (optional)

If `BLOCKING_REVIEW` is true and you want the pipeline to fail when the agent reports critical/security issues:

1. In the prompt, instruct the agent to write a flag file (e.g. `/tmp/critical_issues_found.txt`) with content `true` when it posts any critical/security comment, and `false` otherwise.
2. In a **second** script block in the same job, read that file; if the content is `true`, exit with status 1 so the job fails.

Example of the second script block (runs after the agent):

```bash
if [ "$BLOCKING_REVIEW" = "true" ]; then
  echo "Checking for critical issues..."
  CRITICAL_ISSUES_FOUND=$(cat /tmp/critical_issues_found.txt 2>/dev/null || echo "false")
  if [ "$CRITICAL_ISSUES_FOUND" = "true" ]; then
    echo "❌ Critical issues found and blocking review is enabled. Failing the workflow."
    exit 1
  else
    echo "✅ No blocking issues found."
  fi
fi
```

### Step 7: Job rules and failure policy

- **When to run:** Use rules so the job runs only on merge request pipelines, e.g.  
  `if: $CI_PIPELINE_SOURCE == 'merge_request_event'` and optionally `exists: [.gitlab-ci.yml]`.
- **allow_failure:** Set `allow_failure: true` unless you want a failed code-review (or a failed blocking check) to block merging by default. When blocking is enabled, the second script already fails the job; `allow_failure` then controls whether that failure blocks the pipeline.

---

## GitLab API used

The agent uses the GitLab REST API with the token you provided. Typical endpoints:

| Action                  | Method | Endpoint                                                                                      |
| ----------------------- | ------ | --------------------------------------------------------------------------------------------- |
| List discussions        | GET    | `/projects/:id/merge_requests/:merge_request_iid/discussions`                                 |
| Get MR                  | GET    | `/projects/:id/merge_requests/:merge_request_iid`                                             |
| Get MR changes (diff)   | GET    | `/projects/:id/merge_requests/:merge_request_iid/changes`                                     |
| Add MR note (top-level) | POST   | `/projects/:id/merge_requests/:merge_request_iid/notes`                                       |
| Add discussion (inline) | POST   | `/projects/:id/merge_requests/:merge_request_iid/discussions` with body containing `position` |

**Authentication:** Send the token in a header: `JOB-TOKEN: <CI_JOB_TOKEN>` or `PRIVATE-TOKEN: <AI_CODE_REVIEW_JOB_TOKEN>`.

**Inline comment position:** For comments on a specific line, the body must include a `position` object with: `base_sha`, `start_sha`, `head_sha`, `old_path`, `new_path`, `old_line`, `new_line`. These come from the MR `diff_refs` and the diff parsing. See [GitLab API – Merge request discussions](https://docs.gitlab.com/ee/api/discussions.html).

**Example: POST inline comment (single discussion):**

```bash
# After fetching BASE_SHA, START_SHA, HEAD_SHA from MR diff_refs
curl --request POST \
  --header "PRIVATE-TOKEN: $GITLAB_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "body": "🚨 Critical: possible null dereference here; add a null check before use.",
    "position": {
      "base_sha": "'"$BASE_SHA"'",
      "start_sha": "'"$START_SHA"'",
      "head_sha": "'"$HEAD_SHA"'",
      "old_path": "src/utils/parser.ts",
      "new_path": "src/utils/parser.ts",
      "old_line": null,
      "new_line": 42
    }
  }' \
  "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/discussions"
```

**Example: POST top-level “no issues” note:**

```bash
curl --request POST \
  --header "PRIVATE-TOKEN: $GITLAB_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{"body":"✅ No issues found"}' \
  "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes"
```

---

## Prompt engineering tips

- **Procedural and explicit:** Phrase the prompt as “execute these steps” and include concrete curl examples so the agent performs the HTTP calls instead of only describing them.
- **Limit scope:** Ask only for high-severity issues and cap the number of comments to reduce noise and token usage.
- **Avoid duplicates:** Tell the agent to check existing discussions and skip posting if the same (or similar) feedback already exists on the same lines.
- **Supersede previous “all clear”:** If the agent previously posted “No issues” but the new run finds issues, instruct it to delete or edit that earlier note, or reply that it’s superseded, so reviewers see the current state.

---

## Options and improvements

- **MODEL:** Use the `MODEL` variable to select a model (e.g. `auto` or a specific model name) for the CLI.
- **BLOCKING_REVIEW:** Use rules or variables to enable blocking only on certain branches (e.g. main or protected branches).
- **Project rules:** If the runner has access to the repo (e.g. in a non-ephemeral runner), the agent can load `.cursor/rules` or similar; in typical ephemeral CI the prompt is self-contained in the YAML.
- **Security:** Keep `CURSOR_API_KEY` and GitLab tokens as masked (and optionally protected) variables; never log or echo them.

---

## Troubleshooting

| Problem                  | What to check                                                                                                                                 |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `cursor-agent` not found | Ensure PATH includes `~/.cursor/bin` and `~/.local/bin` after install; verify the install script and binary name (`cursor-agent` vs `agent`). |
| 401 from GitLab API      | Use a token with enough scope (e.g. `AI_CODE_REVIEW_JOB_TOKEN` with `api` scope) and the correct header (`PRIVATE-TOKEN` or `JOB-TOKEN`).     |
| CURSOR_API_KEY missing   | Add it in Project → Settings → CI/CD → Variables; ensure the variable is not only defined for a different branch/protected state.             |
| Comments not inline      | Ensure each POST uses a `position` with all required fields and that base_sha/head_sha match the MR’s current `diff_refs`.                    |

---

## References

- **Cursor:** [CLI Overview](https://cursor.com/docs/cli/overview), [Installation](https://cursor.com/docs/cli/installation), [Using Agent in CLI](https://cursor.com/docs/cli/using) (non-interactive mode).
- **GitLab:** [Merge request discussions API](https://docs.gitlab.com/ee/api/discussions.html), [Merge request notes](https://docs.gitlab.com/ee/api/notes.html).

**Example in this repo:**

- [.gitlab-ci.yml](.gitlab-ci.yml) – includes `deployment/ci/code-review.yml` and defines the `code-check` stage.
- [deployment/ci/code-review.yml](deployment/ci/code-review.yml) – full code-review job: install, env, prompt heredoc, `cursor-agent` invocation, and optional blocking script.
