# @cursor/sdk

TypeScript SDK for Cursor agents.

Documentation: https://cursor.com/docs/api/sdk/typescript

## No-repo cloud agents

Cloud agents can run on an empty VM with no repository. Pass `cloud` (omitting
it selects the local platform) and leave `repos` unset:

```ts
const agent = await Agent.create({
  apiKey: process.env.CURSOR_API_KEY,
  cloud: { repos: [] },
});
```

No-repo agents must be enabled for your account or team, and repository-scoped
API keys cannot create them.

## Run a local agent on a FileSystem

Set `local.fs` and a local agent's own file tools (Read, Write, StrReplace,
Grep, Glob, LS, Delete, and Task) run on a tree you supply instead of the
host disk. The model sees its usual tools under their usual names, and
nothing on the host reaches it: no host files, shell, git status, or hooks.

```ts
import { Agent, tempFs, unionFs } from "@cursor/sdk";

const agent = await Agent.create({
  apiKey: process.env.CURSOR_API_KEY,
  model: { id: "composer-2" },
  local: {
    fs: unionFs({
      "/repo": tempFs({
        "/AGENTS.md": "Answer in one paragraph.",
        "/src/greet.ts": 'export const greeting = "hello";\n',
      }),
      "/notes": tempFs(),
    }),
    reportedWorkspacePath: "/repo",
    settingSources: ["project"],
  },
});

const run = await agent.send('Change the greeting to "hello from fs" and note the change in /notes/log.md');
await run.wait();
```

The tree is a live handle, so the edits above are in `/repo/src/greet.ts` and
`/notes/log.md` when the run finishes. Leave `fs` out and the agent works on
the host disk as before.

### Build a tree

Every helper returns a `FileSystem`, so they compose:

| Helper | What it gives you |
| --- | --- |
| `tempFs(files?)` | An in-memory tree seeded from a path-to-contents record. Stores text and bytes. |
| `unionFs({ "/repo": a, "/notes": b })` | One namespace from several trees, each mounted at a prefix. The longest prefix that covers a path owns it. |
| `overlayFs([top, bottom])` | A read-only view where earlier layers win. |
| `readOnlyFs(tree)` | The same files, with `write` and `delete` refused. |
| `childOverlayFs(tree)` | A writable view whose writes and deletes stay on a private layer, so `tree` never changes. |
| `emptyFs()` | A tree with no files, for an empty mount. |

Pass a factory instead of a tree to build a new tree for each run. It's
called with `{ agentId, sessionId }`:

```ts
local: {
  fs: async ({ agentId }) => unionFs({ "/repo": await checkoutFor(agentId), "/scratch": tempFs() }),
},
```

### Workspace and project rules

The model's workspace is `/` unless `local.reportedWorkspacePath` names a
folder in the tree, such as `/repo` above. Relative paths in tool calls
resolve against it, and Grep, Glob, and LS search it when a call names no
path. Other mounts stay reachable by absolute path.

Project rules come from the tree, never from the host: `<workspace>/AGENTS.md`
and `<workspace>/.cursor/rules/**/*.mdc`, with the same parsing as on the
host. They load only when `settingSources` includes `"project"`, so leave it
out when the tree holds a repository you don't trust. Nested `AGENTS.md`
files, parent folders, and skills aren't discovered under `fs`. Pass skills to
the agent yourself. User and team rules work as usual.

`local.cwd` stays a host directory. It decides where the SDK keeps the agent's
own state and transcripts and never changes what the model sees.

### Tools under `fs`

| Tool | Under `fs` |
| --- | --- |
| Read, Write, StrReplace, and the other edit tools, Delete | Run on the tree. Read returns images as images. |
| Grep, Glob, LS | Run on the tree. Grep patterns are JavaScript `RegExp` source, not ripgrep syntax. LS and Glob stop at 1,000 entries. |
| Task | Runs each child on the parent's tree (see below). |
| WebFetch, WebSearch, MCP, GenerateImage | Work as usual. Output that spills to a file lands in `/tmp`. |
| Todos, AskQuestion, CreatePlan, SwitchMode, Await | Work as usual. |
| Shell, ReadLints, SemanticSearch, and host-only tools | Not available. Naming one in `tools` is a `ConfigurationError`. |

### `/tmp`

Each run gets a new, empty in-memory `/tmp` mounted over your tree, and any
`/tmp` in the tree is hidden for that run. WebFetch and WebSearch results over
20 KB, MCP results over 40 KB, and GenerateImage files are written there, and
the model reads them back with Read or Grep:

```text
WebFetch → Content written to file: /tmp/0b3a6c1e-5d2f-4a8b-9c7d-1e2f3a4b5c6d.txt
Grep { path: "/tmp/0b3a6c1e-5d2f-4a8b-9c7d-1e2f3a4b5c6d.txt", pattern: "License" }
```

