# Run a Cursor SDK agent in CI on a self-hosted runner

Source: https://docs.quake.ai/resources/deployments/ai-agent-in-ci
Markdown: https://docs.quake.ai/resources/deployments/ai-agent-in-ci.md

---

# Run a Cursor SDK agent in CI on a self-hosted runner

Stand up a CI job that invokes a Cursor SDK agent on a self-hosted runner on Quake AI. The job runs on pull requests, reviews the checked-out diff, and posts the result as a pull request comment. You supply the Cursor API key as a CI secret and the runner from [Deploy a self-hosted CI runner](/resources/deployments/deploy-ci-runner).

<PricingCompanion
  components={[
    { kind: "primitive", required: true, label: "Self-hosted runner instance", vm: { flavor: "s1a.small" } },
  ]}
/>

<Figure size="md" caption="Pull-request workflow on a self-hosted runner: a one-shot Cursor SDK agent reviews the diff and posts a PR comment">

```d2
direction: right

repo: "GitHub or GitLab\nrepository" {shape: cylinder}

vm: Quake AI VM {
  runner: "Self-hosted runner\n(from deploy-ci-runner)"
  job: "CI job:\nagent review step"
  runner -> job: pull request event
}

cursor: Cursor API {shape: cylinder}

repo -> vm.runner: opens or updates PR
vm.job -> cursor: Agent.prompt(diff review)
cursor -> vm.job: review text
vm.job -> repo: PR comment
```

</Figure>



This deployment runs on infrastructure you already operate. You supply a Cursor API key from your own Cursor account or team service account, and your CI provider supplies the secret store. Quake AI has no role in authenticating the agent or storing the key.



## Prerequisites

You need:

- A self-hosted runner already registered against your repository, see [Deploy a self-hosted CI runner](/resources/deployments/deploy-ci-runner). The examples below assume a GitHub Actions runner labeled `quake-ai`; the GitLab Runner equivalent is the same shape with `tags` instead of `runs-on`.
- A Cursor API key. User keys live at [Cursor Dashboard → Integrations](https://cursor.com/dashboard/integrations); team service-account keys live in Team Settings → Service accounts.
- Node.js available in the workflow, either preinstalled on the runner or added with a setup step.
- Admin access to the repository to add a CI secret.

## Step 1: Store the Cursor API key as a CI secret

Add the key to your repository's secret store. Never commit it to a workflow file or to the runner's filesystem outside the CI secret mechanism.

For GitHub Actions, go to your repository's **Settings** > **Secrets and variables** > **Actions** > **New repository secret**, name it `CURSOR_API_KEY`, and paste the key.

For GitLab CI/CD, go to **Settings** > **CI/CD** > **Variables** > **Add variable**, name it `CURSOR_API_KEY`, and mark it **Masked** and **Protected**.

## Step 2: Add the agent review script

Add `scripts/agent-pr-review.mjs` to the repository:

```javascript

const baseRef = process.env.PR_BASE_REF ?? "main";

const prompt = [
  `Run \`git diff origin/${baseRef}...HEAD\` in this repository.`,
  "Write a concise code-review comment: summarize the change, flag",
  "correctness or security risks you see, and note missing tests.",
  "Output only the comment text, no preamble.",
].join(" ");

