Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
- Add the `kebab-case-skills` assembly rule, the full skill-tree normalization of path, frontmatter name, and link retargeting, applied to skills alone.
- Enable `kebab-case-skills` on the agentskills provider, because the AgentSkills specification requires lowercase names matching the skill directory.
- Deploy authored casing verbatim on every other provider.
- Add `cursor` to `rune run`: `cursor-agent` in print mode, ask mode when read-only, its own sandbox when writing, and a hint when the sandbox hides the login.
- Require `CURSOR_API_KEY` for `rune run cursor --clean`, which moves `HOME` and `CURSOR_CONFIG_DIR` into the clean root.
- Keep only `--endpoint` from a Cursor profile in `rune run`, and refuse a read-only run when Cursor's `approvalMode` is `unrestricted`.
- Add `cursor` to `rune launch`, which starts `cursor-agent` unless `tools.cursor.binary` names another binary.
- Add a built-in `fable@cursor` profile that selects `claude-fable-5-1-high`. A configured profile of that name replaces it.

### Changed

Expand Down
61 changes: 61 additions & 0 deletions docs/changes/cursor-agent-surface/adr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
title: "Cursor runs through its official agent CLI"
description: "rune run and rune launch reach Cursor only through cursor-agent in print mode with JSON output, map the run modes onto ask mode and Cursor's sandbox, and require an API key for clean runs"
type: adr
category: architecture
tags:
- cli
- run
- launch
status: proposed
created: 2026-09-29
updated: 2026-10-02
author: "@N4M3Z"
project: cli
related:
- "CLI-0024 Interactive and Automated Tool Commands"
- "CLI-0025 Shared Coding Tool Process Supervisor"
responsible: ["@N4M3Z"]
accountable: ["@N4M3Z"]
consulted: ["claude-fable-5-1@claude", "gpt-6-astra@codex", "grok-4.7@claude"]
informed: []
upstream: []
change: cursor-agent-surface
---

# Cursor runs through its official agent CLI

## Context and Problem Statement

The owner wants Cursor models in `rune run` and `rune launch`. Cursor offers two ways in. The official `cursor-agent` CLI has a print mode with one JSON result object. The editor talks to Cursor's backend over a private protocol, and Cursor's terms forbid access through it ([Cursor terms §1.5][TOS]). Claude models on Cursor bill at API price from Cursor's own pool ([Cursor models][MODELS]), so a Cursor route is an extra account, not a cheaper one.

The CLI differs from the other tools in four ways that shape the adapter. It has no system prompt flag. Its read-only mode, `--mode ask`, is enforced by the CLI and not by the operating system, and the model keeps its Shell and Write tools under it. It accepts options that its help does not list, such as `--data-dir` and `--allowed-tools`. It reads its login from the macOS Keychain, which a process inside the Claude Code sandbox cannot reach, and it reads `~/.cursor/rules` even when `CURSOR_CONFIG_DIR` points elsewhere. Each fact was checked against `cursor-agent` on 2026-09-29.

## Considered Options

1. Drive `cursor-agent -p --output-format json` as a sixth adapter, with the prompt on standard input.
2. Talk to Cursor's backend directly with a client for its private protocol.
3. Keep Cursor out of rune and let each user write a wrapper script.

For a clean run there were three further options: swap `HOME` and accept the lost login, set only `CURSOR_CONFIG_DIR`, or require `CURSOR_API_KEY`.

## Decision Outcome

Option 1. `rune run cursor` MUST start `cursor-agent` with `-p --output-format json --workspace <repo> --trust`, send the system prompt and the prompt on standard input, and read `result`, `is_error`, and `usage.outputTokens`. A read-only run MUST add `--mode ask` and MUST refuse when Cursor's saved `approvalMode` is `unrestricted`, one of the three values that Cursor documents ([Cursor CLI configuration][CONFIG]). A workspace-write run MUST add `--force --sandbox enabled`. A profile MUST keep only `-e` and `--endpoint`. `rune launch cursor` MUST start `cursor-agent` unless `tools.cursor.binary` says otherwise. A clean run MUST require `CURSOR_API_KEY`, set `HOME` and `CURSOR_CONFIG_DIR` inside the clean root, and refuse with a configuration error when the key is missing. `fable@cursor` MUST work without config: a built-in profile selects the built-in route `fable-cursor`, `claude-fable-5-1-high`, and a configured profile or route of the same name replaces it.

