OpenSpec: change proposals and living specs
OpenSpec is an open-source spec layer from Fission AI for coding agents working on existing code. Each change gets a proposal, requirement deltas and a task list; after the agent implements it, archiving merges the deltas into openspec/specs/, a living description of how the system behaves now. OpenSpec runs in Claude Code, Codex and Cursor.
Your password-reset endpoint is six years old, nobody wrote down how it behaves, and security wants a rate limit on it this sprint and a shorter token lifetime next sprint. You could hand the agent a ticket and review a diff, or you could run Spec Kit and produce a spec, plan, research notes and contracts for a two-hour change. Neither leaves you with a written, current answer to “what does password reset do today?”
This page is for the developer who runs the agent on a brownfield codebase. It takes those two real changes through OpenSpec, shows the exact files and commands, and ends with a rule for when Spec Kit is the better choice.
What you get from running OpenSpec on an existing codebase
Section titled “What you get from running OpenSpec on an existing codebase”- A working install for Claude Code, Codex and Cursor in one repository, and each tool’s command spelling.
- The rule that decides your first change on code with no specs: only
ADDEDrequirements can create a spec, and why that makes characterization tests step zero. - Two real deltas for the password-reset endpoint: one that creates the spec, one that modifies and removes requirements in it.
- Copy-paste prompts for explore, propose, apply and the pull request.
- A CI job that fails a malformed change and an archive that forgot to update the living spec.
- A decision table for OpenSpec versus Spec Kit when you already have code.
How does OpenSpec’s change model work?
Section titled “How does OpenSpec’s change model work?”OpenSpec keeps two things apart: what the system does now (openspec/specs/) and what one change will alter (openspec/changes/<name>/). This is the tree after the first change in this tutorial has been archived and the second one proposed:
Directoryopenspec/
- config.yaml schema
spec-driven, optionalcontext:andrules: Directoryspecs/
- password-reset/spec.md the living spec, written only by archive
Directorychanges/
Directoryshorten-reset-token-ttl/ in flight
- proposal.md why, what changes, which capabilities
- specs/password-reset/spec.md the delta
- design.md optional, for cross-cutting or risky changes
- tasks.md numbered checkboxes that apply works through
Directoryarchive/
Directory2026-09-26-add-reset-rate-limit/ the first change, kept as history
- …
- config.yaml schema
A delta file uses four section headers. The archive step applies them to the matching file under openspec/specs/, and it is strict about each:
| Delta section | Use it for | What archive does | What trips it |
|---|---|---|---|
## ADDED Requirements | New behaviour, or existing behaviour you are writing down for the first time | Appends the requirement; creates the spec file if it does not exist | A requirement whose header already exists in the spec with different content (even a header differing only in case or spacing); use MODIFIED for that. ADDED is the only operation that can create a spec |
## MODIFIED Requirements | Changed behaviour | Replaces the whole requirement block with yours | Refuses if the spec file does not exist, if the header does not match, or if your block drops a scenario the current spec has |
## REMOVED Requirements | Behaviour you retire, with **Reason** and **Migration** lines | Deletes the requirement | On a spec that does not exist yet, ignores the removal with a warning |
## RENAMED Requirements | A new name only, in FROM: / TO: form | Renames the header | A MODIFIED block that uses the old name |
Inside each section, every requirement is ### Requirement: <name> with normative text (SHALL or MUST), followed by at least one #### Scenario: <name> with WHEN and THEN lines. Scenarios take exactly four hashes; the next sections show what happens when they do not.
The value of the model is the merge. Six months from now, openspec/specs/password-reset/spec.md answers “what does password reset do?” and each archived change folder answers “when and why did that change?”. A per-feature spec tool gives you only the second.
Install OpenSpec and wire it into your agent
Section titled “Install OpenSpec and wire it into your agent”OpenSpec needs Node.js 20.19.0 or later (engines in the 1.13.2 package). The npm package is @fission-ai/openspec and the command is openspec.
-
Install the CLI once per machine (terminal):
Terminal window npm install -g @fission-ai/openspec@latestopenspec --version # expect 1.13.2 or laterThe README also lists an official Homebrew formula:
brew install openspec. The CLI sends anonymous usage stats (command names and version, per the README) and turns them off in CI; to opt out on your machine, setOPENSPEC_TELEMETRY=0or runopenspec config set telemetry.enabled false. -
Initialize the repository for the agents your team uses. One
initcan serve all three:Terminal window cd your-repoopenspec init --tools claude,codex,cursorWrites six skills to
.claude/skills/openspec-*/and six commands to.claude/commands/opsx/:explore,propose,apply,archive,syncandupdate. In a session you type/opsx:propose. The commands carryallowed-tools: Bash(openspec:*), so the agent can call the CLI without a permission prompt; nothing else is pre-approved.Writes the same six skills to
.agents/skills/openspec-*/and no commands, because Codex invokes skills directly. In a session you type$openspec-propose,$openspec-apply-changeor$openspec-archive-change, or describe the task and let Codex pick the skill. OpenSpec’s init output says that in the Codex desktop app you select the skill from Skills in the sidebar.Writes the skills to
.cursor/skills/openspec-*/and commands to.cursor/commands/opsx-*.md. OpenSpec’s init output gives the Cursor spelling as/opsx-propose, with a hyphen instead of a colon. Cursor’s own command handling was not re-checked for this page, because cursor.com was unreachable on 2026-09-26. -
Tell the agent about the project once, in
openspec/config.yaml. Thecontext:field is read before every proposal, andrules:add per-artifact constraints:schema: spec-drivencontext: |Express 4 API in TypeScript, Postgres through Knex, Vitest + supertest.Password reset lives in src/routes/auth/password-reset.ts.rules:tasks:- Every task names the test that proves itproposal:- Always include a Non-goals section -
Commit
openspec/, the skill folders and the command folders, so every teammate and every agent reads the same workflow.
Run your first change on code that has no specs
Section titled “Run your first change on code that has no specs”The first change touches a capability, password reset, that has no file in openspec/specs/. That fact decides how you write it: archive refuses a MODIFIED delta against a spec that does not exist, and it ignores a REMOVED one with only a warning. So the first change describes password reset entirely as ADDED requirements: the rate limit you are adding, plus the existing behaviour you want written down and protected.
-
Pin current behaviour with tests before the agent touches anything. Anything you are about to write into the spec as “existing” needs a test that proves it is true today. Write characterization tests for the reset endpoint: the response for an unknown email, token expiry and single use. Commit them on their own.
-
Explore.
explorereads code and asks questions; its skill forbids writing code.Any behaviour marked UNTESTED goes back to step 1 before you propose, or stays out of the spec.
-
Propose. One command creates the change folder and all its artifacts, then stops. The propose skill states that the request “authorizes planning only”, so the agent will not start implementing in the same turn.
-
Review the delta, not the prose. Open
openspec/changes/add-reset-rate-limit/specs/password-reset/spec.md. An excerpt of what you should accept:## PurposeLets users regain access to their account by email without revealing which addresses have accounts.## ADDED Requirements### Requirement: Reset request rate limitThe system SHALL accept at most 5 reset requests per email address per rolling hour.#### Scenario: Sixth request within an hour- **WHEN** a client sends a 6th reset request for alice@example.com within 60 minutes- **THEN** the system responds 429 with a Retry-After header and sends no email### Requirement: Reset token lifetimeThe system SHALL reject a reset token 24 hours after it was issued.#### Scenario: Token used after 25 hours- **WHEN** a user submits a token issued 25 hours earlier- **THEN** the system rejects it and the password is unchangedAccept it when each scenario has a concrete input and a checkable output, when every “existing” requirement matches a passing characterization test, and when
tasks.mdnames a test for each task.design.mdis optional in thespec-drivenschema; for a rate limit that needs a shared counter across instances, ask for one. -
Validate the structure (terminal):
Terminal window openspec validate add-reset-rate-limit --strictopenspec show add-reset-rate-limit -
Apply.
/opsx:applyworks throughtasks.mdin order, ticks each task- [x]as it finishes it, and pauses when a task is unclear or blocked. The prompt in the next section adds the test gate. -
Archive.
/opsx:archivechecks artifacts and tasks, applies the delta, and moves the change toopenspec/changes/archive/2026-09-26-add-reset-rate-limit/. Because the capability had no spec, archive createsopenspec/specs/password-reset/spec.mdfrom theADDEDrequirements and the## Purpose. From the terminal the same step isopenspec archive add-reset-rate-limit --yes.
Change specified behaviour with MODIFIED and REMOVED deltas
Section titled “Change specified behaviour with MODIFIED and REMOVED deltas”A sprint later, security asks for a 30-minute token lifetime and the end of the legacy query-string link. Now openspec/specs/password-reset/spec.md exists, so the delta can modify and remove requirements in it.
The delta the reviewer sees:
## MODIFIED Requirements
### Requirement: Reset token lifetimeThe system SHALL reject a reset token 30 minutes after it was issued.
#### Scenario: Token used after 25 hours- **WHEN** a user submits a token issued 25 hours earlier- **THEN** the system rejects it and the password is unchanged
#### Scenario: Token used after 31 minutes- **WHEN** a user submits a token issued 31 minutes earlier- **THEN** the system rejects it and the password is unchanged
## REMOVED Requirements
### Requirement: Legacy reset link**Reason**: Tokens in query strings leak through referrer headers and proxy logs.**Migration**: Emails sent since the 2024 template change use /reset/<token>; users with an older email request a new reset.The “25 hours” scenario is still there even though the new one covers it. That is deliberate: MODIFIED replaces the whole block, and in 1.13.2 both openspec validate and archive refuse a MODIFIED block that drops a scenario the current spec still has. Remove a scenario on purpose by editing the living spec in its own reviewed change, not by leaving it out of a delta.
Before you apply, look at the change the way a reviewer will (terminal):
openspec show shorten-reset-token-ttl --type change --diff--diff prints per-requirement diffs of each delta against the current spec, which is the fastest way to confirm that the only lines changing are the ones security asked for.
Loop apply against the tests until the change is done
Section titled “Loop apply against the tests until the change is done”The apply skill ticks tasks; it does not prove anything. Make the test suite the gate between tasks and stop the agent from editing the spec to make its code pass.
/opsx:apply shorten-reset-token-ttlRun it in the session that proposed the change, so the agent keeps the context. If a task goes wrong, /rewind (or Esc Esc) returns the session to its last checkpoint. To enforce “never edit the spec”, start the implementation session with a deny rule for the living specs:
claude --disallowedTools "Edit(/openspec/specs/**)"An Edit rule covers every built-in file-editing tool, and the leading / anchors the path to the project root. Keep the rule on the session, not in .claude/settings.json: /opsx:archive must write to openspec/specs/, so run archive in a session without it. Permissions and sandboxing covers the rule syntax.
$openspec-apply-change shorten-reset-token-ttlPut the test command in AGENTS.md (for example npm test -- tests/auth) so the apply prompt below can refer to it, and run the session with the :workspace permission profile (-c default_permissions=":workspace", beta) rather than full access.
Codex has no per-session path deny for openspec/specs/ here; rely on a CODEOWNERS rule on openspec/specs/ and the CI job in the next section to enforce “never edit the spec”.
/opsx-apply shorten-reset-token-ttlCommit after each task whose tests pass, so a bad task is one git restore away and tasks.md keeps a clean history.
Cursor has no per-session path deny for openspec/specs/ here; rely on a CODEOWNERS rule on openspec/specs/ and the CI job in the next section to enforce “never edit the spec”.
Archive only when every task is ticked and the suite is green. The archive skill warns about unticked tasks but archives anyway if you confirm, and it offers “Archive without syncing”, which moves the change to the archive without touching openspec/specs/. For a behaviour change, choose sync every time.
How do you verify an OpenSpec change without reading every line?
Section titled “How do you verify an OpenSpec change without reading every line?”Split the checks into what is deterministic and what is judgment:
| Gate | What it proves | What it cannot prove | Signs off |
|---|---|---|---|
| Characterization tests before the first change | The “existing” requirements are true today | Anything about the new behaviour | Engineer |
| Delta review before apply | The right behaviour change is written down, with examples | That the code does it | Code owner of the capability |
openspec validate --strict | Delta structure, SHALL/MUST text, a scenario per requirement, no scenario lost from a MODIFIED block | That the requirements are right, or that the code meets them | CI |
| Tests written from each scenario | The code behaves as specified for those inputs | Behaviour nobody specified | CI |
Archive updates openspec/specs/ | The living spec now matches the change | That the code still matches the living spec later | PR reviewer |
Add the structural gates to CI. This job validates every change and spec, fails on a scenario written with three hashes, and fails a pull request that archives a change with deltas without touching openspec/specs/:
name: openspecon: pull_requestpermissions: contents: readjobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 persist-credentials: false - uses: actions/setup-node@v7 with: node-version: 22 - name: Validate changes and specs run: npx -y @fission-ai/openspec@1.13.2 validate --all --strict --no-interactive - name: No three-hash scenarios run: | if grep -rnE '^### Scenario:' openspec/; then echo "Scenarios need four hashes (#### Scenario:)"; exit 1 fi - name: Archived deltas reached the living spec env: BASE_REF: ${{ github.base_ref }} run: | archived=$(git diff --name-only --diff-filter=A "origin/$BASE_REF"...HEAD -- 'openspec/changes/archive/*/specs/*') if [ -n "$archived" ] && git diff --quiet "origin/$BASE_REF"...HEAD -- openspec/specs; then echo "Archived deltas but openspec/specs/ is unchanged:"; echo "$archived"; exit 1 fiThe grep step exists because validation does not catch every malformed scenario. In 1.13.2, a ### Scenario: under a requirement that also has a valid #### Scenario: passes validate --strict with only an INFO line saying the header “is ignored by validation”. Archive then rebuilds the spec, reads the stray header as a requirement with no scenario, and aborts, which is after the code has been written. The grep moves that failure to the pull request.
That description is the evidence bundle for the change. The reviewer reads the spec diff and the scenario-to-test table, as described in reviewing an agent’s pull request, and opens code only where a row says UNTESTED or a file sits outside the task list.
OpenSpec or Spec Kit when you already have code?
Section titled “OpenSpec or Spec Kit when you already have code?”Both write Markdown before code. They differ in the unit of work and in what survives the change. Spec Kit writes one folder per feature, with a constitution, spec, plan, research, data model, contracts and tasks, and gates each stage. OpenSpec writes one folder per change, with a proposal, a delta, an optional design and tasks, and merges the delta into a spec of the whole system. The Spec Kit tutorial runs the other side of this comparison.
| Your situation | Use | Why |
|---|---|---|
| Small or medium change to a system that already works, several times a week | OpenSpec | One review point per change, and archive keeps openspec/specs/ current |
| You need to answer “what does this capability do now?” from the repository | OpenSpec | Spec Kit leaves per-feature folders and no merged spec |
| New service or product, or a feature with several user stories a product owner must approve | Spec Kit | Constitution, clarify, analyze and converge give a gated trail per stage |
| Regulated work that needs a documented plan, research and contracts per feature | Spec Kit | OpenSpec’s design.md is optional and has no stage gates |
A bug that restores behaviour already in openspec/specs/ | Neither framework | A failing test that cites the requirement |
| A refactor with no behaviour change | Neither framework | Tests before and after; if you use OpenSpec, set skip_specs: true in the change’s .openspec.yaml |
A workable split is Spec Kit for a new service and OpenSpec for the stream of changes after it ships. Pick one per repository and say so in CLAUDE.md or AGENTS.md; two spec frameworks on the same code give the agent two sources of truth. The spec-driven frameworks comparison runs one feature through both.
What does OpenSpec cost in context and ceremony?
Section titled “What does OpenSpec cost in context and ceremony?”Context. OpenSpec installs project skills and commands, not a plugin, so claude plugin details does not apply. Measured on 2026-09-26 on the files openspec init --tools claude (1.13.2) wrote, the six skill descriptions and six command descriptions add about 2,200 characters to every session, roughly 550 tokens at four characters per token. Each skill loads its full instructions only when it runs: openspec-explore is 22.8 KB and openspec-propose 16.3 KB. The frameworks overview compares this with plugin bundles that cost tens of thousands of tokens.
Ceremony. One change produces three or four files, and the reviewer reads the delta, usually under a page. The real cost is discipline: someone has to review the delta before apply, and someone has to confirm archive updated the living spec. Skip either, and you have a folder of Markdown nobody trusts.
What breaks when you run OpenSpec on a brownfield repo?
Section titled “What breaks when you run OpenSpec on a brownfield repo?”Archive fails on the first change. Symptom: archive stops with “target spec does not exist; only ADDED requirements are allowed for new specs”. The agent wrote MODIFIED for behaviour that exists in code but not yet in openspec/specs/, and validate --strict did not stop it: it reports the change as valid and prints only an INFO line saying archive would refuse the delta. Recovery: rewrite the delta as ADDED requirements with a ## Purpose, backed by characterization tests.
A removal silently does nothing. Symptom: the archived change lists a REMOVED requirement, and the living spec never mentions it. On a capability with no spec yet, archive ignores REMOVED with a warning. Recovery: on a first change, leave retired behaviour out of the spec entirely and record the removal in proposal.md.
Archive refuses a MODIFIED delta. Symptom: “current spec contains scenario(s) not present in the modified block”. The agent wrote a fragment instead of the whole requirement. Recovery: copy the full block from openspec/specs/, every scenario included, then edit it.
A scenario is ignored, then archive fails. Symptom: validate --strict is green, the tests generated from the delta skip one scenario, and archive later aborts with “Requirement must have at least one scenario”. The scenario was written ### Scenario:, so validation ignored it and archive read it as a requirement. Recovery: change it to #### Scenario:, add its test, and keep the grep step in the CI job above so the next one fails at review.
The living spec drifts from the code. Symptom: someone archived with “Archive without syncing”, or changed behaviour without a proposal, and openspec/specs/ describes last quarter’s system. Recovery: the “archived deltas reached the living spec” CI step, a CODEOWNERS rule on openspec/specs/, and the drift checks in spec-driven development.
The agent edits the spec to match its code. Symptom: an implementation commit changes the delta or openspec/specs/ and the tests pass. Recovery: deny writes to openspec/specs/ in implementation sessions, require the capability owner’s approval on any change there, and protect the tests that encode the scenarios.
Old tutorials send you down the wrong path. Symptom: npm i -g openspec installs version 0.0.0, or /openspec:proposal does nothing. Recovery: install @fission-ai/openspec and use the /opsx:* commands shown for your tool.