v4.12.0
Warning
ADVISORY (added 2026-08-20) — read before upgrading.
If audit/managed-content-manifest.json is missing from your install, do not run
hooks/local/upgrade.sh. It half-applies: new files arrive, the engine that calls them does not,
VERSION advances anyway, and the run writes upstream's manifest in as your base — after which
your stale files are reported as your own edits and the tree freezes.
If you have already run it from that state: do not re-run it, do not delete or
re-stamp the manifest, and do not bootstrap blindly — each recreates the same wrong base.
Preserve your *.pre-upgrade-* backups and wait for the recovery path.
Full guidance: https://github.com/fusebase-dev/fusebase-flow/blob/main/docs/ADVISORY-2026-08-20-missing-base-upgrade.md
v4.12.0 — the health check can see the enforcement layer
Full entry: CHANGELOG.md § [4.12.0]. This file records release evidence and the upgrade note.
Reported by the paperclip+hermes-v1 consumer, fifth in their escalation chain. Every claim in their
report survived an independent adversarial review before they filed it — including two of their own
companion claims that the review refuted.
Why this is a minor, not a patch
/fusebase-health gains a verdict it could not previously reach, and the manifest stampers can now
refuse to write. Both are new user-visible outcomes.
The defect
The settings arm keyed on exactly two facts: the count of expected event keys, and whether
hooks/handlers/stop.py appeared in the Stop chain. PreToolUse was never inspected. A tree where
the CLI regenerated .claude/settings.json and stripped everything was therefore indistinguishable
from a tree that never opted in, and both printed the same ✓.
The consumer observed both rows: the same line minutes after finding enforcement stripped, and again
after restoring it. Four such removals appear in their history.
Partial stripping was already caught — if stop.py survived but events were incomplete, the arm
recorded drift. The blind spot was specifically everything gone at once, which is what a wholesale
regeneration produces.
Why it matters beyond one consumer: /fusebase-health is the command an operator runs to answer
"did the update break anything?", and it answered yes-only-for-content — skills, agents, overlays —
while the runtime enforcement layer (FR-06/07/12) could be absent.
The fix
A dedicated, schema-validated intent marker — state/audit/flow-hook-wiring-intent.json — is written
only after a successful --wire-hooks, on a surface the CLI never rewrites. The arm then reads:
| Marker | Flow handler in the PreToolUse chain |
Verdict |
|---|---|---|
| valid, enabled | present | ✓ wired |
| valid, enabled | absent | DRIFT, exit 1 |
absent, or enabled: false |
either | today's opt-in-default line, unchanged |
Presence is not proof. The check matches the canonical handler (hooks/handlers/pre_tool_use.py)
inside the chain, not merely the "PreToolUse": key — a chain with the key and someone else's handler
is not Flow enforcement.
The drift carries its recovery, including --wire-hooks, because the default recovery explicitly
does not modify .claude/settings.json. Without that, detection would only relocate the operator's
forensics. Health remains read-only and never repairs.
Deliberate opt-out is first-class: --forget-hook-wiring records intent withdrawal, and the arm
goes quiet. A false alarm would be worse than the old silence, because it teaches operators to ignore
the alarm. Six states where the marker could lie — opt-out, settings replacement, an archived tree
carrying another checkout's marker, a malformed marker, missing settings, and a non-Flow handler — are
each handled and each tested.
Also — the stampers refuse to attest bytes that never ship
All three manifest stampers now emit a diagnostic, return non-zero, and leave the manifest
untouched when a covered file holds CRLF under a resolved eol=lf. A warning that still writes a
knowingly non-canonical attestation is observability, not a guard.
This closes the proven CRLF subclass only. It does not settle
stamper-hashes-worktree-not-artifact — hashing committed bytes would fix artifact fidelity but
trade away local-tamper detection, and that trade stays open and undecided.
The consumer's forensics and this repo's own history converged on the same mechanism: five
occurrences here in three days, most recently a baseline file that hashed CRLF, shipped LF, and
reddened CI twice while looking clean locally, because the stamper and the verifier read the same
wrong bytes and agreed.
A guard that cannot load never fails a stamp. It degrades open and says so. The net is optional;
the manifest is not.
Also fixed
docs/release-notes/v4.7.0.md warned "use the 4.7.0 bootstrap-upgrade.sh, not the copy already
installed" and then printed the installed path. The v4.6.1 copy has no base-synthesis step at all, so
which copy runs decides whether a base exists. The README and install docs print the same command and
were deliberately left alone — v4.11.0 made that command correct for them.
Release evidence
| Item | Value |
|---|---|
| Tag verification | the v4.12.0 run is the authoritative gate; a local run is never release evidence |
| Pre-tag verification | 6c4e1e5 green on both platforms, 1134/1134 on Windows |
| New phases | hook-wiring-intent (23 rows), stamp-eol-guard (18 rows) — first full-tier run |
Known residuals
- Trees wired before this release carry no marker and stay silent until their next
--wire-hooks. Marker-absence means "not known to have opted in", never "never opted in". - Manual
cp .claude/settings.json.examplewiring is a permanent false negative, unavoidable without a separate acknowledgement path. - Drift attribution in
verify(the consumer's §3.2) is filed, not built: git-history attribution is unreliable on shallow and non-Git trees, where "no post-stamp commit" becomes false. It belongs behind an explicit flag. upgrade.shremains at its FR-25 baseline; the next change touching it must decompose it first.- The command gate still denies honest prose quoting a destructive pattern (K21/M8).