Option 2 breaks the terms and would break with every Cursor release. Option 3 is the wrapper that the 2026-09-26 retirement of vendor-named shims removed. For clean runs, a `HOME` swap alone fails with "Authentication required", and `CURSOR_CONFIG_DIR` alone still loads the owner's rules, so neither gives a clean run.

## Consequences

- Cursor models are reachable from `rune run` under the same owned-argument filter, JSON output, and failure kinds as the other tools.
- A read-only Cursor run relies on `cursor-agent` enforcing ask mode. Three live read-only runs on 2026-10-01 that asked for a file wrote nothing, under `approvalMode` `auto-review` and under a copied `unrestricted` config, and the model named its Shell and Write tools as present but forbidden. No kernel sandbox backs the mode, because `--sandbox enabled` without `--force` asks for approval that print mode cannot give. A caller who needs a kernel boundary for a read-only run must pick another tool. The refusal of `unrestricted` is defense in depth against that enforcement failing: an owner with that saved mode must choose `allowlist`, `auto-review`, or a workspace-write run.
- A Cursor profile cannot pass headers, `--api-key`, or any flag other than the endpoint flag. The key goes in the profile `env` as `CURSOR_API_KEY`. A profile that needs more must use `rune launch`, which passes profile arguments unchanged.
- `--clean` costs the owner an API key. The key lives in the environment, never in the argument list, so it does not appear in `ps` output or in the `rune run --dry-run` argument list.
- A clean run drops the owner's `~/.cursor/hooks`, the same as a clean run of any other tool drops its hooks.
- `cursor-agent` fails inside the Claude Code sandbox. rune adds a hint to that failure but does not leave the sandbox itself. The harness must exclude `rune`, and the owner's template must list `cursor-agent` for direct calls.
- The built-in profile table is new. It is the first profile that rune supplies without config, because Cursor names each Fable effort level as its own id and no tool-neutral `fable` route can serve it. Cursor models on Cursor's list carry the label "NO ZDR", so a `fable@cursor` prompt is not covered by zero data retention.
- Cursor does not document the `usage` block. If it changes, completion tokens become absent, and the run still succeeds.

[TOS]: https://cursor.com/terms-of-service
[MODELS]: https://cursor.com/docs/models
[CONFIG]: https://cursor.com/docs/cli/reference/configuration
24 changes: 24 additions & 0 deletions docs/changes/cursor-agent-surface/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Cursor agent surface: design

## Approach

Cursor joins `rune run` as a sixth tool in the existing adapter set: one `Surface::Cursor` variant, one row in each table, and one module with the argument builder and the parser. It uses the same process request, owned-argument filter, and failure type as the other tools. The alternative was a generic "JSON print tool" adapter driven by config. It lost because each tool differs in prompt transport, access flags, and error shape, and the owned table is the security boundary, which config must not widen.

## Structure