`/tmp` is dropped when the run ends. Your tree never receives these files, so
a read-only tree works.

### Task children

Task children, and their own children, work on the same tree as the session
that started them, the way children without `fs` work in the same host
folder. The parent sees a child's writes and deletes as soon as they happen,
including files under `/tmp`:

```ts
await agent.send("Use a Task to fix the failing test in src/, then show me the diff of what it changed");
```

Children use the parent's workspace and tool allowlist and have no shell. To
keep a whole run's writes off the tree you pass in, return
`childOverlayFs(tree)` from an `fs` factory; the run then writes to an
in-memory layer that is dropped when the run ends.

### Run a file tool outside a turn

`createFileSystemTools(tree, { workspace })` returns the same native tools as
plain functions, for a direct tool-call endpoint, a test, or a CLI. Each
returns the tool call a run's `tool_call` event carries:

```ts
import { createFileSystemTools } from "@cursor/sdk";

const tools = createFileSystemTools(tree, { workspace: "/repo" });

const call = await tools.Read.execute({ path: "src/greet.ts", limit: 20 });
if (call.result?.status === "success") {
  console.log(call.result.value.content);
}

for (const tool of Object.values(tools)) {
  console.log(tool.name, tool.annotations.readOnlyHint);
}
```

`annotations` uses the MCP tool annotation shape: Read, LS, Grep, and Glob
are read-only, and Write, StrReplace, and Delete are destructive.

### Write your own FileSystem

A `FileSystem` has seven methods:

```ts
interface FileSystem {
  list(prefix?: string, options?: ListOptions): AsyncIterable<DirEntry>;
  stat(path: string): Promise<FileInfo>;
  glob(pattern: string, dir?: string, options?: GlobOptions): AsyncIterable<DirEntry>;
  read(path: string): Promise<string | Uint8Array>;
  grep(query: FSGrepQuery): AsyncIterable<FSGrepResult>;
  write(path: string, contents: string | Uint8Array): Promise<void>;
  delete(path: string): Promise<void>;
}
```

The tools depend on this contract:

- Paths are absolute POSIX paths rooted at `/`, with no trailing slash except
  `/` itself. Prefixes match by path segment: `/agent` covers `/agent/skills`
  but not `/agent-tools`.
- `list` and `glob` yield each path once, in lexicographic order. `grep`
  yields by path, then line number.
- A missing path is a value, not an error: `list` and `glob` yield nothing,
  and `stat` returns `{ kind: "missing" }`.
- `read`, `write`, and `delete` throw a `PathError` subclass, such as
  `FileNotFoundError` or `ReadOnlyMountError`. Its message is what the model
  sees.
- `ListOptions.excludes`, `limit`, and `maxDepth` and `FSGrepQuery.maxResults`
  let a remote tree stop early, and you can ignore them, because the tools
  apply the same limits afterwards. With `maxDepth`, yield one
  `{ path, kind: "directory" }` entry for each subtree the depth cuts off.
  LS lists one level at a time this way instead of walking a whole remote
  repository.
- `write` gets bytes as well as text, for example a GenerateImage file. A
  tree that stores only text should throw `WriteFailedError` for bytes
  rather than corrupt them.
- `grep` patterns are JavaScript `RegExp` source. Throw
  `InvalidGrepPatternError` for one that doesn't compile.

Wrapping an existing tree is the shortest path. This one records every
change the agent makes:

```ts
import type { FileSystem } from "@cursor/sdk";

function auditedFs(inner: FileSystem, onChange: (change: string) => void): FileSystem {
  return {
    list: (prefix, options) => inner.list(prefix, options),
    stat: (path) => inner.stat(path),
    glob: (pattern, dir, options) => inner.glob(pattern, dir, options),
    read: (path) => inner.read(path),
    grep: (query) => inner.grep(query),
    async write(path, contents) {
      await inner.write(path, contents);
      onChange(`wrote ${path}`);
    },
    async delete(path) {
      await inner.delete(path);
      onChange(`deleted ${path}`);
    },
  };
}
```

### Limits

- `fs` is for local agents. Setting it on a cloud agent is a
  `ConfigurationError`.
- The Python SDK has no `fs`, because the tree is an in-process TypeScript
  object.
- `fs` can't be combined with `local.subagentInherit` or
  `local.resourceProviderOptions`. Task children already run on the tree.
- Resuming an agent that was created with `fs` needs `fs` again.
- `LocalSendOptions.fs` replaces the tree for one send and keeps the agent's
  `reportedWorkspacePath` unless the send sets its own.
