Versioning
Safeword follows strict semver. This contract enables auto-upgrade to trust
patch AND minor bumps within the same major. Major bumps are the only class
that requires user action.
Semver Rules
Patch (0.27.0 -> 0.27.1) — Auto-upgradeable
- Bug fixes in hooks, reconcile, or CLI commands
- Typo/grammar fixes in owned docs and guides
- Performance improvements with no behavior change
- Bumping safeword's own dependencies (at patch level)
Minor (0.27.0 -> 0.28.0) — Auto-upgradeable (additive only)
Auto-applied silently at SessionStart, same as patch. The contract: minors are
strictly additive. They may add capability but must not remove or change
existing behavior. If a change can't fit this constraint, bump major instead.
- New hooks, skills, guides, or templates
- New CLI commands or flags
- New language pack support
- Additive schema changes (new owned/managed files)
- New quality gates or checks
- Additive config.json fields (new optional keys with defaults)
- Changes to hook output format (additive only — extra lines, new fields; no removal/rename)
Major (0.x -> 1.0, 1.x -> 2.0) — Notify, user decides
The only class that breaks auto-upgrade silence. User runs
bunx safeword@<version> setup manually after reviewing the changelog.
- Removed or renamed hooks, skills, or commands
- Changed reconcile behavior (owned -> managed, file moves)
- Breaking schema changes
- Changed config file format
- Removed language pack support
- Hook exit code or protocol changes
- Any change that would make an existing user's working setup behave differently
The Key Test
"If a project auto-upgrades to this version at SessionStart, will anything break?"
No, only fixes -> patch. No, but adds new capability -> minor (still auto). Possibly -> major (notify only).
Pre-1.0 Note
Safeword is pre-1.0 but follows strict semver anyway. The ecosystem convention
(Renovate, Dependabot) treats 0.x as inherently unstable — Renovate excludes
0.x from auto-merge by default. Our patch + minor auto-upgrade policy is a
deliberate commitment backed by this skill, not an ecosystem default.
Contributors are held to a higher standard than the ecosystem expects for 0.x
packages: minor releases must be strictly additive and pass the "auto-upgrade
at SessionStart — does anything break?" test in the negative.
Applying This
- Auto-upgrade logic: Auto-apply patch + minor bumps silently. Notify on major.
- Changelog: Label every entry as patch/minor/major
- PR review: Verify the version bump matches the change type. Bumping minor for anything other than strict addition is now a contract break — be especially careful here, because minors auto-propagate.
- When unsure: Bump major, not minor — false-major costs users a manual upgrade; false-minor silently breaks them.
Operating: cutting a release
The publish path is CI-driven via OIDC trusted publishing. Tag push → GitHub Actions Release workflow → npm with provenance. No local bun publish needed (or wanted) for normal releases.
Procedure:
Decide the bump using rules above. Patch / Minor / Major.
Bump version in all five release-tracked artifacts (pre-commit and release-contract tests enforce the Codex match):
packages/cli/package.json → version
.claude-plugin/marketplace.json → plugins[0].version
packages/cli/codex-plugin/.codex-plugin/plugin.json → version
packages/cli/codex-plugin/hooks.json → all five bunx commands pin safeword@<version>
packages/cli/codex-plugin/skills/** → generated, never hand-edited:
bun run --cwd packages/cli generate:codex-plugin
Codex's skills embed bunx --bun safeword@<version> because that plugin has
no project-local .safeword/hooks to call (V2AH4B). Skip the regeneration
and the release ships skills pinned to the previous version. Verify with
grep -r "safeword@<previous-version>" packages/cli/codex-plugin — it must
return nothing.
Then regenerate the lockfile so bun.lock's packages/cli workspace
version tracks package.json — otherwise it drifts and CI's lockfile-drift
gate fails the next PR that touches package.json (see #312):
bun install # rewrites bun.lock's workspace version; no resolution change
Validate Codex activation evidence for releases that change the Codex
plugin, its hooks, or its activation contract. The deterministic check is
required and covers both a still-running install-time host and a fresh host:
bun run --cwd packages/cli test tests/codex-plugin/headless-activation-check.test.ts
When authenticated Codex credentials are available, also run the opt-in
cache smoke with an explicit compatible model. The example below was checked
on 2026-08-03 against OpenAI's model guidance,
which recommends gpt-5.6-terra when balancing capability and cost. Model
availability is account- and CLI-dependent, so re-check that guidance and
select a currently supported model before each release:
SAFEWORD_RUN_CODEX_LIVE_SMOKE=1 SAFEWORD_CODEX_SMOKE_MODEL=gpt-5.6-terra bun run --cwd packages/cli test:smoke:live
The live command proves all five hooks from structured profile evidence.
Its headless codex exec process must leave activation pending; only a fresh
Desktop/app-server host and its activation receipt prove a Desktop restart.
PR + admin-merge. main is protected:
git checkout -b release/vX.Y.Z
git add packages/cli/package.json .claude-plugin/marketplace.json packages/cli/codex-plugin/.codex-plugin/plugin.json packages/cli/codex-plugin/hooks.json bun.lock
git commit -m "chore(release): vX.Y.Z"
git push -u origin release/vX.Y.Z
gh pr create --title "chore(release): vX.Y.Z" --body "..."
# after CI green:
gh pr merge --delete-branch --admin < num > --squash
Annotated tag on the merge commit. Body should roll up changes since the prior tag — see git show v0.35.1 for the style.
git checkout main && git pull --ff-only origin main
git tag -a vX.Y.Z HEAD -m "Release vX.Y.Z
<rollup of changes since prior tag>"
git push origin vX.Y.Z
Tag push triggers .github/workflows/release.yml.
Verify the publish. Watch the run, then confirm on npm:
gh run view conclusion -q '.conclusion' < id > --json # → success
npm view safeword version # → X.Y.Z
Optional: bunx safeword@latest install in this repo to round-trip the dogfood install.
Named failure modes (match symptoms, then fix):
- Workflow doesn't fire after tag push — the tagged commit lacks
.github/workflows/release.yml. Move the tag forward to a commit that has it (git tag -d vX.Y.Z && git tag -a vX.Y.Z origin/main ... && git push --delete origin vX.Y.Z && git push origin vX.Y.Z).
404 Not Found - PUT /safeword at npm publish step — trusted-publisher config on https://www.npmjs.com/package/safeword/access doesn't match the OIDC claims. Verify Organization/Repository/Workflow filename/Environment name fields exactly match release.yml.
422 Unprocessable Entity ... repository.url is "" — packages/cli/package.json lost its repository field. Restore it.
- Verify-npm-version step fails — Node 24's bundled npm dropped below 11.5.1 (rare; means Node was downgraded). Pin
node-version higher in release.yml.
404 OIDC token exchange ... package not found — npm trusted publisher entry was deleted or never saved. Re-create on npmjs.com.
Failure-mode triage: the workflow's release.yml has inline comments at each non-obvious step; read those before guessing. The publish-job step list (post-#146) is intentionally minimal — failures are localized.
1---2name: versioning3description: Safeword semver commitment and release discipline. Use when bumping versions, cutting releases, deciding what goes in a patch vs minor vs major, or reviewing changelog entries. Also use when auto-upgrade logic needs to know what's safe to apply silently.4---56# Versioning78Safeword follows strict semver. This contract enables auto-upgrade to trust9patch AND minor bumps within the same major. Major bumps are the only class10that requires user action.1112## Semver Rules1314### Patch (0.27.0 -> 0.27.1) — Auto-upgradeable1516- Bug fixes in hooks, reconcile, or CLI commands17- Typo/grammar fixes in owned docs and guides18- Performance improvements with no behavior change19- Bumping safeword's own dependencies (at patch level)2021### Minor (0.27.0 -> 0.28.0) — Auto-upgradeable (additive only)2223Auto-applied silently at SessionStart, same as patch. The contract: minors are24**strictly additive**. They may add capability but must not remove or change25existing behavior. If a change can't fit this constraint, bump major instead.2627- New hooks, skills, guides, or templates28- New CLI commands or flags29- New language pack support30- Additive schema changes (new owned/managed files)31- New quality gates or checks32- **Additive** config.json fields (new optional keys with defaults)33- Changes to hook output format (additive only — extra lines, new fields; no removal/rename)3435### Major (0.x -> 1.0, 1.x -> 2.0) — Notify, user decides3637The only class that breaks auto-upgrade silence. User runs38`bunx safeword@<version> setup` manually after reviewing the changelog.3940- Removed or renamed hooks, skills, or commands41- Changed reconcile behavior (owned -> managed, file moves)42- Breaking schema changes43- Changed config file format44- Removed language pack support45- Hook exit code or protocol changes46- Any change that would make an existing user's working setup behave differently4748## The Key Test4950> "If a project auto-upgrades to this version at SessionStart, will anything break?"51>52> **No, only fixes** -> patch. **No, but adds new capability** -> minor (still auto). **Possibly** -> major (notify only).5354## Pre-1.0 Note5556Safeword is pre-1.0 but follows strict semver anyway. The ecosystem convention57(Renovate, Dependabot) treats 0.x as inherently unstable — Renovate excludes580.x from auto-merge by default. Our patch + minor auto-upgrade policy is a59deliberate commitment backed by this skill, not an ecosystem default.60Contributors are held to a higher standard than the ecosystem expects for 0.x61packages: minor releases must be strictly additive and pass the "auto-upgrade62at SessionStart — does anything break?" test in the negative.6364## Applying This6566- **Auto-upgrade logic:** Auto-apply patch + minor bumps silently. Notify on major.67- **Changelog:** Label every entry as patch/minor/major68- **PR review:** Verify the version bump matches the change type. **Bumping minor for anything other than strict addition is now a contract break** — be especially careful here, because minors auto-propagate.69- **When unsure:** Bump major, not minor — false-major costs users a manual upgrade; false-minor silently breaks them.7071## Operating: cutting a release7273The publish path is CI-driven via OIDC trusted publishing. Tag push → GitHub Actions `Release` workflow → npm with provenance. No local `bun publish` needed (or wanted) for normal releases.7475**Procedure:**76771. **Decide the bump** using rules above. Patch / Minor / Major.78792. **Bump version in all five release-tracked artifacts** (pre-commit and release-contract tests enforce the Codex match):80 - `packages/cli/package.json` → `version`81 - `.claude-plugin/marketplace.json` → `plugins[0].version`82 - `packages/cli/codex-plugin/.codex-plugin/plugin.json` → `version`83 - `packages/cli/codex-plugin/hooks.json` → all five `bunx` commands pin `safeword@<version>`84 - `packages/cli/codex-plugin/skills/**` → **generated**, never hand-edited:8586 ```bash87 bun run --cwd packages/cli generate:codex-plugin88 ```8990 Codex's skills embed `bunx --bun safeword@<version>` because that plugin has91 no project-local `.safeword/hooks` to call (V2AH4B). Skip the regeneration92 and the release ships skills pinned to the previous version. Verify with93 `grep -r "safeword@<previous-version>" packages/cli/codex-plugin` — it must94 return nothing.9596 Then **regenerate the lockfile** so `bun.lock`'s `packages/cli` workspace97 version tracks `package.json` — otherwise it drifts and CI's lockfile-drift98 gate fails the next PR that touches `package.json` (see #312):99100 ```bash101 bun install # rewrites bun.lock's workspace version; no resolution change102 ```1031043. **Validate Codex activation evidence** for releases that change the Codex105 plugin, its hooks, or its activation contract. The deterministic check is106 required and covers both a still-running install-time host and a fresh host:107108 ```bash109 bun run --cwd packages/cli test tests/codex-plugin/headless-activation-check.test.ts110 ```111112 When authenticated Codex credentials are available, also run the opt-in113 cache smoke with an explicit compatible model. The example below was checked114 on 2026-08-03 against [OpenAI's model guidance](https://developers.openai.com/api/docs/guides/latest-model),115 which recommends `gpt-5.6-terra` when balancing capability and cost. Model116 availability is account- and CLI-dependent, so re-check that guidance and117 select a currently supported model before each release:118119 ```bash120 SAFEWORD_RUN_CODEX_LIVE_SMOKE=1 SAFEWORD_CODEX_SMOKE_MODEL=gpt-5.6-terra bun run --cwd packages/cli test:smoke:live121 ```122123 The live command proves all five hooks from structured profile evidence.124 Its headless `codex exec` process must leave activation pending; only a fresh125 Desktop/app-server host and its activation receipt prove a Desktop restart.1261274. **PR + admin-merge.** `main` is protected:128129 ```bash130 git checkout -b release/vX.Y.Z131 git add packages/cli/package.json .claude-plugin/marketplace.json packages/cli/codex-plugin/.codex-plugin/plugin.json packages/cli/codex-plugin/hooks.json bun.lock132 git commit -m "chore(release): vX.Y.Z"133 git push -u origin release/vX.Y.Z134 gh pr create --title "chore(release): vX.Y.Z" --body "..."135 # after CI green:136 gh pr merge --delete-branch --admin < num > --squash137 ```1381395. **Annotated tag on the merge commit.** Body should roll up changes since the prior tag — see `git show v0.35.1` for the style.140141 ```bash142 git checkout main && git pull --ff-only origin main143 git tag -a vX.Y.Z HEAD -m "Release vX.Y.Z144 145 <rollup of changes since prior tag>"146 git push origin vX.Y.Z147 ```148149 Tag push triggers `.github/workflows/release.yml`.1501516. **Verify the publish.** Watch the run, then confirm on npm:152153 ```bash154 gh run view conclusion -q '.conclusion' < id > --json # → success155 npm view safeword version # → X.Y.Z156 ```157158 Optional: `bunx safeword@latest install` in this repo to round-trip the dogfood install.159160**Named failure modes** (match symptoms, then fix):161162- **Workflow doesn't fire after tag push** — the tagged commit lacks `.github/workflows/release.yml`. Move the tag forward to a commit that has it (`git tag -d vX.Y.Z && git tag -a vX.Y.Z origin/main ... && git push --delete origin vX.Y.Z && git push origin vX.Y.Z`).163- **`404 Not Found - PUT /safeword` at npm publish step** — trusted-publisher config on https://www.npmjs.com/package/safeword/access doesn't match the OIDC claims. Verify Organization/Repository/Workflow filename/Environment name fields exactly match `release.yml`.164- **`422 Unprocessable Entity ... repository.url is ""`** — `packages/cli/package.json` lost its `repository` field. Restore it.165- **Verify-npm-version step fails** — Node 24's bundled npm dropped below 11.5.1 (rare; means Node was downgraded). Pin `node-version` higher in `release.yml`.166- **`404 OIDC token exchange ... package not found`** — npm trusted publisher entry was deleted or never saved. Re-create on npmjs.com.167168**Failure-mode triage:** the workflow's `release.yml` has inline comments at each non-obvious step; read those before guessing. The publish-job step list (post-#146) is intentionally minimal — failures are localized.