Skip to content

Multi-repo workflows in Cursor

Multi-repo work in Cursor gives Cursor’s Agent the contracts of other repositories without blurring ownership: repositories changed together open as one multi-root workspace, consumed ones stay as pinned contract snapshots, and every cross-repo change lands contract first, with each repository’s CI proving compatibility.

This page is for developers and tech leads whose services live in separate repositories. You need to add phoneNumber to the user profile. That touches shared-types, user-service, the gateway schema, and two frontends. You open Cursor in user-service, and Agent cannot see the shared types, so it invents a UserDto that almost matches. Three PRs later, the web app compiles against a field the API never returns.

Sources for this page, checked on 2026-09-26: we checked multi-root loading and the rule file format against the agent runtime inside Cursor’s own @cursor/sdk 1.0.32 package. Its local-agent dirs option is documented as “Additional / multi-root workspace folders”, from which “project rules, skills, and request-context workspace metadata load from every unique path”. The GitHub MCP URLs come from GitHub’s github-mcp-server documentation. We ran the openapi-typescript 7.13.0 and @graphql-inspector/cli 7.0.0 commands locally. cursor.com could not be reached from the writing environment, so compare menu labels with your own Cursor 3.x build.

  • A decision table: which repositories to open, which to snapshot, and which to read over MCP.
  • A versioned workspace repository that holds the .code-workspace file and one ownership rule every agent session loads.
  • A drift check that fails CI when a vendored contract no longer matches its generated types, and a scheduled check that fails when upstream publishes a newer contract tag.
  • A contract-first order of work, with four copy-paste prompts for planning, the producer, the consumers, and the workspace check.
  • An evidence table that lets a reviewer approve five linked PRs by reading one contract diff, not every generated line.

Which multi-repo approach fits your repositories?

Section titled “Which multi-repo approach fits your repositories?”

Agent works well with what it can open and poorly with what it has to guess. The rule of thumb: open what you edit, snapshot what you only read.

Your situationGive AgentWhy
You change two to four repositories together and own all of themA multi-root workspaceAgent reads and edits the real code, and rules load from every folder
You call another team’s service and never edit itA pinned contract snapshot plus a rule that points at itAgent sees the exact contract; a scheduled check tells you when upstream publishes a newer tag
You occasionally need to look something up in a repository you have not clonedThe GitHub MCP server, read-onlyNo clone, no write access, one query at a time
All services live in one repositoryNot this pageSee large codebase strategies in Cursor and monorepo workflows

The microservices guide for Cursor covers service boundaries and topology rules. This page covers the mechanics that only exist when the code is split across repositories.

Set up a multi-root workspace for repositories you change together

Section titled “Set up a multi-root workspace for repositories you change together”

Cursor opens VS Code .code-workspace files. Put that file in its own small repository so the folder list and the ownership rule are versioned, reviewed, and shared by the whole team.

  1. Clone the repositories side by side and create a workspace repository next to them:

    ~/src/acme/
    ├── platform-workspace/ # new: workspace file + cross-repo rule
    ├── shared-types/
    ├── user-service/
    ├── api-gateway/
    └── web-app/
  2. Save platform-workspace/platform.code-workspace. Paths are relative to the workspace file, and name is the label Agent and you see for each root:

    {
    "folders": [
    { "path": ".", "name": "platform-workspace" },
    { "path": "../shared-types", "name": "shared-types" },
    { "path": "../user-service", "name": "user-service" },
    { "path": "../api-gateway", "name": "api-gateway" },
    { "path": "../web-app", "name": "web-app" }
    ],
    "settings": {
    "search.exclude": { "**/node_modules": true, "**/dist": true }
    }
    }
  3. Add a .cursorignore to each repository so build output and generated bundles stay out of Agent’s context:

    dist/
    build/
    coverage/
    *.min.js
  4. Open the file with File › Open Workspace from File, then run the workspace check prompt below in a new Agent chat.

If a folder is missing from the table, the path in the workspace file is wrong. Do not rely on the rule list alone, because Agent can misreport what it has loaded. The quoted line is the observable check: if the answer paraphrases the “Dependency direction:” line or omits it, the rule is not loaded. If a rule is missing, check that it sits under .cursor/rules/ and ends in .mdc: the runtime loads project rules from .cursor/rules/**/*.mdc. The last answer matters because the SDK runtime keeps one primary folder for shell commands. Expect npm test to run in one repository unless Agent changes directory first, and write verify commands with cd or git -C so they cannot run in the wrong place.

A rule that copies field lists goes stale the week the contract changes, and a stale rule is worse than none, because Agent trusts it. Put ownership, direction, and verify commands in the rule, and point at the contract files instead of restating them.

Save platform-workspace/.cursor/rules/workspace-map.mdc:

---
description: Ownership, dependency direction and verify commands for the platform workspace
alwaysApply: true
---
Repositories in this workspace and what each owns:
- shared-types: the source of truth for API types. Published as @acme/shared-types.
- user-service: owns the users table and /api/users. Its contract is user-service/openapi.yaml.
- api-gateway: owns the public GraphQL schema in api-gateway/schema.graphql.
- web-app: consumes the gateway only. It never calls user-service directly.
Dependency direction: shared-types -> user-service -> api-gateway -> web-app.
A change always lands in that order. Never edit a repository to the left of the
one the task names unless the task says so.
Contract rules:
- Change a contract additively first (new optional field, new endpoint). Remove
or rename only in a later change, after every consumer has shipped.
- Read types from the contract files above. Never hand-write a copy of them.
Verify commands (run from the workspace root):
- shared-types: cd ../shared-types && npm test && npx tsc --noEmit
- user-service: cd ../user-service && npm test
- api-gateway: cd ../api-gateway && npm test
- web-app: cd ../web-app && npx tsc --noEmit && npm test

For deeper rule design, including file-scoped globs rules per service, see project rules in Cursor and custom rules and templates.

Keep pinned snapshots of repositories you only consume

Section titled “Keep pinned snapshots of repositories you only consume”

When another team owns the service, do not open its code. Vendor its published contract at a tagged version, generate types from it, and let CI tell you when the snapshot and the types disagree or when upstream moves on.

Terminal window
# scripts/sync-contracts.sh (run from the consumer repository root)
set -euo pipefail
TAG="${1:?usage: sync-contracts.sh <user-service tag>}"
gh api "repos/acme/user-service/contents/openapi.yaml?ref=${TAG}" \
-H "Accept: application/vnd.github.raw+json" > contracts/user-service.openapi.yaml
echo "${TAG}" > contracts/user-service.version
npx openapi-typescript contracts/user-service.openapi.yaml -o src/generated/user-service.ts

The CI gate is two commands. --check exits with status 1 and prints “Generated types are not up-to-date!” when someone edits the snapshot or the generated file by hand. The type check then proves the client code still compiles against the contract:

Terminal window
npx openapi-typescript contracts/user-service.openapi.yaml -o src/generated/user-service.ts --check
npx tsc --noEmit

That gate only compares the vendored snapshot with the types generated from it. It never looks at user-service itself, so a new upstream tag passes unnoticed. Catch upstream drift with a separate job on a schedule (a daily cron in your CI) that compares the pinned tag with the latest release:

Terminal window
latest=$(gh release view --repo acme/user-service --json tagName -q .tagName)
[ "$latest" = "$(cat contracts/user-service.version)" ] || { echo "user-service $latest available"; exit 1; }

In CI, give this job a GH_TOKEN with read-only Contents access to acme/user-service (a fine-grained token or GitHub App token from your secret store). The default GITHUB_TOKEN cannot read other repositories. Grant the workflow permissions: contents: read.

When it fails, run scripts/sync-contracts.sh with the new tag and open a PR. The type check in that PR shows what the new contract breaks.

Add a file-scoped rule, .cursor/rules/consumed-contracts.mdc, so Agent uses the generated types instead of inventing its own:

---
description: How this repository consumes user-service
globs: src/clients/**/*.ts
alwaysApply: false
---
Types for user-service come only from src/generated/user-service.ts, which is
generated from contracts/user-service.openapi.yaml (version in
contracts/user-service.version). Never edit either file by hand.
If a field you need is missing from the generated types, stop and tell me.
Do not add it locally: the contract has to change upstream first.

Read a repository you have not cloned with the GitHub MCP server

Section titled “Read a repository you have not cloned with the GitHub MCP server”

For a one-off question (“how does the billing service sign webhooks?”), cloning is overkill. GitHub’s MCP server can read files and search code in any repository your token can see. Use the read-only repos toolset so the session cannot push, comment, or open issues. Add it to your user-level ~/.cursor/mcp.json, not the repository’s, so the token stays personal:

{
"mcpServers": {
"github-read": {
"url": "https://api.githubcopilot.com/mcp/x/repos/readonly",
"headers": { "Authorization": "Bearer ${env:GITHUB_READ_TOKEN}" }
}
}
}

Create GITHUB_READ_TOKEN as a fine-grained personal access token with read-only Contents access to the repositories you need, and export it in the shell that launches Cursor. The ${env:…} reference keeps the token out of the file. Treat everything the server returns as untrusted input: a file in another repository can contain instructions aimed at your agent, which is one more reason to keep the toolset read-only. The GitHub MCP server guide covers the other toolsets and the local Docker install.

If you read the same repository every day, stop querying it. Clone it and add it to the workspace, or snapshot its contract.

