Skip to main content

Command Palette

Search for a command to run...

Origin

Set up CloneKit in CI with origin repo clone-fast

origin repo clone-fast replaces git clone in CI jobs. Instead of cloning from scratch, it downloads a prebuilt clone kit, a packed snapshot of the repository that Cursor builds for each repository with CloneKit turned on, then fetches only the objects added since the kit was built. This page shows how to get a short-lived Origin token onto the runner and run the command in Buildkite, GitHub Actions, and other CI systems.

How it works

Each run of origin repo clone-fast {owner}/{repo} [DIR] does the following:

  1. Fetches the kit manifest from Origin with the token in CURSOR_AUTH_TOKEN. The manifest lists the kit's artifacts and the commit at its tip.
  2. Downloads the pack artifacts from gitcdn.origin.cursor.com and checks their sizes. With --verify, it also checks their hashes while the download runs.
  3. Installs them into the destination directory, which must be empty.
  4. Tops up the clone: fetches the objects added since the kit was built and checks out the current tip of the default branch. With --no-top-up, the checkout stays at the kit's tip. --bare creates a bare repository with no working tree.

When the kit path succeeds, the command prints mode: clone-kit on stdout. If any step fails, the default --fallback auto runs a plain git clone instead and prints mode: git-clone. Pass --fallback never to exit with status 1 instead.

The token and the checkout behave the same way on every CI system:

  • Tokens expire after at most 15 minutes and cannot be refreshed. Mint one immediately before the clone.
  • The CLI reads the token from CURSOR_AUTH_TOKEN. Export it in the shell that runs origin.
  • The clone ends on the default branch tip. To build a specific commit, run git fetch origin SHA and git checkout SHA afterwards. Origin serves any reachable commit by SHA.

Prerequisites

  • The Origin CLI on the runner. Install it with curl -fsSL https://downloads.cursor.com/origin/install.sh | sh, which puts the binary at $HOME/.local/bin/origin. Linux runners need glibc on x64 or arm64; Alpine and other musl images are not supported. macOS runners are supported. See Install the Origin CLI.
  • bash, git, jq, curl 7.55 or later, and openssl 1.1.1 or later. clone-fast uses curl for its downloads.
  • Network egress to these hosts:
HostUsed for
downloads.cursor.comInstalling the CLI
origin.cursor.comThe kit manifest and git operations
gitcdn.origin.cursor.comKit downloads
api.cursor.comMinting tokens and mirror sync requests from an Origin App

Choose a path

CI systemWhere the token comes fromSection
Buildkite, hosted or self-hosted agents, with Origin connected as the repository providerThe Buildkite Agent API, in a checkout hookBuildkite
GitHub ActionsYour Origin App, in a workflow stepGitHub Actions
GitLab CI, Jenkins, CircleCI, and self-hosted fleetsYour Origin App, in the jobOther CI systems

Buildkite

Buildkite mints the token for you. Replace the agent's default git checkout with a checkout hook that requests a token from the Buildkite Agent API and runs origin repo clone-fast.

You must be a Buildkite organization administrator to connect Origin, and an Origin administrator to install the Buildkite app.

1

Connect Origin to Buildkite

In Buildkite, select Settings > Repository Providers > Add Provider > Origin, or select Connect Origin account on the New Pipeline page. Select the owner and the repositories, then install the Buildkite app. Buildkite requests read access to repository contents and pull requests, and read and write access to checks. For details, see Origin in the Buildkite documentation.

2

Install the Origin CLI on the agent

Follow the Prerequisites, with curl, git, and jq on PATH.

3

Keep the checkout directory empty

If the agent keeps its build directory between builds, empty $BUILDKITE_BUILD_CHECKOUT_PATH before the hook runs. clone-fast needs an empty directory and falls back to git clone when it finds files there.

4

Add the checkout hook

On a self-hosted agent, save the script below as checkout in the agent's --hooks-path directory. On Buildkite hosted agents, ship it as the checkout hook of a non-vendored plugin, or set checkout: { skip: true } on the step and run the same commands in the step's command. A repository hook cannot define checkout, because the repository is not checked out yet. See Agent hooks and Git checkout in the Buildkite documentation.

The hook requests a token scoped to the pipeline's repository, exports it as CURSOR_AUTH_TOKEN, clones with clone-fast, registers the git credential helper with origin auth setup-git, and checks out $BUILDKITE_COMMIT. The agent token reaches curl through stdin, so it never appears on a command line:

#!/usr/bin/env bashset -euo pipefailbody=$(printf '{"repo_url":"%s"}' "$BUILDKITE_REPO")token=$(printf 'Authorization: Token %s\n' "$BUILDKITE_AGENT_ACCESS_TOKEN" \  | curl -fsS -X POST -H @- \      -H 'Content-Type: application/json' -H 'Accept: application/json' \      --data "$body" \      "${BUILDKITE_AGENT_ENDPOINT%/}/jobs/${BUILDKITE_JOB_ID}/cursor_origin_access_token" \  | jq -er '.token')# clone-fast takes owner/repo, not the clone URL.repo=${BUILDKITE_REPO#https://origin.cursor.com/}repo=${repo#git/}repo=${repo%.git}export CURSOR_AUTH_TOKEN="$token"origin repo clone-fast "$repo" "$BUILDKITE_BUILD_CHECKOUT_PATH" --verifyorigin auth setup-gitcd "$BUILDKITE_BUILD_CHECKOUT_PATH"git fetch origin "$BUILDKITE_COMMIT"git checkout -q "$BUILDKITE_COMMIT"

When a build starts without a commit, BUILDKITE_COMMIT is HEAD. The two git lines then fetch the remote HEAD and leave the working tree on the default branch tip that clone-fast already checked out.

  • Send $BUILDKITE_REPO unchanged. repo_url must match the repository URL registered on the pipeline exactly, including .git. A different spelling returns HTTP 400.
  • The token is read-only and scoped to the pipeline's repository. It carries repository:contents:read and expires after 15 minutes. A later job, or a git operation more than 15 minutes after the mint, needs a fresh token from the same request.
  • Retry on 503. If the Agent API answers HTTP 503, wait for its Retry-After header and retry, as Buildkite's own credential helper does.

GitHub Actions and other CI providers

GitHub Actions and other CI systems mint their own tokens. You create an Origin App once, install it on the repositories you clone, and give each job the app's private key and two ids. The job signs a short-lived app JWT and exchanges it for an installation token. For the full field reference, see App JWT and Create Installation Access Token.

Create an Origin App

You must be a workspace admin to install the app.

Register the app

1

Generate an Ed25519 key pair

Only Ed25519 keys are accepted:

openssl genpkey -algorithm ED25519 -out origin-app-private.pemopenssl pkey -in origin-app-private.pem -pubout -out origin-app-public.pem
2

Create the app and add the public key

In Origin app settings, create the app and add the contents of origin-app-public.pem as a signing key. An app holds up to 10 active signing keys.

3

Copy the App ID

The App ID is on the app's page. App IDs start with app_.

Install the app

1

Install the app on your owner

From the app's install page in the same settings, install the app on the owner that holds the repositories you clone, and select those repositories.

2

Copy the installation id

The installation id is in the URL of the installation's page, /codebase/settings/apps/installations/{installationId}. Installation ids start with i_.

Store the credentials

Store the private key as a secret in your CI system and expose it to the job as the environment variable ORIGIN_APP_PRIVATE_KEY, containing the full PEM text. Store the App ID and the installation id as the plain variables ORIGIN_APP_ID and ORIGIN_INSTALLATION_ID. If your CI system mounts secrets as files, load the key first with ORIGIN_APP_PRIVATE_KEY=$(cat /path/to/origin-app-private.pem).

Mint a token in the job

The function below signs the app JWT and exchanges it for an installation token. The JWT uses alg EdDSA, sets iss and kid to the App ID and aud to origin-apps, and sets exp 15 minutes ahead. The App JWT reference suggests a lifetime of about five minutes; because an installation token never outlives the JWT that minted it, this recipe signs a 15-minute JWT so the token gets its full 15 minutes. The request asks for repository:contents:read, which covers clone, fetch, and pull. Push needs repository:contents:write, and the installation must have granted that scope. Add "repositoryIds":[...] to the request body to narrow the token to some of the installation's repositories.

The private key reaches openssl through a file descriptor and the bearer header reaches curl through stdin, so neither appears on a command line, where other processes on the runner could read it. The function sets and exports CURSOR_AUTH_TOKEN directly, so the token never touches a file, and a failed mint stops the job under set -e. It needs bash, openssl 1.1.1 or later, curl 7.55 or later, and jq:

origin_app_token() {  b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }  now=$(date +%s)  header=$(printf '{"alg":"EdDSA","kid":"%s","typ":"JWT"}' "$ORIGIN_APP_ID" | b64url)  claims=$(printf '{"iss":"%s","aud":"origin-apps","iat":%d,"exp":%d}' \    "$ORIGIN_APP_ID" "$now" "$((now + 900))" | b64url)  # openssl -rawin needs a seekable input; the signing input holds no secret.  signing_input=$(mktemp)  trap 'rm -f "$signing_input"' EXIT  printf '%s.%s' "$header" "$claims" > "$signing_input"  signature=$(openssl pkeyutl -sign -rawin -in "$signing_input" \    -inkey <(printf '%s\n' "$ORIGIN_APP_PRIVATE_KEY") | b64url)  app_jwt="$header.$claims.$signature"  CURSOR_AUTH_TOKEN=$(printf 'Authorization: Bearer %s\n' "$app_jwt" \    | curl -fsS -X POST -H @- -H 'Content-Type: application/json' \        --data '{"scopes":["repository:contents:read"]}' \        "https://api.cursor.com/v1/origin/app/installations/${ORIGIN_INSTALLATION_ID}/access_tokens" \    | jq -er '.token')  export CURSOR_AUTH_TOKEN}