- `src/cli/surface/cursor.rs`: `BOOLEAN_OPTIONS` and `OWNED_OPTIONS` hold the owned flags, including `--plugin-dir`, because a plugin can add tools. `keep_allowed` then keeps only the flags in `KEPT_OPTIONS`, `-e` and `--endpoint`, and drops every other profile argument, because `cursor-agent` accepts options that its help does not list and because `--api-key` would put the key in `ps`. Its warnings name a flag or a position, never a value. `check_access` refuses a read-only run when the `cli-config.json` in `config_dir` sets `approvalMode` to `unrestricted`. `config_dir` resolves the directory as `cursor-agent` does: `CURSOR_CONFIG_DIR`, then `$XDG_CONFIG_HOME/cursor`, then `$HOME/.cursor`, reading the launch environment before rune's own. A kept flag without a plain value is dropped, because commander takes the next token as the value whatever it is. `args` builds the argument list from the invocation. `prompt_input` puts the system prompt in front of the prompt. `invoke` sends that text on standard input, and `parse` reads the last result object. `prepare_clean_state` returns the configuration error when `CURSOR_API_KEY` is missing. `sandbox_hint` decides whether standard error shows the Keychain failure of a sandboxed process, and `with_sandbox_hint` puts the hint in front of that standard error.
- `src/cli/surface/mod.rs`: the `Surface::Cursor` variant, `from_tool("cursor")`, the name, the table dispatch, the `prepare_clean_state` and `process_request` match cases, and the `invoke_surface` dispatch. The filter uses `drop_whole`, because Cursor has no generic config channel, and passes its result through `keep_allowed`. `check_access` dispatches the access check, and `run/mod.rs` calls it before the tool starts, so a refusal is a configuration error with exit 2.
- `src/cli/launch/mod.rs`: `cursor` in `KNOWN_TOOLS`, and `default_binary(name)`, which maps `cursor` to `cursor-agent`. Both `resolve_tool` and `list_tools` call it, so the list cannot disagree with the launch. `BUILT_IN_PROFILES` holds the `fable@cursor` profile, which selects the built-in route `fable-cursor` (`claude-fable-5-1-high`). Cursor needs this table because it names each Fable effort level as its own id, so no tool-neutral `fable` route can serve it. `resolve_profile` checks the config first, then this table, and `profile_names` merges both for the missing-profile error and the tool list. The built-in routes move into one `BUILT_IN_ROUTES` table, which both the lookup and the unknown-route error read. `model_args` puts `--model <id>` in front of the profile arguments of a Cursor launch and records the pair in `ResolvedLaunch::model_args`. `rune run` strips that exact prefix, because it passes the model itself, so no owned-argument warning fires for a model that rune generated.
- `src/cli/run/mod.rs` and the `run` help text: the supported-tool message and the examples name Cursor.

The argument list for a run is `-p --output-format json --workspace <repo> --trust`, then `--mode ask` or `--force --sandbox enabled`, then `--model <id>` when a model is set, then the kept profile arguments, in the same order as the other tools. The prompt goes on standard input with the system prompt in front of it.

For a clean run the process request sets `HOME` to the clean root and `CURSOR_CONFIG_DIR` to `<root>/.cursor`. The API key stays in the environment. `has_api_key` checks the launch environment, which holds profile `env` values, before rune's own, because the child gets the launch environment last.

## Risks

- **A new Cursor flag widens access.** The allow-list covers it: a profile keeps only the account and endpoint flags, so a new or unlisted flag never reaches the tool. The owned table stays for its value-aware warnings. The bypass table test has Cursor rows for owned and unlisted flags.
- **Ask mode is a CLI rule, not a kernel boundary.** `cursor-agent` enforces it: every live read-only run that asked for a file wrote nothing, including one under a copied `unrestricted` config. The model still lists Shell, Write, and StrReplace as its tools under ask mode. `check_access` refuses `unrestricted` as defense in depth, because it is the one approval mode that would run such a call without asking if the enforcement failed. A read-only run cannot add a kernel sandbox, because `--sandbox enabled` without `--force` still asks for approval, and print mode cannot answer. The ADR records this limit.
- **The JSON shape changes.** Cursor does not document `usage`. The parser makes `usage`, `subtype`, and `is_error` optional. A line without `type: result` or with a non-string `result` is not a result object. An empty or missing `result` fails as an empty reply.
- **The sandbox hint misfires.** The hint matches the certificate-store text, or "Authentication required" only while `SANDBOX_RUNTIME` is set. Outside the sandbox, a real missing login gets no hint.
- **A clean home drops the owner's Cursor hooks.** `~/.cursor/hooks` holds the owner's shell guard. A clean run loses it by design, like every other clean run. Workspace-write clean runs rely on Cursor's own sandbox.
38 changes: 38 additions & 0 deletions docs/changes/cursor-agent-surface/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
adr: docs/changes/cursor-agent-surface/adr.md
status: proposed
decisions: ["Cursor runs through its official agent CLI"]
---