try {
  const result = await Agent.prompt(prompt, {
    apiKey: process.env.CURSOR_API_KEY,
    model: { id: "composer-2.5" },
    local: { cwd: process.cwd() },
  });
  if (result.status === "error") {
    console.error(`run failed: ${result.id}`);
    process.exit(2);
  }
  process.stdout.write(result.result ?? "");
} catch (err) {
  if (err instanceof CursorAgentError) {
    console.error(`startup failed: ${err.message}, retryable=${err.isRetryable}`);
    process.exit(1);
  }
  throw err;
}
```

The script uses the one-shot `Agent.prompt(...)` pattern: no streaming, no follow-up turns, the process exits once the agent finishes. The agent runs locally against the runner's checked-out repository (`local: { cwd: process.cwd() }`), so it reads the diff itself with its own tool calls rather than the script shelling out to `git diff`. A thrown `CursorAgentError` means the run never started (exit code 1); `result.status === "error"` means the run started and failed midway (exit code 2). See the [Cursor SDK skill](https://cursor.com/docs/sdk/typescript) for the full pattern reference.

## Step 3: Add the CI job

For a GitHub Actions workflow at `.github/workflows/agent-review.yml`:

```yaml
name: Agent PR review

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: [self-hosted, quake-ai]
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: "22"

      - run: npm install @cursor/sdk

      - name: Run agent review
        id: agent
        env:
          CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
          PR_BASE_REF: ${{ github.base_ref }}
        run: node scripts/agent-pr-review.mjs > agent-review.md

      - uses: peter-evans/create-or-update-comment@v4
        with:
          issue-number: ${{ github.event.pull_request.number }}
          body-path: agent-review.md
```

`fetch-depth: 0` is required: the agent's `git diff` call needs the base branch's history, which a shallow checkout does not include. The `create-or-update-comment` step posts the captured output as a PR comment using the workflow's built-in `GITHUB_TOKEN` permissions; it needs no extra secret.

For GitLab CI/CD, the equivalent job uses `tags` instead of `runs-on` and the GitLab API to post a merge request note:

```yaml
agent-review:
  stage: review
  tags:
    - quake-ai
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script:
    - npm install @cursor/sdk
    - node scripts/agent-pr-review.mjs > agent-review.md
    - |
      curl --request POST \
        --header "PRIVATE-TOKEN: $GITLAB_API_TOKEN" \
        --form "body=<agent-review.md" \
        "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes"
  variables:
    PR_BASE_REF: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
```

`GITLAB_API_TOKEN` is a separate masked, protected CI/CD variable holding a project or personal access token with API scope; GitLab does not expose an equivalent of GitHub's automatic `GITHUB_TOKEN` for posting notes from a job.

## Step 4: Verify

Open a test pull request against the repository. Confirm the workflow runs on the self-hosted runner and check the job logs from your CI provider's pipeline UI.

A successful run ends with the comment posted on the pull request. If the job exits with code 1, the run never started; check that `CURSOR_API_KEY` is set and the runner has outbound network access to reach the Cursor API. If it exits with code 2, the run started and the agent's transcript failed midway; rerun with the run ID logged by the script for further investigation.

## Scope

This is a CI job pattern on infrastructure you operate, not a hosted review product. You are responsible for the runner's uptime, the CI secret's rotation, and any cost the Cursor API key's plan accrues per run. Keep each job's prompt narrow and reproducible so reviewers can predict what the agent does on every run.

## Next steps

- [Deploy a self-hosted CI runner](/resources/deployments/deploy-ci-runner): the runner this deployment composes on
- [AI-native development](/resources/solutions/ai-native-development): the broader pattern of pointing AI tools at Quake AI Developer and running the result on Quake AI compute
- [AI tools reference](/resources/ai-assisted-development/ai-tools-reference): MCP tool inputs and response shapes if you also want the agent to query the Quake AI knowledge graph
- [How to store application secrets and inject them at runtime](/docs/security/how-to/inject-app-secrets): the same secret-handling pattern for credentials your CI job's deploy steps need

## Clean up

Remove the workflow file (`.github/workflows/agent-review.yml` or the GitLab `agent-review` job) and the `scripts/agent-pr-review.mjs` script from the repository. Delete the `CURSOR_API_KEY` secret from your CI provider's secret store, and revoke the key from [Cursor Dashboard → Integrations](https://cursor.com/dashboard/integrations) if you created it solely for this deployment. The self-hosted runner itself is unaffected; follow [Deploy a self-hosted CI runner](/resources/deployments/deploy-ci-runner)'s clean-up section if you also want to deregister it.
