Cursor is the AI code editor that made “the model edits your codebase, not just the current line” feel normal. Under the marketing it is a set of specific engineering bets: fork the editor instead of writing a plugin, index the codebase as embeddings while keeping the source on your machine, train a small model to predict your next edit, and split every code change into a slow model that decides and a fast model that types. In this lesson, we’ll take each of those apart and see the mechanism.
We’ll stay grounded in Cursor’s own engineering blog, docs, and the Fireworks writeup on their apply model, and we’ll draw each subsystem as a diagram, then show the code or pseudocode that makes the diagram real. The aim is not a feature tour. It is to understand the handful of latency and retrieval tricks that let an editor feel instant while a large model is thinking behind it.
By the end you’ll be able to:
- Explain why Cursor is a fork of VS Code and not an extension.
- Trace a query through the indexing pipeline and say exactly what is stored on Cursor’s servers and what never leaves your machine.
- Describe how the Tab model predicts an edit and a cursor jump, not just the next token.
- Explain speculative edits: why the file being edited is its own draft model.
- Tell apart the Shadow Workspace and Background Agents, which are constantly confused.
A note on sourcing: some of the best-known “how Cursor works” writeups are reverse-engineered. Where a claim is only from a secondary source, or where Cursor has deliberately not disclosed something (the vector database vendor, the embedding model, the Tab model’s size), this lesson says so instead of guessing.
Why Cursor forked the editor instead of shipping a plugin
Cursor is a fork of Visual Studio Code, not a VS Code extension. That choice is the root of most of what follows. An extension runs inside the sandbox VS Code gives it: it can add a panel and call an API, but it cannot change how the editor applies edits, cannot run a hidden second copy of the workspace, and cannot deeply control indexing. Features like a speculative Tab model, a shadow workspace, and multi-file atomic edits need to reach below that sandbox, so Cursor took the whole editor and modified it (design discussion on the Lex Fridman interview with the Cursor team).
The shape is a thin local client plus a model-serving backend. The editor on your machine holds the source files and orchestrates; embedding generation, the vector index, the apply model, and the background-agent VMs all run server-side (secure codebase indexing).
flowchart LR
subgraph LOCAL["Your machine (forked editor)"]
FILES["Source files"]
LSP["Language server, lint"]
UI["Editor UI, diffs"]
end
subgraph CLOUD["Cursor backend"]
EMB["Embedding service"]
VDB["Vector index<br/>(embeddings + path/line)"]
APPLY["Fast Apply model"]
AGENTS["Background-agent VMs"]
end
FILES -->|"chunks to embed"| EMB
EMB --> VDB
UI -->|"edit sketch"| APPLY
APPLY -->|"full diff"| UI
VDB -->|"path + line ranges"| LOCAL
The one idea to carry forward: the editor keeps your code and decides what to send; the cloud does the heavy compute. That division is what the privacy story and the latency story are both built on. The cost of the choice, which Cursor acknowledges, is that they now merge upstream VS Code changes into a diverging codebase forever.
What Cursor stores, and what stays on your machine
The most common misconception is that Cursor uploads your code, but it does not store your source. It stores vectors and location metadata, and reads the actual code from your machine at the moment it needs it.
The pipeline: Cursor chunks your files locally, sends the chunks to its server to be turned into embeddings (numeric vectors that place similar code near each other), and stores those vectors alongside metadata (file path, start and end line numbers), not the raw text (secure codebase indexing). At query time it embeds your question, finds the nearest vectors, gets back paths and line ranges, and then reads that content from your local disk to build the model’s context. It is a retrieval-augmented loop where the retrieved thing is a location, not the code itself.
flowchart LR A["Local file"] --> B["Chunk locally"] B -->|"send chunk text"| C["Embed (server)"] C --> D["Vector index<br/>vector + path + lines"] Q["Your query"] --> E["Embed query"] E --> F["Nearest-neighbor search"] D --> F F -->|"paths + line ranges"| G["Read content from LOCAL disk"] G --> H["Build model context"]
Under the hood a stored record is just a vector plus where-it-came-from:
{
"vector": [0.0123, -0.0391, 0.221, "... ~1–3k dims ..."],
"path": "src/auth/login.ts",
"start_line": 42,
"end_line": 78
}
Because the payload is a vector and a line range, a leak of the index does not hand over your source. The tradeoff is one extra local read per hit, which is cheap.
Keeping the index fresh without re-embedding everything
Re-embedding a large repo on every keystroke would be wasteful, so Cursor syncs with a Merkle tree: hash every file, then hash each folder from its children’s hashes, up to a single root hash. When you edit one file, only that file’s hash and the hashes of its parent folders change, so a comparison against the server’s tree reveals exactly which files moved. For a 50,000-file repo, the filenames plus SHA-256 hashes total about 3.2 MB (secure codebase indexing). Embeddings are also cached by chunk content, so an unchanged chunk is never re-embedded.
def folder_hash(node):
if node.is_file:
return sha256(node.bytes)
return sha256("".join(folder_hash(c) for c in sorted(node.children)))
# One edit changes the file's hash and every ANCESTOR folder hash up to root.
# Diffing local root vs server root, then walking down only where hashes
# differ, finds the changed files in O(depth), not O(repo size).
def changed_files(local, server):
if local.hash == server.hash:
return [] # whole subtree identical, prune it
if local.is_file:
return [local.path]
return [p for l, s in zip(local.children, server.children)
for p in changed_files(l, s)]
The teaching point: a Merkle tree turns “what changed?” from a full scan into a hash comparison that prunes everything identical. It is the same structure Git and Dropbox use, applied to keeping an embedding index current.
The user-facing hook is @codebase, which runs this semantic search across the whole project; in agent mode the model calls the search tool itself (context and mentions).
Two honest flags. Secondary sources name the vector database as Turbopuffer, but Cursor’s own indexing post does not name the vendor, and it does not disclose which embedding model it uses. Treat both as undisclosed.
Tab predicts your next edit and where you will move next
Ordinary autocomplete predicts the next few tokens at your cursor. Cursor’s Tab is a different job: since March 2024 it is a custom sparse model trained specifically to predict edits, over billions of tokens of edit data (Tab update). Two things make it feel different from autocomplete.
First, it predicts both the edit near your cursor and where you will go next, a “jump.” Rename a symbol and Tab can propose the follow-on edit in another file and offer to jump you there. Second, its inputs are not just the current line: it reads open tabs, your recent edit history, and cursor context, and treats “what edit comes next” as a low-entropy prediction.
flowchart LR
subgraph STATE["Editor state fed to Tab"]
CUR["Current file + cursor"]
HIST["Recent edit history"]
TABS["Open tabs"]
end
STATE --> MODEL["Sparse edit model"]
MODEL --> E1["Edit near cursor (a diff)"]
MODEL --> E2["Jump target<br/>(possibly another file)"]
The shape of what Tab consumes and emits, as a sketch:
suggestion = tab_model.predict(
current_file = buffer,
cursor = position,
recent_edits = edit_log[-N:], # what you just changed
open_tabs = [t.path for t in tabs],
)
# Returns an EDIT (a diff to apply here) plus an optional JUMP:
# suggestion.diff -> {range, replacement}
# suggestion.jump -> {path, line} # "your next edit is probably here"
The latest Tab model, Fusion, comes with hard numbers (Tab update): its context window grew from 5,500 to 13,000 tokens, its median latency dropped from 475 ms to 260 ms, it handles 25% more difficult edits per line and 10x longer stretches of changes, and it adds near-instant jumps. At scale, Tab produces over a billion edited characters per day, and request volume has grown about 100x since launch.
Two honest flags. Cursor describes Tab only as a “custom sparse language model” and does not disclose its parameter count or base architecture. And a figure circulating in secondary writeups, a 272,000-token Tab context, contradicts Cursor’s own 13,000 for Fusion. Use 13,000.
The line to remember: Tab is a next-action model. Predicting where you will edit next is a harder and more useful task than predicting the next token, and it is why the editor seems to move with you.
Two models per edit: one decides, one types
When Cursor’s agent changes your code, two different models are involved, and separating them is the key to how it stays fast.
The reasoning step is done by a capable model (Cursor’s in-house Composer, or a frontier model you pick). Composer is an in-house mixture-of-experts model trained with reinforcement learning across many dev environments, so it learns to run searches, fix linter errors, and write and run tests. Cursor reports generation about 4x faster than similar models, with most agent turns under 30 seconds, and is candid that frontier models still beat it on raw quality (Composer).
But a reasoning model is slow and expensive to emit a whole file, so it does not. It emits a terse edit sketch with the unchanged parts elided (“// ... existing code ...”), and a second, specialized model expands that sketch into a full-file diff. That second model is Fast Apply: a fine-tuned Llama-3-70B served with speculative edits, hitting roughly 1000 tokens/second (about 3500 characters/second), which is about 13x faster than vanilla Llama-3-70B and about 9x faster than their earlier GPT-4 apply (Fireworks writeup on Cursor’s apply model).
flowchart LR R["Reasoning model<br/>(Composer / frontier)"] -->|"edit sketch<br/>with elisions"| FA["Fast Apply<br/>(Llama-3-70B ft)"] FA -->|"full-file diff<br/>~1000 tok/s"| STAGE["Staged diff"] STAGE -->|"per-file accept / reject"| DISK["Write to disk"]
Why the file is its own draft model
Fast Apply’s speed comes from speculative edits, and the insight is elegant. Classic speculative decoding pairs a small “draft” model with a big “verifier”: the draft guesses several tokens ahead and the big model checks them in one pass. In apply, you already have a near-perfect draft: the existing file. Most of the output is identical to the current source, so you feed the original file as the speculation and let the model verify long runs of it with deterministic greedy generation, only diverging where the edit actually changes something (Fireworks writeup, Lex Fridman interview).
# The existing file is the "draft". Verify the longest prefix that still
# matches greedy generation; when it diverges, that is where the edit is.
i = 0
while i < len(original):
guess = original[i] # free speculation from the file
actual = model.greedy_next(context, prefix=output)
if actual == guess:
output.append(guess); i += 1 # matched a whole line for near-zero cost
else:
output.append(actual) # divergence = the real edit
# resync to the original after the changed region, keep speculating
i = resync(original, output)
Because most lines match, most of the output is validated in bulk rather than generated token by token, which is where the ~13x comes from. Nothing is written until you approve: Cursor stages the whole change set as one diff with per-file accept and reject (Composer).
The line to remember: the slow model decides what to change, the fast model renders how the file now reads, and the fast model is fast because the old file is 95% of the answer.
How Cursor decides what the model sees
A model can only use what is in its window, so Cursor gives you explicit controls over that window.
Rules are version-controlled instructions in .cursor/rules, written as .mdc files (Markdown with a small frontmatter). A rule can be always-on, scoped to files by glob, requested by the agent when relevant, or invoked manually (rules). The legacy single .cursorrules file still works but is superseded.
---
description: API layer conventions
globs: ["src/api/**/*.ts"]
alwaysApply: false
---
- Validate all input with Zod at the boundary.
- Never call fetch directly; use the `http` client in src/api/http.ts.
@-mentions pull specific things into context on demand: @Files and @Folders, @Terminals (terminal output), @Chats (a prior conversation), @Git (@Commit for the working diff, @Branch for the diff against main), @Browser, plus @codebase, @docs, and @web for retrieval (mentions). The guidance is simple: @-mention when you know the relevant files; otherwise let the agent find them by semantic search.
As the window fills, Cursor auto-compresses older conversation into a summary, and a “context ring” shows how full the window is, broken down by category:
flowchart LR
subgraph RING["Context window budget"]
S["System prompt"]
T["Tools"]
R["Rules"]
K["Skills"]
M["MCP"]
SUB["Subagents"]
SUM["Summarized conversation"]
C["Live conversation"]
end
The teaching point: rules are the durable, checked-in part of the context; @-mentions are the per-turn part; and the ring exists because every one of those categories competes for the same finite window.
The agent’s tools, and how you add your own
Cursor’s agent ships with a built-in tool set: read and edit files, semantic search, grep (literal string search), list a directory, run terminal commands, and access the web (Composer). The two search tools are complementary: semantic search finds code by meaning, grep finds an exact string, and a good agent reaches for the right one.
To connect tools Cursor did not build, it supports MCP (the Model Context Protocol, a standard way to expose external tools and data to a model). You configure servers in Settings or in ~/.cursor/mcp.json (global) or .cursor/mcp.json (project), over either a local stdio process or a remote SSE endpoint (MCP docs).
// .cursor/mcp.json
{
"mcpServers": {
"local-db": { "command": "npx", "args": ["-y", "@you/db-mcp"] },
"remote-api": { "url": "https://mcp.example.com/sse" }
}
}
Cursor exposes up to about 40 MCP tools to the agent and auto-selects the relevant ones per turn, and the Cursor CLI respects the same mcp.json and .cursor/rules as the editor. The idea worth keeping: MCP is a tool bus, so the same server works in the editor and the CLI without rewiring.
Two different “runs it in the background”, kept straight
People conflate these constantly, so pin them down. They solve different problems.
A Background (Cloud) Agent runs on an isolated Ubuntu VM in Cursor’s cloud: it clones your repo, works on a dedicated agent/ branch, runs commands and tests for minutes to hours, and opens a pull request for you to review, all without your laptop being on (background agents). Multiple agents run in parallel, each in its own git worktree so their file edits do not collide, which also lets you run the same task on different models and compare.
flowchart LR T["Task"] --> VM["Spin up Ubuntu VM"] VM --> CL["Clone repo"] CL --> WT["Own git worktree,<br/>agent/ branch"] WT --> RUN["Edit, run, test<br/>(minutes to hours)"] RUN --> PR["Open a PR"]
# Worktrees are why N agents can edit the same repo at once without conflict:
git worktree add ../agent-a agent/feature-a # agent A's isolated tree
git worktree add ../agent-b agent/feature-b # agent B's, fully separate files
The Shadow Workspace is a local, different thing. To let the AI see lint and language-server feedback on a proposed edit without touching your real files, Cursor spawns a hidden Electron window pointed at the same files, applies the edit there, and reads back the errors over an independent IPC channel (shadow workspace). Their proposed path to full “runnability” is a kernel-level folder proxy (FUSE on Linux) where writes go to an in-memory override store instead of disk.
flowchart LR AI["AI proposes an edit"] --> SHADOW["Hidden window<br/>(same files)"] SHADOW --> LSP["Language server / linter"] LSP -->|"errors, types"| AI NOTE["Your real files: untouched"]
Keep them apart: the Shadow Workspace is a local sandbox for getting instant lint feedback on a draft edit; Background Agents are remote VMs for long, unattended tasks that end in a PR. Cursor even reused the background-agent VM infrastructure to run the hundreds of thousands of sandboxed environments it needed to train Composer with reinforcement learning (Composer).
Which model does what
Cursor mixes frontier models (from OpenAI, Anthropic, and Google) with in-house ones, and the roster changes often, so the live source is the models doc. The mapping worth remembering is feature to model, because different jobs have different latency budgets:
| Job | Model | Why |
|---|---|---|
| Tab (next edit) | Fusion, in-house | Must answer in ~260 ms on every keystroke |
| Apply (sketch to diff) | Fast Apply, Llama-3-70B fine-tuned | Speculative edits for ~1000 tok/s |
| Agent reasoning | Composer (in-house MoE) or frontier | Runs searches, tests, multi-file edits |
Composer’s training infrastructure is public in outline: asynchronous reinforcement learning built on PyTorch and Ray, MXFP8 mixture-of-experts kernels with expert parallelism, scaling to thousands of GPUs, where the low-precision MXFP8 format gives faster inference without a separate quantization step (Composer). For model selection, a “Cursor Router” can pick a model per request under cost, balance, or intelligence modes.
Honest flag: prompt-caching specifics and the serving stack for the frontier models are not disclosed, and any version numbers date quickly, so attribute them to a date and the docs page.
Putting it together
Cursor is a stack of latency bets sitting on one architectural choice. Forking the editor is what makes the rest possible. Indexing stores vectors and locations so retrieval is fast and your source stays local. Tab predicts your next edit and jump so the editor moves with you. The agent splits every change into a slow decider and a fast applier, and the applier is fast because the old file is most of the new file. The shadow workspace gives drafts real lint feedback locally, while background agents take long jobs to the cloud and hand back a PR.
If you remember one thing: the reason Cursor feels instant while a big model thinks is that it almost never makes the big model do the fast part. Retrieval, next-edit prediction, and apply are each handled by something small and specialized, and the frontier model is spent only on the decision.