A cross-repo change fails in the gaps between PRs, so the order matters more than any single prompt. Plan the whole change once, then implement it one repository at a time, each in its own Agent chat, so every diff stays scoped to one PR.

  1. Plan across the workspace. Switch the chat to Plan Mode, which “creates detailed implementation plans before writing any code”, and paste the planning prompt below. After you approve the plan, save it as platform-workspace/plans/phone-number.md (for example, switch to Agent mode and ask it to write the file) so every later chat can reference it.

  2. Change the contract additively. In shared-types (or the producer’s OpenAPI file), add the field as optional and publish a pre-release version, for example @acme/shared-types@2.4.0-rc.1. Gate it before the final release: for the TypeScript package, install the rc in each consumer and run npx tsc --noEmit with no code changes, which must pass because an optional field breaks nothing; for OpenAPI or GraphQL contracts, run the breaking-change diff from the evidence table below.

  3. Implement the producer. In a new chat scoped to user-service, run the producer prompt. Its PR merges once its own CI is green.

  4. Adapt each consumer. One chat per consumer, pointed at the pre-release version. Each consumer PR shows its type check and tests against the new contract.

  5. Merge in dependency order, then clean up. Contract, then producer, then consumers. Removing or renaming the old shape is a separate change, after every consumer has deployed.

To run the consumers in parallel, keep one chat per repository. Cursor’s Worktrees “let Agent work in isolated Git checkouts” (Cursor docs, checked 2026-08-28). A Git worktree always belongs to one repository, so they suit parallel attempts inside a repository, not a change across several. The Agents Window is where you watch several of those sessions side by side. For unattended runs of the same change across repositories, see Cloud Agents and Automations.

How do you prove a cross-repo change works without reading every diff?

Section titled “How do you prove a cross-repo change works without reading every diff?”

Nobody should read five generated diffs line by line. The reviewer reads the contract diff, which is small, and accepts the rest on evidence each repository’s CI produces:

EvidenceCommand or sourceWhat it catches
The contract change is not breakingnpx @graphql-inspector/cli diff old.graphql new.graphql exits 1 on a breaking change and 0 on an additive one; for OpenAPI, use the breaking-change diff in the microservices guideA removed or renamed field slipped into the “additive” step
Vendored snapshots match their generated typesnpx openapi-typescript … --checkA hand-edited snapshot or generated file
The pinned contract is the latestThe scheduled gh release view check aboveA consumer pinned to a stale upstream tag
Each consumer compiles against the new contractnpx tsc --noEmit in each consumerA consumer that relies on the old shape
Consumer expectations hold on the producerConsumer-driven contract tests with @pact-foundation/pact, verified in the producer’s CIA producer change that type-checks but returns the wrong data
Each repository still worksEvery PR’s own CI, greenRegressions unrelated to the contract

Put the linked PRs, the merge order, and each gate’s result in the description of the contract PR, using the evidence bundle format. Sign-off follows ownership: the owner of the contract (named in CODEOWNERS for the schema file) approves the contract PR after reading its diff. Each repository’s owner approves that repository’s PR on its evidence.

What breaks in multi-repo Cursor workflows?

Section titled “What breaks in multi-repo Cursor workflows?”

Agent edits a repository the task did not name. A multi-root workspace makes every folder writable. Restore the stray files with the chat’s checkpoint or git -C ../api-gateway restore ., then run git -C ../api-gateway status --short and remove the untracked files it lists (git -C ../api-gateway clean -n to preview, clean -f to delete): restore only reverts tracked files. Then keep implementation chats to one repository and name the allowed folder in the first line of the prompt, as the prompts above do.

A test passes in the wrong repository. Agent ran npm test in the primary folder, not the one it changed. Put cd ../<repo> && or git -C in every verify command in the rule, and ask for the exit code of each command, not a summary.

The rule never loads. Run the workspace check prompt. A rule that does not appear is usually a .md file instead of .mdc, sits outside .cursor/rules/, or has broken frontmatter.

A snapshot drifts from upstream. The consumer compiles against last month’s contract. The --check gate does not see this: it catches only local hand-edits. Run the scheduled upstream check above, re-run scripts/sync-contracts.sh with the new tag, and let Agent fix the type errors that appear. Make the --check gate a required CI status so hand-edits cannot slip through either.

A breaking change merged before its consumers. A field was renamed, not added. Revert the contract PR first. Then re-land it as add, migrate consumers, and remove later. Consumers that already shipped keep working because the revert restores the old shape.

Consumers deploy before the producer. The web app asks for phoneNumber and gets undefined. Nullable, optional fields make this harmless, which is why the plan prompt asks for a backward-compatibility argument at every step. For behaviour changes, put the consumer code behind a flag until the producer is live.

The workspace is slow and Agent’s answers pull in unrelated code. Too many folders or too little ignored. Remove folders you only read and snapshot them instead, and extend each .cursorignore.