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/skillsand 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