The cursor backend
What a Cursor run executes, how its model and effort are named, how a Cursor seat is logged in and seated, where it is confined, and what to do when a spawn is refused.
For whoever seats the cursor backend on a machine and writes the orders that run on it.
After reading it you can name a cursor model correctly in an order, log a cursor seat in or give
it a key, have it seated, and act on every refusal a cursor spawn can give you.
Before you start: an admitted machine (saphan machine admit) and an
account with the vendor of cursor-agent.
What it runs
The cursor backend drives Cursor's command-line agent, cursor-agent. Cursor's installer puts
the same program on disk under two names, cursor-agent and agent; the engine runs the absolute path
you register for the machine and never looks the program up on PATH.
| backend name | cursor — - backend: cursor in an order, --backend cursor on a seat |
| how it is run | headless, one prompt per run, its output streamed as JSON lines that the run record and the console transcript read |
| the CLI version | one generation is pinned, exactly: 2026.10.14-941492e. A seat whose program reports any other version is refused cursor-version-skew before the spawn. ⚠ The pinned package exists for Linux x86-64 and macOS Apple silicon only |
| Cursor's own sandbox | switched off on every run, because nested inside the engine's confinement it breaks the run. The engine's confinement is the boundary — see below |
| self-update | switched off on every run |
| the model endpoint | the run is pointed at the guardrail proxy on Cursor's own command line, so its model traffic passes through that proxy |
| egress | the shipped baseline admits one host, api2.cursor.sh on port 443, reached through that proxy — Egress policies |
| grant files | two, written into every run's worktree: .cursor/cli.json (the role's permission block) and .cursor/hooks.json (the hook that answers each tool call with the engine's verdict). A run that cannot be given the hook file is refused cursor-hooks-unavailable — Agent grants |
| money | ⚠ Cursor reports no cost per run, so --budget-usd is recorded and not enforced; the bound on a Cursor run is its time limit. A model with no row in the pricing registry is recorded as unpriced, never as zero, and the spawn warns |
| allowance | saphan fleet usage reads a Cursor seat's included allowance from the CLI's own usage panel without spending a model turn, and ranks the seat by headroom like any other — saphan fleet |
Naming a model and its effort
An order names the model without an effort, and the effort on its own line — exactly as for every other backend:
The cursor-agent CLI has no effort switch: it spells the model and the effort as one id. The engine
composes that id for you — model grok-4.7 with effort high runs as --model grok-4.7-high.
- ⛔ Never type the effort into the model (
- model: grok-4.7-high). The effort is a separate setting, and a price row matches the model without it, so onegrok-4.7row prices every effort. - ⛔ Never use the bracket form
grok-4.7[effort=high]. Cursor's own help shows it for another model; for Grok, the vendor refused it. - ⛔ Never pick a
-fastvariant. - ⛔
autois refused (cursor-leg-token-forbidden): Cursor resolves it on its side to another vendor's model, so the run would not run the model the order pinned.
The efforts an order may name for Cursor: low, medium, high, xhigh. Anything else is refused
order-effort-not-accepted before any spawn.
Where the effort comes from, first to last: --effort, the order's - effort:, then the
configured default dispatch.effort.<role>.cursor — for an executor, dispatch.effort.executor.cursor.
A default model is dispatch.model.<role>.cursor; with one set, an order may name only - backend:.
Which models a cursor run may use is a list you keep, and a pair not on it is refused at dispatch:
⚠ The value replaces the list, so write every id you still want. The ids are the ones
HOME=<seat home> cursor-agent --list-models prints, with the effort suffix removed.
⚠ There is no rehearsal of this check today. saphan dry-run dispatch place is refused
(dry-run-no-rehearsal-path), and saphan dispatch check --model checks only the capability
catalogue, so it passes a model the accepted list will refuse. Read saphan config show before you
hand an order over.
Where it runs, and where it is confined
| platform | does the engine confine it? | exercised |
|---|---|---|
| Linux x86-64, local or reached over ssh | yes — Landlock, with Cursor's own sandbox switched off, so the engine's jail is the only boundary | yes — a confined run completed on a remote Linux x86-64 machine, at CLI 2026.10.14-941492e |
| macOS Apple silicon, a local run | yes — Seatbelt, with the same grant as on Linux | ⚠ the confinement was rehearsed on macOS up to Cursor's sign-in check with no sandbox denial; a completed confined model turn on macOS is not yet in the evidence |
| macOS reached over ssh | ⛔ no — the run record says no engine boundary was claimed, and a run that declares an egress policy is refused by name | not exercised: no such seat has been measured |
What the jail grants inside the seat's home is only what cursor-agent was measured writing there:
.cursor, .npm, and the data directory the engine points Cursor at.
⚠ Not covered, and not measured rather than known harmless: cursor-agent also writes session logs and
run markers in the system temporary directory. Those writes are not granted, and whether denying them
affects a turn has not been measured.
⚠ The prompt rides the cursor-agent command line. A brief that makes the composed prompt 131 072 bytes or
longer is refused cursor-prompt-too-large-for-argv.
Attribution is off: the agent signs none of your commits
Before every cursor spawn — local or remote, behind the proxy or not — the engine sets two keys in
the seat's own Cursor settings file, <seat home>/.cursor/cli-config.json:
Every other key of that file is kept. So a commit or a pull request made by a cursor run carries no
co-author trailer naming the agent — the cursor-agent CLI was found set to attribute on every seat, and this
is why the engine writes the setting itself rather than trusting the seat.
If the engine cannot read that file as JSON, or cannot write it, the spawn is refused
cursor-seat-http1-unavailable (the same refusal guards the HTTP/1.1 key a run behind the proxy
needs), and the refusal names the keys. Repair or remove the file — a file holding valid JSON, or no file
at all, is accepted — and dispatch again.
How a cursor seat authenticates
The platform of the machine that runs the leg decides it:
| seat on | authenticates by | expiry |
|---|---|---|
| Linux | Cursor's own stored login under the seat's home, <seat home>/.config/cursor/auth.json, made with saphan machine login | the login ends; the engine reads when, before every spawn |
| macOS | an API key, because Cursor's browser login cannot be stored under a seat's home on macOS (Cursor's write to the macOS keychain fails) | none the engine can read; a revoked key surfaces at Cursor |
⛔ There is no fallback. A macOS cursor seat with no key is refused, never run on whatever login the
machine happens to hold. And the dispatcher's own CURSOR_API_KEY never reaches a run: the run's
environment is built, not inherited, and --env CURSOR_API_KEY is refused by name.
The ceremony for a cursor seat, numbered
Every step here is a human act. Steps 2 and 3 are the owner's.
Install and seat (once per seat)
-
Install Cursor's CLI at the pinned version (
2026.10.14-941492e) on the machine that will run the seat. ⚠ The vendor's own installer installs the newest version, and a seat on any other version is refusedcursor-version-skew. -
Register the program's absolute path for that machine — an owner act, confirmed with a one-time code:
Skip this if
saphan machine listalready shows acursorprogram for the machine, or if you passed the same--binary cursor=…tosaphan machine admit. -
Seat it — the owner's act, signed with the root key. A
cursorseat's profile directory is its home: Cursor keys its login on$HOME, so--config-diris the home the engine binds every time it spawns or logs in that seat. -
Accept the model if it is not already on the list:
saphan config set probe.model.cursor <every id you want>.
Then follow one of the two acts below, by the seat's platform.
Log a Linux seat in (the first time, and whenever the engine says the login ended)
-
From a terminal, run:
The engine runs
<registered program> loginunder the seat's own home — in place for a local seat, over its own ssh connection with a terminal for a remote one — so you never type a path or ssh anywhere yourself. It prints theHOME=<value>it bound, which is the same value every spawn of the seat binds. Without a terminal it is refusedmachine-login-no-operator. -
Complete the flow
cursor-agentprompts for: open the URL it prints in a browser on your own machine, finish there, and let the CLI exit. A non-zero exit is refusedmachine-login-abandoned, and nothing is written. -
Read the verdict. The engine then sends one short real prompt under the same home — it costs one small model turn on the seat's allowance.
ALIVEis the only pass; anything else is refusedmachine-login-unverifiedand names what to do. Cursor's own status command is not used: it reads a cached login and can report "logged in" over a login the vendor no longer accepts. -
Tell the record: run the
saphan seat auth-performed --identity <seat> --machine <machine-id> --by <person>line the verb prints. It does not write that entry itself.
To measure a seat later without logging in again, add --verify-only to step 5's line; it sends the same
one prompt.
Give a macOS seat its key
saphan machine login on a macOS Cursor seat is refused by name, and the refusal points here.
-
Create a key in the vendor's dashboard and copy it once; the dashboard will not show it again. ⚠ Two seats may share one key, but then they are one identity to the vendor — give each seat its own entry name in step 6 either way.
-
Add the key to
credentials.jsonon the machine that runs the leg — the file thecredentials.filerow ofsaphan config shownames, mode0600, a flat object of strings (Secrets):⛔ Never paste the key on a command line or into a configuration key: a shell history is a file.
-
Name the entry on the seat — the entry's name, never the key:
-
Check without spending:
saphan config showlistsseat.<seat>.credentialwith its value redacted. Do not dispatch a test turn to check the key — it spends the seat's allowance; the first real dispatch is the proof.
Rotating or revoking: replace the value in credentials.json; the next dispatch reads it fresh.
Revoke it in the vendor's dashboard.
⚠ A key reaches a run only on a local run. A key-based seat reached over ssh is refused
cursor-seat-credential-transport, because the remote command line has no carrier that puts a key in
the run's environment alone.
When a spawn is refused
| what you see | what happened | what you do |
|---|---|---|
"the stored Cursor login on … ended … seconds ago … Refused: this Cursor spawn, before it started" — the pre-spawn seat check, rule R23 | a Linux seat's stored login has expired. The engine reads its expiry before every spawn and attempts no refresh | log the seat in again — steps 5–8 — then dispatch again |
| a note on a spawn that proceeded: "cursor login on … ends in N days" | under 7 days of login left | log the seat in again before then |
| a note on a spawn that proceeded: the Cursor login "was NOT measured" | the expiry could not be read. This is an unread axis, not health | saphan machine login … --verify-only for that seat |
"cursor · <model> is not a pair the config accepts (probe.model.cursor)" | the order's model is not on the accepted list | saphan config show, then saphan config set probe.model.cursor <every id you want> — step 4 |
order-effort-not-accepted | the order's effort is not low, medium, high or xhigh | change the order's - effort:, or remove it to take the configured default |
cursor-leg-token-forbidden | the run would carry something a governed Cursor run may not — for a model, auto | name a concrete model |
cursor-version-skew | the seat's program is not the pinned CLI version | install the pinned version on that machine — step 1 |
cursor-seat-credential-unset | a key-based seat has no seat.<seat>.credential | steps 5–7 of the macOS act |
cursor-seat-credential-absent | the named entry left credentials.json between the configuration load and the spawn | add the entry, or name one that exists |
cursor-seat-credential-empty | the entry's value is empty | put the issued key in the entry |
cursor-seat-credential-transport | a key-based seat is reached over ssh | run the leg on the machine that holds the seat, or remove seat.<seat>.credential to keep an ssh seat on its home login |
credentials-name-unknown — on any command | the configuration names an entry credentials.json does not carry | add the entry, or correct the name |
credentials-name-is-a-key | a value shaped like a Cursor key was typed where the entry's name belongs; the refusal never prints it | put the key in credentials.json and name its entry |
cursor-api-key-env-inherited / seat-env-collision | --env CURSOR_API_KEY, or a permission class minting it | drop it; the seat's key is delivered by the engine alone |
cursor-seat-http1-unavailable | the engine could not read or write the seat's cli-config.json (attribution and, behind the proxy, HTTP/1.1) | repair or remove that file, then dispatch again |
cursor-prompt-too-large-for-argv | the composed prompt reached 131 072 bytes | shorten the brief |
machine-login-no-operator · machine-login-abandoned · machine-login-unverified | the login act itself | run from a terminal · re-run (nothing was written) · check the home the verb printed; re-run or --verify-only |
What this page does not prove
- ⚠ A completed confined model turn on macOS is not yet in the evidence; the macOS confinement is
rehearsed up to the
cursor-agentsign-in check. - ⚠ A remote Mac is not claimed as confined, and no such seat has been run.
- ⚠ Whether
cursor-agentrenews an expired login by itself is not measured. The engine refuses an expired login rather than find out; if it does renew, that refusal is stricter than it needs to be. - ⚠ A newer
cursor-agentCLI is refused until the pinned version is measured again — the flags, the output shape and the refusals above are read from one generation.
Adding a vendor CLI to an image
Four ready build files, the three properties they all share, and what each CLI does behind a proxy — because two of the four ignore the variable most people reach for first.
Android images: a JDK, not an SDK
The android class image carries a JDK and no Android SDK, why that will not change, and the permission class written against a default SDK install.