# Cursor agent surface

## Why

Cursor sells access to models that the local proxy does not serve, and its official command-line agent, `cursor-agent`, has a print mode with JSON output. `rune run` supports claude, codex, agy, grok, and opencode, so a Cursor model today needs a hand-written wrapper around `cursor-agent`, with its own flags for access and output. The owner asked on 2026-09-29 for Cursor as a `rune run` *surface*, which is a harness that the run can start and read a result from, and as a `rune launch` tool, through the official CLI only. Cursor's terms forbid access to the service through its private protocol ([Cursor terms §1.5][TOS]).

## What Changes

- `rune launch cursor` and `rune launch <profile>@cursor` start `cursor-agent`. The default binary is not the tool name, because `cursor` on a Mac opens the editor. `tools.cursor.binary` still overrides it.
- `rune run cursor` starts `cursor-agent -p --output-format json --workspace <repo> --trust` and sends the prompt on standard input. Read-only adds `--mode ask`. Workspace-write adds `--force --sandbox enabled`. The run reads `result`, `is_error`, and `usage.outputTokens` from the one JSON object Cursor prints.
- Cursor has no system prompt flag, so the run puts the system prompt in front of the prompt, as it does for codex and agy.
- The flags the run sets for Cursor join the owned table, so a profile that carries `--yolo`, `--force`, or `--mode` loses them with a warning. Beyond that table, a profile keeps only `-e` and `--endpoint`, because `cursor-agent` accepts options that its help does not list and reads a positional as a subcommand or as the prompt.
- A read-only run refuses when Cursor's saved `approvalMode` is `unrestricted`, as defense in depth: it is the one mode that would run a write without asking if ask mode failed.
- `fable@cursor` works without config through a built-in profile and the built-in route `fable-cursor`, `claude-fable-5-1-high`. A configured profile or route of the same name replaces it. A Cursor launch passes the route's model as `--model`.
- `rune run cursor --clean` needs `CURSOR_API_KEY` in the environment. With the key, the run points `HOME` and `CURSOR_CONFIG_DIR` at the clean root. Without it, the run refuses with a configuration error. The reason is in the ADR: Cursor keeps its login in the macOS Keychain and reads `~/.cursor/rules` whatever `CURSOR_CONFIG_DIR` says.
- A Cursor failure that shows the Keychain signature of a sandboxed process gets a hint: run rune as a bare command outside the harness sandbox.

## Capabilities

- harness-run-command (modified)
- harness-launch-command (modified)

## Impact

- `src/cli/surface/`: a Cursor module with the argument builder, the response parser, the clean-state rules, and the sandbox hint, plus the `Surface` enum, the owned and boolean tables, and the dispatch in `mod.rs`.
- `src/cli/launch/mod.rs`: `cursor` in the known tools, one default-binary function that both the resolver and the tool list use, `BUILT_IN_PROFILES` with `resolve_profile` and `profile_names`, the `fable-cursor` route, and `model_args` for the Cursor launch.
- `src/cli/run/mod.rs` and the `run` help text: the supported-tool message names Cursor.
- `CHANGELOG.md` and the proof under `docs/proofs/cursor-agent-surface/`.
- Outside the repository: the Claude Code settings template gains `cursor-agent *` beside the other harness binaries in `sandbox.excludedCommands`. `rune *` is on that list already, so a bare `rune run cursor` starts outside the sandbox.
- The bench keeps its provider list. Cursor in the bench is a separate change.

[TOS]: https://cursor.com/terms-of-service
Loading
Loading