Call it immediately before the clone into an empty directory, then register the git credential helper so later git commands authenticate while CURSOR_AUTH_TOKEN stays exported. Replace acme/widgets with your repository and COMMIT_SHA with the commit your CI system exposes for the build:

set -euo pipefailorigin_app_tokenorigin repo clone-fast acme/widgets . --verifyorigin auth setup-gitgit fetch origin "$COMMIT_SHA"git checkout -q "$COMMIT_SHA"

GitHub Actions

GitHub Actions uses the Origin App path, with the private key in an Actions secret and the two ids in repository variables. Because GitHub fires the workflow, the repository lives on GitHub and Origin holds it as a mirror. The workflow asks Origin to pull the build's commit, clones with clone-fast into the empty $GITHUB_WORKSPACE, then checks that commit out. It never calls GitHub, so it needs no GITHUB_TOKEN permissions and does not use actions/checkout.

1

Add the secret

In your repository, select Settings > Secrets and variables > Actions, then add the secret ORIGIN_APP_PRIVATE_KEY with the full private key PEM.

2

Add the variables

On the same page, add the repository variables ORIGIN_APP_ID and ORIGIN_INSTALLATION_ID.

3

Add the workflow

Save the workflow below as .github/workflows/ci.yml, set ORIGIN_REPO to the mirror's {owner}/{repo} on Origin, and add your build steps after the clone step.

The workflow installs the CLI, mints a token, waits for Origin to mirror the commit, and clones it:

name: cion:  push:    branches: [main]  pull_request:permissions: {}jobs:  build:    runs-on: ubuntu-latest    defaults:      run:        shell: bash    steps:      - name: Install the Origin CLI        run: |          curl -fsSL https://downloads.cursor.com/origin/install.sh | sh          echo "$HOME/.local/bin" >> "$GITHUB_PATH"      - name: Clone from Origin with clone-fast        env:          ORIGIN_APP_ID: ${{ vars.ORIGIN_APP_ID }}          ORIGIN_INSTALLATION_ID: ${{ vars.ORIGIN_INSTALLATION_ID }}          ORIGIN_APP_PRIVATE_KEY: ${{ secrets.ORIGIN_APP_PRIVATE_KEY }}          ORIGIN_REPO: acme/widgets          BUILD_BRANCH: ${{ github.head_ref || github.ref_name }}          BUILD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}        run: |          set -euo pipefail          origin_app_token() {            b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }            now=$(date +%s)            header=$(printf '{"alg":"EdDSA","kid":"%s","typ":"JWT"}' "$ORIGIN_APP_ID" | b64url)            claims=$(printf '{"iss":"%s","aud":"origin-apps","iat":%d,"exp":%d}' \              "$ORIGIN_APP_ID" "$now" "$((now + 900))" | b64url)            signing_input=$(mktemp)            trap 'rm -f "$signing_input"' EXIT            printf '%s.%s' "$header" "$claims" > "$signing_input"            signature=$(openssl pkeyutl -sign -rawin -in "$signing_input" \              -inkey <(printf '%s\n' "$ORIGIN_APP_PRIVATE_KEY") | b64url)            app_jwt="$header.$claims.$signature"            echo "::add-mask::$app_jwt"            CURSOR_AUTH_TOKEN=$(printf 'Authorization: Bearer %s\n' "$app_jwt" \              | curl -fsS -X POST -H @- -H 'Content-Type: application/json' \                  --data '{"scopes":["repository:contents:read"]}' \                  "https://api.cursor.com/v1/origin/app/installations/${ORIGIN_INSTALLATION_ID}/access_tokens" \              | jq -er '.token')            echo "::add-mask::$CURSOR_AUTH_TOKEN"            export CURSOR_AUTH_TOKEN          }          origin_app_token          # Origin mirrors GitHub with a delay. Wait up to about two minutes for this commit.          body=$(printf '{"ref":"refs/heads/%s","sha":"%s","wait":true}' "$BUILD_BRANCH" "$BUILD_SHA")          printf 'Authorization: Bearer %s\n' "$CURSOR_AUTH_TOKEN" \            | curl -fsS -X POST -H @- -H 'Content-Type: application/json' --data "$body" \                "https://api.cursor.com/v1/origin/repos/${ORIGIN_REPO}:syncMirror" \            | jq -e '.synced' > /dev/null \            || { echo "Origin has not mirrored $BUILD_SHA from $BUILD_BRANCH yet" >&2; exit 1; }          origin repo clone-fast "$ORIGIN_REPO" . --verify          origin auth setup-git          git fetch origin "$BUILD_SHA"          git checkout -q "$BUILD_SHA"
  • On pull_request, the workflow builds the pull request head. github.sha is a merge commit that exists only on GitHub, so BUILD_SHA and BUILD_BRANCH use the pull request's head commit and branch instead.
  • The sync request is Sync Mirror. It accepts the installation token, needs repository:contents:read, and returns 200 with "synced": true or 202 with "synced": false after its wait budget of about two minutes. If Origin is the source of truth and GitHub is the mirror, delete the sync request: the commit is already on Origin, and Origin rejects sync requests for repositories that do not pull from an upstream source.
  • Both credentials are masked. ::add-mask:: hides the app JWT and the installation token in the job log.
  • Later steps mint again. A later step that needs Origin git access defines and calls the function again. The token is never written to $GITHUB_ENV or $GITHUB_OUTPUT, which are files on disk.
  • Pull requests from forks are not mirrored. Their head branch is not in your repository. Build those from GitHub.

