Cursor SDK multi-cwd project settings only load from first cwd

Where does the bug appear (feature/product)?

Cursor SDK

Describe the Bug

LocalAgentOptions(cwd=[root_a, root_b], setting_sources=["project"]) appears
to configure only the first cwd as the agent’s project workspace for system
prompt metadata, project skills, and project rules.

The same behavior occurs whether cwd is passed as list[str] or
list[pathlib.Path].

Steps to Reproduce

Run:

export CURSOR_API_KEY=...
uv run python script.py

The repro script creates two temporary repos. Each repo contains:

  • one workspace marker file;
  • two project skills, one under .agents/skills and one under
    .cursor/skills;
  • two always-apply project rules under .cursor/rules.

It then runs four cases:

  • cwd=[str(root_alpha), str(root_beta)]
  • cwd=[str(root_beta), str(root_alpha)]
  • cwd=[Path(root_alpha), Path(root_beta)]
  • cwd=[Path(root_beta), Path(root_alpha)]

Each case asks the agent, without using filesystem tools, to report visible
workspace paths, skills, and rules from its system prompt/project metadata. It
then asks the agent to read marker files from both roots to distinguish project
metadata loading from filesystem access.

script.py:

from __future__ import annotations

import json
import os
import signal
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any

from cursor_sdk import Agent, AgentOptions, LocalAgentOptions


MODEL = os.environ.get("CURSOR_SDK_REPRO_MODEL", "default")
SETTING_SOURCES = ["project"]
CASE_TIMEOUT_SECONDS = int(os.environ.get("CURSOR_SDK_REPRO_CASE_TIMEOUT_SECONDS", "150"))

SYSTEM_CONTEXT_PROMPT = """
This is a Cursor SDK multi-cwd diagnostic.

Do not use filesystem tools for this prompt. Answer only from the system prompt,
workspace metadata, available_skills, and loaded rules/configuration already
provided to you.

Please report:
1. How many Cursor workspace roots / cwd roots you believe are configured.
2. The exact Workspace Path values shown to you, if any.
3. The available skill names and any ALPHA/BETA skill sentinel strings visible.
4. The loaded project rules and any ALPHA/BETA rule sentinel strings visible.
""".strip()

FILE_ACCESS_PROMPT = """
Use tools if needed. Check whether you can read both workspace marker files:

- WORKSPACE_MARKER_ALPHA.txt should contain ALPHA_WORKSPACE_FILE_SENTINEL.
- WORKSPACE_MARKER_BETA.txt should contain BETA_WORKSPACE_FILE_SENTINEL.

Report whether you found each sentinel and how you found it.
""".strip()


def main() -> int:
    if len(sys.argv) == 3 and sys.argv[1] == "--one":
        payload = json.loads(sys.argv[2])
        print(json.dumps(_run_case(payload), ensure_ascii=False), flush=True)
        return 0

    if not os.environ.get("CURSOR_API_KEY"):
        raise SystemExit("Set CURSOR_API_KEY before running this repro.")

    with tempfile.TemporaryDirectory(prefix="cursor-sdk-multi-cwd-repro-") as tmp:
        base = Path(tmp)
        root_a = base / "repo-alpha"
        root_b = base / "repo-beta"
        _create_repo(root_a, "alpha", "ALPHA")
        _create_repo(root_b, "beta", "BETA")

        cases = [
            {
                "name": "str-cwd-alpha-beta",
                "cwd_mode": "str",
                "roots": [str(root_a), str(root_b)],
            },
            {
                "name": "str-cwd-beta-alpha",
                "cwd_mode": "str",
                "roots": [str(root_b), str(root_a)],
            },
            {
                "name": "pathlike-cwd-alpha-beta",
                "cwd_mode": "pathlike",
                "roots": [str(root_a), str(root_b)],
            },
            {
                "name": "pathlike-cwd-beta-alpha",
                "cwd_mode": "pathlike",
                "roots": [str(root_b), str(root_a)],
            },
        ]

        print("# Cursor SDK multi-cwd project settings repro")
        print(f"python={sys.version.split()[0]}")
        print(f"model={MODEL}")
        print(f"setting_sources={SETTING_SOURCES}")
        print(f"root_alpha={root_a}")
        print(f"root_beta={root_b}")
        print(f"pathlib.Path is os.PathLike={isinstance(root_a, os.PathLike)}")
        print()

        for case in cases:
            result = _run_case_subprocess(case)
            _print_case(case, result)

    return 0


def _run_case_subprocess(case: dict[str, Any]) -> dict[str, Any]:
    command = [sys.executable, __file__, "--one", json.dumps(case)]
    process = subprocess.Popen(
        command,
        cwd=Path(__file__).parents[1],
        env={**os.environ, "PYTHONUNBUFFERED": "1"},
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        preexec_fn=os.setsid,
    )
    try:
        stdout, stderr = process.communicate(timeout=CASE_TIMEOUT_SECONDS)
    except subprocess.TimeoutExpired:
        os.killpg(process.pid, signal.SIGTERM)
        try:
            stdout, stderr = process.communicate(timeout=5)
        except subprocess.TimeoutExpired:
            os.killpg(process.pid, signal.SIGKILL)
            stdout, stderr = process.communicate()
        return {
            "status": "timeout",
            "stdout": stdout,
            "stderr": stderr,
            "timeout_seconds": CASE_TIMEOUT_SECONDS,
        }
    if process.returncode != 0:
        return {
            "status": "exit",
            "returncode": process.returncode,
            "stdout": stdout,
            "stderr": stderr,
        }
    try:
        return json.loads(stdout)
    except json.JSONDecodeError:
        return {"status": "bad-json", "stdout": stdout, "stderr": stderr}


