Verifying Cursor Pins
A Cursor model pin maps an OAT ladder model ID to the frontmatter selector that
materialized role files carry, for example gpt-5.6-sol-medium to
gpt-5.6-sol[reasoning=medium], or the verified exact ID
claude-sonnet-5-5-medium. Adding one to
packages/cli/src/providers/cursor/codec/catalog.ts requires evidence that
Cursor actually resolves that selector to the intended model.
This runbook produces that evidence. Run it before adding any mapping.
Why a probe is required
DR-260718-explicit-cursor-pin-mapping requires mapping-specific,
native-launch evidence before a mapping may be marked approved. Two things
that look like evidence are not:
- Structural validation. That a mapping is well-formed says nothing about what Cursor does with it.
- Agent self-report. A subagent asked to name its own model is not a reliable witness, and the probe agents in this runbook deliberately do not ask.
The catalog alone is also insufficient wherever a family carries both a thinking and a non-thinking flat ID at the same rung. Historically, Opus 5 carried both at low, medium, and high, so the bracket selector is genuinely ambiguous there and only a probe can resolve it. At rungs where only one variant exists, the catalog settles the question without a probe — but you still need the probe to prove the effort parameter was honored at all.
Evidence channel
Cursor's agent lifecycle hooks report resolution from Cursor itself. Two independent events agree:
subagentStart.subagent_model— the model the subagent will use, and the authoritative field per Cursor's hooks reference.preToolUse.model— observed from inside the subagent's own tool call.
Two channels that do not work:
- The
cursor-agentCLI and remote runtime. Agent lifecycle hooks do not fire there. A Task subagent launched from that runtime produces zero hook events, and every row records as not-reported. Run probes from the Cursor desktop app's Agent Chat. - The subagent card label. It is built from the requested bracket parameters and drops the thinking qualifier, so it cannot distinguish the thinking and non-thinking variants — precisely the distinction a probe exists to settle.
As of Cursor 3.12.30, model_params (which would carry an explicit thinking
flag) is empty and model_id is absent. Thinking is therefore established by
the resolved slug itself, which is sound because the non-thinking IDs are
distinct.
The 2026-09-23 Opus 5.5 desktop probe is an example: all five
claude-opus-5-5[effort=...] selectors resolved to matching flat IDs in
Cursor 3.20.14, while effort=ultra fell back to medium. The Sonnet 5 high control
resolved to claude-sonnet-5-thinking-high, correcting an older flat ID. The provenance summary at
packages/cli/src/providers/cursor/codec/__fixtures__/README.md identifies
the adjacent redacted summary and 32-event native JSONL records in the OAT
repository. Re-run this procedure for a new family or changed
provider behavior.
The 2026-09-25 probe on Cursor 3.21.18 approved Grok 4.6
(grok-4.6[effort=...,fast=false]) and Fable 5.1
(claude-fable-5-1[effort=...], which always resolves to the -thinking-
flat IDs). It also found a family that ignores bracket selectors entirely:
every Grok 4.7 bracket spelling tried resolved to grok-4.7-high-fast, the
account default that the unknown-family control also received, while bare
flat IDs such as grok-4.7-medium resolved exactly. Two lessons follow:
- Do not assume a new version keeps its predecessor's selector syntax. Probe the syntax, not just the rungs.
- A fallback can look like success. When the account default belongs to
the family under test, compare every subject row with the unknown-family
control and probe non-default rungs. Here even
effort=low,fast=falselanded on the high-fast default.
The 2026-10-01 revision of DR-260718-explicit-cursor-pin-mapping permits
verified exact-ID entries as well as bracket selectors. Grok 4.7 remains
outside the shipped catalog until its own entry is added and approved. The
redacted evidence is in the same fixtures directory.
The 2026-10-01 Sonnet 5.5 probe on Cursor 3.22.12 confirmed the same syntax
distinction: every claude-sonnet-5-5[effort=...] selector fell back to
grok-4.7-high-fast, but all five exact IDs
claude-sonnet-5-5-low|medium|high|xhigh|max resolved correctly. Both rounds
included a working Opus 5.5 low control and unknown-family and unsupported-effort
controls that fell back. The summary and 64 correlated native events are
retained beside the earlier fixtures.
Cursor documents specific model IDs and bracket parameters. Choose a candidate from the live catalog, then probe its exact frontmatter spelling. Documentation and catalog presence do not replace native evidence.
Prerequisites
- Cursor desktop app, with the repository open as a workspace.
- The live catalog for the families you intend to pin:
cursor-agent modelsRead the full output rather than truncating it. You need to see whether a thinking and a non-thinking ID both exist at each rung, and which rung carries the family's unqualified display label.
Setting up capture
Both files are temporary. Delete them when the probe is done.
Create .cursor/hooks/g01-capture.sh:
#!/bin/bash
# TEMPORARY pin-probe capture hook. Delete after probing.
set -u
OUT_DIR="/tmp/g01-probe"
OUT="$OUT_DIR/hooks.jsonl"
mkdir -p "$OUT_DIR" 2>/dev/null || :
input=$(cat)
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
printf '{"captured_at":"%s","payload":%s}\n' "$ts" "$input" >>"$OUT" 2>/dev/null || :
event=$(printf '%s' "$input" |
sed -n 's/.*"hook_event_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
# Observe only. Never block, never fail closed.
case "$event" in
subagentStop) printf '{}\n' ;;
*) printf '{"permission":"allow"}\n' ;;
esac
exit 0Make it executable, then register it in .cursor/hooks.json for
subagentStart, subagentStop, and preToolUse:
{
"version": 1,
"hooks": {
"subagentStart": [
{ "command": ".cursor/hooks/g01-capture.sh", "timeout": 10 }
],
"subagentStop": [
{ "command": ".cursor/hooks/g01-capture.sh", "timeout": 10 }
],
"preToolUse": [{ "command": ".cursor/hooks/g01-capture.sh", "timeout": 10 }]
}
}The hook must observe only. A capture hook that can block or fail closed can change the behavior it is supposed to measure.
Restart Cursor or reload the window so the hooks register.
Writing probe agents
One throwaway agent per selector, under .cursor/agents/. Keep them trivial:
the probe is the hook payload, not anything the agent produces.
---
# TEMPORARY pin probe - not oat-managed. Delete after probing.
# Class: subject - expect gpt-5.6-sol-medium
name: zz-pin-probe-sol56-medium
description: Temporary pin probe (subject) for gpt-5.6-sol[reasoning=medium]. Delete after use.
model: gpt-5.6-sol[reasoning=medium]
---
## Role
You are a throwaway pin probe. Run exactly one command and then stop:
```bash
echo PIN-PROBE zz-pin-probe-sol56-medium
```
Then reply with the single word `done`. Do not read files or use any
other tool.
Cursor's hook payload is the evidence for this probe. Your own claim about
your identity is not evidence and is deliberately not requested.The echo exists to force a tool call, which is what produces the
corroborating preToolUse event.
Subjects and controls
Probe every rung you intend to pin. Then add controls, which are what make the subject results interpretable:
| Class | Example selector | Proves |
|---|---|---|
| Subject | gpt-5.6-sol[reasoning=medium] | The mapping resolves as intended |
| Positive control | claude-sonnet-5[effort=high] | Reproduces an already-verified mapping |
| Negative control | claude-opus-9[effort=high] | Unknown family — the channel reports resolution, not the request |
| Negative control | gpt-5.6-sol[reasoning=unknown] | Unknown effort — exposes rung-level fallback |
The negative controls carry most of the interpretive weight. Without them you cannot distinguish a hook that reports what Cursor actually resolved from one that merely echoes what you asked for. A bogus family that comes back as the bogus family would invalidate the entire run.
Running
Launch each probe agent from the Cursor desktop app's Agent Chat, as a subagent with no model override, so Cursor resolves from the agent's frontmatter. Let each finish before starting the next.
Then read the resolutions:
python3 - <<'PY'
import json
for line in open('/tmp/g01-probe/hooks.jsonl'):
p = json.loads(line).get('payload', {})
if p.get('hook_event_name') == 'subagentStart':
print(p.get('subagent_name'), '->', p.get('subagent_model'))
PYInterpreting results
The following Opus 5 observations document earlier probes; Opus 5 is retired from current mappings. Use the current live catalogue for new selectors.
Unresolvable components fall back silently
Cursor does not reject a malformed pin. It substitutes a default for whichever component it cannot resolve, with no error or warning:
- An unknown family falls back to the account default model. Probing
claude-opus-9[effort=high]resolved tocursor-grok-4.5-high-fast. - An unknown effort falls back to that family's default rung. Probing
claude-opus-5[effort=ultra]resolved toclaude-opus-5-thinking-high.
This makes a typo in a selector more dangerous than a plain downgrade: the pin silently tracks whatever Cursor currently designates as the default, so a vendor-side change alters capability with no change in this repository.
The default rung is family-specific
Exactly one rung per family carries an unqualified display label in the live
catalog, and that marks the default. It is not always high:
| Family | Default rung | Unqualified label | Contrast |
|---|---|---|---|
| Opus 5 | high | Opus 5 1M | Opus 5 1M Medium |
| Opus 4.8 | high | Opus 4.8 1M | Opus 4.8 1M Extra High |
| Opus 4.7 | xhigh | Opus 4.7 1M | Opus 4.7 1M High |
Opus 4.7 is the disconfirming case for a naive "high is always the default" reading.
A default-rung result proves nothing on its own
If the rung you requested happens to be the family default, the observation is equally consistent with the effort parameter being honored and with it being ignored. That row cannot stand alone.
Always probe at least one non-default rung in the same family. A set of non-default rungs that each resolve as requested is what demonstrates the parameter is genuinely honored; a default-rung row is then sound by inference across the set rather than from its own evidence.
Recording the result
Add the mapping with a probe record, which ties it to the evidence that approved it:
approvedMapping(
'claude-sonnet-5-5-medium',
'claude-sonnet-5-5-medium',
'explicit-model-id',
{
probeName: 'zz-pin-probe-sonnet55-flat-medium',
probeRecord: {
submittedSelector: 'claude-sonnet-5-5-medium',
resolvedModel: 'claude-sonnet-5-5-medium',
verifiedAt: '2026-10-01',
evidencePath:
'packages/cli/src/providers/cursor/codec/__fixtures__/cursor-pin-probe-2026-10-01.jsonl',
},
},
),The record asserts that submittedSelector equals the mapping's
frontmatterModel and resolvedModel equals its ladderModelId. A mapping
edited later without re-probing therefore fails its own consistency test rather
than inheriting an approval that never covered the new selector.
Retain the raw payloads alongside the summary. They are the primary evidence; the summary is an interpretation of them.
For an exact-ID mapping, use the explicit-model-id syntax family and a
mapping-specific probe record. The frontmatter selector must equal the ladder
ID, and the independently captured submitted selector and resolved model must
agree with both. Missing or mismatched evidence must fail materialization;
there is no generic model-ID pass-through. Preserve rejected selector results
and control observations alongside the successful rungs.
Cleanup
Delete every temporary artifact:
.cursor/hooks.jsonand.cursor/hooks/g01-capture.sh- all
zz-pin-probe-*agent files - the capture file under
/tmp
Before committing retained payloads, redact machine and identity fields.
Cursor hook payloads carry user_email, workspace_roots, and an absolute
transcript_path. None carries probe evidence — resolution is read from
subagent_model — so removing them costs nothing and keeps private data out of
the repository.
Confirm the workspace is clean:
oat sync --scope projectRelated
- Smoke Testing - Runbook for the live workflow smoke runner.
- Dispatch Policy - How pins reach the recommendation and role files.
- Provider Sync - Provider-specific sync behavior.