Releases: biznuts/never-stale
Release list
v0.11.0 — gate hardening, honest CI, i18n catch-up
Adversarial full-project review → three PRs: gate hardening (#10), docs/CI truthfulness (#11), i18n catch-up (#12). Every gate defect was empirically reproduced before fixing and re-verified after. Suite: 92 tests, now run on Linux + Windows × Node 20/24.
Fixed (the gate)
- Symlink/junction escape in
syncPairspaths — a relative path could resolve through a symlink or junction to a file outside the repo (junctions are privilege-free on Windows). Every pair path is now realpath-confined to the repo root's real path; escaping pairs are skipped and the reminder stays byte-identical to the unconfigured one. - A corrupt local marker no longer silently disables a team opt-in — a
.local.jsonthat fails to parse falls back to the committed team marker beside it; a parsing local marker still overrides (including anenabled: falseveto). - UTF-8 BOM no longer disables a marker (common with Windows editors) — stripped before
JSON.parse. - Windows path-case mismatches no longer suppress the gate (
x:\vsX:\) — containment and pair-matching compares case-fold on win32. - TOCTOU in the capped read —
hash-mode source reads are fd-based (open → fstat → read the stat'd size, ≤ 512 KB), refusing non-regular files. - Mode-aware edit-retarget wording — "(and its synced-to marker)" only for
hashpairs; reserved modes never retarget. Multi-pair drift lines each carry the[never-stale]prefix.
Added
NotebookEditin the PostToolUse matcher — notebook edits get the same reminder and retargeting as Edit/Write/MultiEdit.- 13 regression tests for all of the above, and
test/release.test.mjspinning every version stamp toplugin.jsonso a bump cannot ship half-done.
Changed
- CI is honest and broader: ubuntu + windows × Node 20/24 (the win32 junction/case-fold paths are now actually exercised); the marketplace version step no longer passes a vacuous comparison silently.
- Docs truthfulness sweep:
declared/versionmodes consistently described as deferred — not planned; CONTRIBUTING documents all four test files; stale bare/never-stalereferences fixed. - Translations caught up: all four READMEs (
zh-Hant,zh-Hans,ja,ko) gain the fullsyncPairssection (absent since v0.9.0) plus this release's changes.
Full changelog: v0.10.1...v0.11.0
v0.10.1 — linear normalize (ReDoS fix)
Fixes a ReDoS surface in the documented body-hash one-liner (/never-stale:setup and /never-stale:status). The old /\s+$/ (plus /^\n+|\n+$/g) backtracks catastrophically on a long run that never reaches the anchor — a single ~200 KB line of spaces measured ~21.6 s. Both one-liners now use the same linear char-scan normalization the gate and the hash-mode preview already use.
- Hash-compatible: linear and regex forms produce byte-identical hashes for all realistic bodies (verified against the dogfood fence hash) → no recorded hash changes, no migration.
- A guard test runs the real shipped one-liner and asserts it stays linear and never reintroduces
/\s+$/. - Roadmap honesty:
version/declaredmodes documented as deferred and not planned (reserved no-ops).
This line also adopts hash-mode dogfooding in the repo (pairs the gate to its design doc). No plugin behaviour change vs 0.10.0 when syncPairs is unconfigured.
Full notes: CHANGELOG.md.
v0.10.0 — content-based drift detection (hash mode)
Phase 2's first increment: mode: "hash". A syncPairs entry can now do a true content comparison instead of the mtime heuristic.
- The snapshot embeds a synced-to marker naming the source content it last reconciled to:
<!-- never-stale:synced-to 8c2b42f56e6fd699 --> - On compact, the gate hashes the source's normalized content (CRLF, trailing whitespace, and leading/trailing blank lines normalized away → cosmetic churn is not drift) and flags a mismatch, reporting the new hash so you can paste it back.
- A snapshot with no marker, or a source missing / larger than the 512 KB cap, is treated as unknown (never drift) and surfaced in
/never-stale:status.
The synced-to marker is matched with a static, gate-owned pattern — no user-supplied regex, no ReDoS surface — and the source read is size-capped and happens only on the low-frequency compact path. /never-stale:status previews hash-mode pairs. With no syncPairs configured, the reminder stays byte-identical.
Full notes: CHANGELOG.md.
v0.9.0 — optional drift detection (syncPairs, mtime mode)
Phase 1 of drift detection. A marker may pair a source doc (a ledger/changelog that grows) with a snapshot doc meant to stay reconciled to it, via syncPairs. When the snapshot falls behind, the gate turns silent drift into a signal:
- on compact — appends an advisory drift note to the reminder;
- on edit — if you edited a configured source, the nudge retargets to "update the paired snapshot."
Ships mode: "mtime" only — a pure fs.stat comparison: no file read, no regex → no ReDoS surface, no per-edit I/O. hash / declared / version reserved.
A dedicated safety suite validates each pair's shape, rejects unsafe paths (absolute / drive-qualified / UNC / parent-escaping), and statically flags ReDoS-prone regexes. With no syncPairs configured, the reminder is byte-identical to 0.8.0.
Full notes: CHANGELOG.md.
v0.8.0 — update sweep + language codes
Tools for keeping opted-in projects tidy after a plugin upgrade — without ever changing the gate's behavior.
New: /never-stale:update
After you upgrade the plugin, projects you opted in earlier keep markers (and CLAUDE.md fences) stamped with the version that wrote them. The gate ignores that stamp, so the drift is purely cosmetic — but /never-stale:update sweeps your projects and reconciles the bookkeeping in one pass:
- bumps each marker's recorded
versionto the installed version, - normalizes the recorded languages to canonical display strings and adds language codes,
- refreshes the
CLAUDE.mdfencev=tag (body andhash=untouched, so the "edited since scaffold" signal is preserved), - migrates away legacy v0.5.0 residue.
It never re-asks your languages and never changes what the gate fires. Shows a per-project plan and confirms; --dry-run previews. Pass a parent path to sweep many repos at once, e.g. /never-stale:update ~/projects.
Canonical language codes
The marker gains optional spokenCode / writtenCode (en, zh-Hant, zh-HK, zh-Hans; omitted for an "Other" language) alongside the human display strings — so a recorded language stays comparable even when the display wording drifts (e.g. "Traditional Chinese (HK)" vs "Traditional Chinese (Hong Kong)"). setup writes them; update backfills older markers.
Version-drift surfacing
/never-stale:status and /never-stale:list now compare each marker's recorded version against the installed plugin version and flag a stale marker — making clear it's cosmetic and pointing at /never-stale:update.
Migration
None required. The gate reads only enabled + the per-event flags and ignores the version and language strings, so the marker's extra fields are inert and existing setups keep working. marker.schema.json gains the two optional code properties; markers without them stay valid.
Update: /plugin marketplace update biznuts then /plugin install never-stale@biznuts (restart or /reload-plugins), then optionally /never-stale:update to reconcile your projects.
Full changelog: https://github.com/biznuts/never-stale/blob/main/CHANGELOG.md
v0.7.0 — verb subcommands
never-stale is now driven by verb subcommands instead of one flag-laden command. Plugin commands are namespaced, so the old /never-stale:never-stale invocation and the --off/--list/--dry-run flags read as plain actions now.
New commands
| Command | What it does |
|---|---|
/never-stale:setup |
Opt this project in (scaffold CLAUDE.md + write the marker). --dry-run previews. |
/never-stale:off · /never-stale:on |
Pause · resume — flip the marker's enabled, keeping the marker, languages, and CLAUDE.md block. |
/never-stale:status |
Read-only health check: what governs this project and whether the gate would fire. |
/never-stale:list |
List every opted-in / legacy project on disk. |
/never-stale:remove |
Full teardown — delete the marker and strip the CLAUDE.md block. --dry-run previews. |
Highlights
- Reversible pause/resume.
/never-stale:offsets"enabled": false(gate goes silent) while keeping everything;/never-stale:onflips it back. On a committed team marker,offoffers a local"enabled": falseoverride so a single checkout can pause without touching the repo. - New
/never-stale:status— governing marker (team vs local), enabled state, recorded languages,CLAUDE.mdfence integrity (intact / edited / missing / malformed), and a one-line fires-vs-silent verdict. - The bare
/never-stalecommand is now a deprecated help screen that lists the verbs and maps the old flags to them.
Migration
None required. The gate is untouched ("enabled": false was already honored) and the marker / CLAUDE.md formats are unchanged, so existing setups keep working. Old flags map to: --off → /never-stale:remove, --list → /never-stale:list, --dry-run → /never-stale:setup --dry-run.
Update: /plugin marketplace update biznuts then /plugin install never-stale@biznuts (restart or /reload-plugins).
Full changelog: https://github.com/biznuts/never-stale/blob/main/CHANGELOG.md
v0.6.0
Changed
- Hooks now ship inside the plugin (
hooks/hooks.json+
hooks/never-stale-gate.js, resolved via${CLAUDE_PLUGIN_ROOT}) and register
machine-wide./never-staleno longer writes any hook or script into your project —
only a marker and aCLAUDE.mdblock. CLAUDE.mdrules are now wrapped in<!-- never-stale:begin … -->/
<!-- never-stale:end -->sentinels, so teardown is reliable even after you edit
the text inside.
Added
- Per-project opt-in marker —
.claude/never-stale.json(committed/team) or
.claude/never-stale.local.json(gitignored/local, which overrides a committed
marker and can veto it with"enabled": false"). - Git-bounded upward walk: the gate resolves the governing project by walking up
from the launch directory to the nearest ancestor marker, bounded by the git repo
root — so launching from a subdirectory still works and a marker outside the repo
never governs it. /never-stale --listto enumerate every opted-in (and legacy) project on disk.- Automatic migration of 0.5.0 installs when you re-run
/never-stale. NEVER_STALE_DEBUG=1diagnostic log in the OS temp directory.
Removed
- The project-local
never-stale-reminder.jsscript and the per-project
settings.jsonhooks (replaced by the plugin-owned, marker-gated hook). Existing
0.5.0 projects keep working until migrated.
v0.5.0
Added
/never-stale --off— per-project teardown that is symmetric to setup: it shows
a plan, confirms, and removes only never-stale's own artifacts (hook script, its
settings.jsonhooks, andCLAUDE.mdsections still matching the template).
--off --dry-runpreviews the removal only. Foreign hooks, user-edited sections,
andsettings.local.jsonare never touched.