Other CI systems

GitLab CI, Jenkins, CircleCI, and self-hosted fleets use the Origin App path directly. Store the private key and the two ids as described in Store the credentials, then run the function from Mint a token in the job immediately before origin repo clone-fast, and fetch and check out the commit your CI system exposes for the build.

Verify the first run

Run the first job with --fallback never. A missing kit then fails the job with exit status 1 instead of silently running a slow git clone. Keep the flag on until the kit path succeeds.

A successful run prints clone-kit: manifest=..., one download line per artifact, a clone-kit timings: block, and clone-kit: ready DIR (head SHA) on stderr, then mode: clone-kit on stdout, and exits 0. The timings block breaks the run into manifest, download, verify, and checkout phases. To measure the gain, compare its total with a plain git clone of the same repository on the same runner.

When the kit path does not complete, stderr shows one machine-readable line, clone-kit-result: status=fallback phase=PHASE or clone-kit-result: status=failed phase=PHASE, followed by the reason. With the default --fallback auto, stdout then shows mode: git-clone.

Troubleshooting

Each entry starts with the line the job log shows.

Manifest fetch returns 404

clone-kit-result: status=fallback phase=manifest with HTTP 404. No clone kit exists for the repository yet; contact your Cursor account team to turn CloneKit on. If the URL in the message contains https:// twice, the command received a clone URL instead of {owner}/{repo}.

Manifest fetch returns 401

clone-kit manifest fetch failed: HTTP 401. Origin rejected the token: it expired, was never valid, or its installation was removed. Mint a new token immediately before the clone. With the default --fallback auto, the fallback git clone fails with the same code and exit status 128, so the log shows both errors.

Manifest fetch returns 403

clone-kit manifest fetch failed: HTTP 403. The installation, or the Buildkite pipeline token, does not cover this repository. Reinstall the app or reselect the repositories it covers. As with 401, the fallback git clone fails with the same code and exit status 128.

Git asks for a username

fatal: could not read Username for 'https://origin.cursor.com'. Either the CLI is out of date or CURSOR_AUTH_TOKEN is not exported in the shell that runs git. Run origin update, or export the variable.

The clone falls back in the auth phase

clone-kit-result: status=fallback phase=auth with Not authenticated. CURSOR_AUTH_TOKEN is not exported into the process that runs origin. Export it in the same shell before the origin command.

The mint returns 401 Issuer is not authorized

401 {"code":16,"message":"Issuer is not authorized"} from the token endpoint. The iss claim is not a registered App ID, or the signing key is not registered on that app. Confirm that ORIGIN_APP_ID matches the app and that the app lists the signing key.

origin auth status reports a valid token but the clone fails

origin auth status derives Token: valid or Token: expired from the expiry claim inside CURSOR_AUTH_TOKEN. It does not ask Origin whether it accepts the token, so valid means only that the token has not expired. The CLI treats a token as expired five minutes before its expiry claim. origin commands refuse an expired token before making any request, and clone-fast logs clone-kit-result: status=fallback phase=auth with The injected CURSOR_AUTH_TOKEN session has expired. Mint a new token and retry.

If the kit path still fails, send your Cursor account team the job log with the clone-kit-result line, the repository, and the output of origin --version.