Skip to content

v4.14.1

Choose a tag to compare

@github-actions github-actions released this 25 Aug 23:43
· 74 commits to main since this release

v4.14.1 — --wire-hooks did not wire enforcement on an occupied event

Full entry: CHANGELOG.md § [4.14.1]. This file records release evidence and the upgrade note.

CORRECTION TO v4.14.0. That release told exposed consumers to run
bash hooks/local/post-fusebase-update.sh --wire-hooks. If your .claude/settings.json already
had a PreToolUse array of your own, that command did not wire Flow's enforcement handler — it
exited 0, reported "applied 7 changes", and recorded an intent marker saying you had opted in.

Re-run it on this release, then confirm:

bash hooks/local/post-fusebase-update.sh --wire-hooks
grep -c 'hooks/handlers/pre_tool_use.py' .claude/settings.json    # must be >= 1

Published tags are immutable, so v4.14.0's note cannot be rewritten. This correction lives here.

The defect

settings-json-merge.py's add branch was a wholesale replace, correctly guarded to fire only when
there was nothing to preserve:

if event not in hooks or not hooks[event]:
    hooks[event] = [make_event_block(event)]

When the array was non-empty it fell through to two functions that both skip any block not already
naming a Flow handler — so a Flow block that was absent was never added. Stop was the only
event with an add-beside path.
Measured: all five non-Stop events had the gap.

Event Flow handler after --wire-hooks on a consumer-only array (pre-fix)
SessionStart · UserPromptSubmit · PreToolUse · PostToolUse · PreCompact 0 — not wired
Stop 1 — the only event with an add-beside path

Why this was worse than a missing feature

post-fusebase-update.sh recorded the wiring-intent marker on that exit 0. So the tree recorded
intent while enforcement was absent — and the health arm added in v4.12.0 then correctly reported
Flow PreToolUse ENFORCEMENT STRIPPED and prescribed --wire-hooks, the command that had just
produced the state
. A loop that could not converge.

The code already guarded against a near version of this: a comment at the recording site states
intent is written only on a merge that "actually succeeded… so a failed or aborted merge never leaves
an intent the health check would then report as stripped enforcement." That protection is real and it
did not cover this case, because the merge did succeed. It applied seven other changes. Success
was measured as exit 0, not as achieved the thing.

The fix

  • Add beside, never replace. When an event array is non-empty but contains no Flow block, Flow's block is appended alongside the consumer's. No drop, no reorder, no overwrite — the guarantee _widen_matchers already documented survives.
  • Intent keys on the achieved state. The marker is written only when the canonical handler is verifiably present in the merged chain, not when the merge exited 0. The predicate is ffhc_hwi_wired (0 wired / 1 absent / 2 no settings file).

Flow's block is appended as a separate block, not merged into the consumer's chain. Joining their
chain would inherit their matcher — measured as Bash|Edit|Write on a real consumer tree — which
would re-open the v4.14.0 PowerShell bypass on every such install. Multi-block-per-event is documented
host behaviour and is already the shape the "correctly wired" control case treats as valid, though
Flow's own example ships one block per event. A certain security regression avoided was preferred over
an inferred host behaviour relied upon.

Evidence

hooks/tests/test-wire-hooks-add-beside.sh — 35 rows, tag wire-hooks-beside.

Pre-fix Post-fix
Phase 20/35 PASS, 15 FAIL 35/35 PASS
Controls: key-absent, [], consumer+flow PASS ×5 events PASS
Consumer-only array FAIL ×5 PASS ×5
Convergence — health arm after a successful --wire-hooks FAILENFORCEMENT STRIPPED PASS
Unachieved wiring records no marker FAIL — marker written with handler absent PASS

The three control rows passing before the fix is the point: only the consumer-only row
discriminates. Regression: 227/227 across every phase touching these files.

Also in this release

The E2–E5 consumer-proposal close-out: three documentation additions and five backlog filings.

  • policies/approval-policy.yml now states that a VALID artifact authorizes its action repeatedly until expiry — it is not single-use. A reader could previously infer single-use from the TTL prose. The one real narrowing is recorded too: a digest-bound protected_path_edit is single-changeset, not single-use.
  • hooks/README.md gains a consumer-authored-hooks section: hooks in a group run in parallel, deny takes precedence, and a hook that cannot start or exceeds its timeout is non-blocking — the host fails open. A consumer security hook must bound its runtime well below its timeout and convert every local failure to a deny. Provenance is labelled: read from the Claude Code hooks reference by the consumer who filed it, not measured by Flow.
  • workflows/violation-recovery.md documents the FR-07 first-edit deadlock: the tool-time arm is defence-in-depth for already-staged content, the commit gate is the boundary, and the sanctioned path is stage → mint → commit → --consume.

Known residual — reported, not fixed

post-fusebase-update.sh Step 5b reports "(re)installed Flow git fallback hooks" from an exit code
plus the absence of a string, without asserting the hook file exists, is executable, or is Flow's —
the same exit-code-not-outcome shape. Lower blast radius: a transient report line, not a durable
marker a later check reads back as an accusation, and no non-convergent loop. Filed separately.