How to implement Cursor Agent CLI into GitLab for code review

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:


What you need in GitLab

VariableRequiredPurpose
CURSOR_API_KEYYesAuthenticates Cursor CLI in CI. Add as a masked CI/CD variable.
AI_CODE_REVIEW_JOB_TOKENRecommendedPersonal 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):

Loading diagram…

Sequence: code-review job and GitLab API:

Loading diagram…

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
1# deployment/ci/code-review.yml 2code-review: 3 stage: code-check 4 image: ubuntu:latest 5 variables: 6 MODEL: ${MODEL:auto} 7 BLOCKING_REVIEW: ${BLOCKING_REVIEW:-false} 8 script: 9 - | 10 # ... install, env, prompt, cursor-agent ... 11 - | 12 # ... optional blocking check ... 13 allow_failure: true 14 rules: 15 - if: $CI_PIPELINE_SOURCE == 'merge_request_event' 16 exists: 17 - .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:

ActionMethodEndpoint
List discussionsGET/projects/:id/merge_requests/:merge_request_iid/discussions
Get MRGET/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.

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

ProblemWhat to check
cursor-agent not foundEnsure PATH includes ~/.cursor/bin and ~/.local/bin after install; verify the install script and binary name (cursor-agent vs agent).
401 from GitLab APIUse 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 missingAdd it in Project → Settings → CI/CD → Variables; ensure the variable is not only defined for a different branch/protected state.
Comments not inlineEnsure each POST uses a position with all required fields and that base_sha/head_sha match the MR’s current diff_refs.

References

Example in this repo:

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