def _run_case(case: dict[str, Any]) -> dict[str, Any]:
    roots = [Path(root) for root in case["roots"]]
    cwd: list[str] | list[Path]
    if case["cwd_mode"] == "pathlike":
        cwd = roots
    else:
        cwd = [str(root) for root in roots]

    options = AgentOptions(
        model=MODEL,
        api_key=os.environ["CURSOR_API_KEY"],
        local=LocalAgentOptions(cwd=cwd, setting_sources=SETTING_SOURCES),
    )

    with Agent.create(options) as agent:
        system_context = agent.send(SYSTEM_CONTEXT_PROMPT).wait()
        file_access = agent.send(FILE_ACCESS_PROMPT).wait()

    return {
        "status": "ok",
        "system_context_status": getattr(system_context, "status", None),
        "system_context_text": getattr(system_context, "result", "") or "",
        "file_access_status": getattr(file_access, "status", None),
        "file_access_text": getattr(file_access, "result", "") or "",
    }


def _create_repo(root: Path, slug: str, label: str) -> None:
    root.mkdir(parents=True)
    (root / f"WORKSPACE_MARKER_{label}.txt").write_text(
        f"{label}_WORKSPACE_FILE_SENTINEL\n",
        encoding="utf-8",
    )
    (root / "README.md").write_text(f"# repo-{slug}\n", encoding="utf-8")
    _write_skill(root, ".agents", f"{slug}-agents-skill", f"{label}_AGENTS_SKILL_SENTINEL")
    _write_skill(root, ".cursor", f"{slug}-cursor-skill", f"{label}_CURSOR_SKILL_SENTINEL")
    _write_rule(root, f"{slug}-rule-one", f"{label}_RULE_ONE_SENTINEL")
    _write_rule(root, f"{slug}-rule-two", f"{label}_RULE_TWO_SENTINEL")


def _write_skill(root: Path, config_dir: str, name: str, sentinel: str) -> None:
    skill_dir = root / config_dir / "skills" / name
    skill_dir.mkdir(parents=True)
    (skill_dir / "SKILL.md").write_text(
        "\n".join(
            [
                "---",
                f"name: {name}",
                f"description: Use this skill only when the user says {sentinel}.",
                "---",
                f"When this skill is active, mention {sentinel}.",
                "",
            ]
        ),
        encoding="utf-8",
    )


def _write_rule(root: Path, name: str, sentinel: str) -> None:
    rule_dir = root / ".cursor" / "rules"
    rule_dir.mkdir(parents=True, exist_ok=True)
    (rule_dir / f"{name}.mdc").write_text(
        "\n".join(
            [
                "---",
                "alwaysApply: true",
                f"description: Always mention {sentinel} when listing loaded rules.",
                "---",
                f"# {name}",
                "",
                f"When asked about loaded project rules, mention {sentinel}.",
                "",
            ]
        ),
        encoding="utf-8",
    )


def _print_case(case: dict[str, Any], result: dict[str, Any]) -> None:
    print(f"## {case['name']}")
    print(f"cwd_mode={case['cwd_mode']}")
    print(f"roots={case['roots']}")
    print(f"status={result.get('status')}")
    if result.get("status") != "ok":
        print(_compact(result))
        print()
        return

    system_context = result.get("system_context_text", "")
    file_access = result.get("file_access_text", "")
    print(
        "system_context_presence="
        + json.dumps(_presence(system_context), ensure_ascii=False, sort_keys=True)
    )
    print("system_context_excerpt=" + _excerpt(system_context))
    print(
        "file_access_presence="
        + json.dumps(_presence(file_access), ensure_ascii=False, sort_keys=True)
    )
    print("file_access_excerpt=" + _excerpt(file_access))
    print()


def _presence(text: str) -> dict[str, bool]:
    return {
        "alpha_workspace_file": "ALPHA_WORKSPACE_FILE_SENTINEL" in text,
        "beta_workspace_file": "BETA_WORKSPACE_FILE_SENTINEL" in text,
        "alpha_agents_skill": "ALPHA_AGENTS_SKILL_SENTINEL" in text
        or "alpha-agents-skill" in text,
        "alpha_cursor_skill": "ALPHA_CURSOR_SKILL_SENTINEL" in text
        or "alpha-cursor-skill" in text,
        "beta_agents_skill": "BETA_AGENTS_SKILL_SENTINEL" in text
        or "beta-agents-skill" in text,
        "beta_cursor_skill": "BETA_CURSOR_SKILL_SENTINEL" in text
        or "beta-cursor-skill" in text,
        "alpha_rule_one": "ALPHA_RULE_ONE_SENTINEL" in text,
        "alpha_rule_two": "ALPHA_RULE_TWO_SENTINEL" in text,
        "beta_rule_one": "BETA_RULE_ONE_SENTINEL" in text,
        "beta_rule_two": "BETA_RULE_TWO_SENTINEL" in text,
    }


def _excerpt(text: str) -> str:
    return " ".join(text.replace("\n", " ").split())[:1200]


def _compact(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False, sort_keys=True)[:2000]


if __name__ == "__main__":
    raise SystemExit(main())

Expected Behavior

For a local SDK agent created with multiple cwd roots:

  • the system prompt/workspace metadata should expose all configured workspace
    roots, or the SDK docs should state that only the first root is active for
    project settings;
  • project skills from all cwd roots should be available;
  • project rules from all cwd roots should be loaded.

Operating System

Linux

Version Information

cursor-sdk version is 0.1.5

Does this stop you from using Cursor

Sometimes - I can sometimes use Cursor