v2.1.34 — Reachability
bkit v2.1.34 — Reachability
v2.1.33 made bkit's defenses act when they fired. This release is about the ones
that never fired at all — and about the decisions that were declared in the code
and never taken.
Every finding was reproduced against a real Claude Code runtime (v2.1.226) with
claude -p --plugin-dir, never inferred from documentation. The reproduction
harness ships with the release, so any claim here can be re-run.
Highlights
A registered hook had never run once since v2.1.1. The FileChanged handler
was dead for three independent reasons, each confirmed against a live runtime:
if holds exactly one permission rule and rejects | alternation; if is
evaluated only on tool events, and FileChanged is not one; and FileChanged's
matcher names literal files, so the path glob the handler needed was not
expressible there at all. The capability moved to an event that actually fires.
Hook timeouts were 1000× too large, on every event. timeout is measured in
seconds; bkit wrote milliseconds. A declared 10000 on Stop meant 2 hours 46
minutes, not 10 seconds, so a hung hook had no effective cancellation. That is
the real cause behind issue #139, whose symptom alone was patched in v2.1.30.
Ten destructive rules said they would ask you, and never did. They were
detected, written to the audit log, and then permitted in silence, because the
hook branched only on critical. See What changes for you below — this is the
change you are most likely to notice.
A quality gate reported a number it had never measured. In an empty
directory with no design and no implementation, the headline gate returned
matchRate: 100, passed: true. A related hook reported 0% whenever it failed
to parse a rate — arguably worse, because a fabricated zero looks like
diligence. Both now report the absence of a measurement, and an unmeasured gate
blocks advancement rather than passing or failing.
A new L6 contract layer proves hooks actually dispatch. L1–L5 all call
bkit's own code, which is how eight shipped features could be dead while
thousands of assertions stayed green. L6 records what a real session observed
together with the hash of the hooks.json it observed it against, and CI
enforces that the evidence still describes what ships — with no CLI and no
credentials on the runner. Editing hooks.json without re-recording turns CI
red, by design. That mechanism fired during this release's own development.
What changes for you
Destructive commands that declared a confirmation now raise one
Ten rules have carried defaultAction: 'ask' since the rule table was written.
The hook only ever acted on critical, so the rest ran without a word.
| Command | Before | After |
|---|---|---|
rm -rf ./tmp/build |
refused — even when scoped | asks |
rm -rf /, rm -rf ~, rm -rf $HOME |
refused | refused (unchanged) |
git reset --hard HEAD~1 |
ran silently | asks |
git merge main, git push origin main |
ran silently | asks |
access to *.pem / *.key files |
ran silently | asks |
curl … | bash |
ran silently | refused |
eval "$(echo … | base64 -d)" |
ran silently | refused |
find / -delete |
ran silently | refused |
dd of=/dev/disk0 |
ran silently | refused |
npm test, git status, git push origin <branch> |
ran | ran (unchanged) |
Ordinary work is deliberately untouched. A confirmation tier that interrupts
npm test gets switched off within a day and takes the refusal tier with it, so
a regression suite runs the shipped hook against both lists on every build.
There is no environment variable to mute the tier: if a rule asks too often, the
rule is wrong and should be narrowed.
Two guards that refused correct commands were fixed
A quoted heredoc body is data, not a command line — so writing documentation
about the guard no longer trips it (issue #145). A scoped delete is no longer
graded by text belonging to a completely different command later in the same
block. And a heredoc pattern no longer scans past its own terminator into
unrelated commands.
Prompts in your language reach the right specialist
| Prompt | Before | After |
|---|---|---|
보안 취약점 점검해줘 |
code-analyzer |
security-architect |
necesito una revisión de seguridad |
code-analyzer |
security-architect |
bitte Sicherheit prüfen |
gap-detector |
security-architect |
Two causes, both closed: the router returned the first-declared match rather
than the strongest, and code-analyzer claimed the bare word "security" in
eight languages although its trigger is the compound "security scan".
Sessions start lighter
The 8-language trigger vocabulary moved out of agent and skill descriptions —
which Claude Code loads into context for the whole session — and into code,
where it costs nothing. Frontmatter is now English-only and free of CJK
entirely: 61,967 → 54,188 bytes of always-resident text (~15.5K → ~13.5K
tokens), 1,371 → 0 CJK characters.
A hung hook stops hanging
Hook budgets are now 3–10 seconds, and every one of the 28 handlers was measured
against its own budget rather than assigned a number: worst case 5.7%–38.4% of
budget across five runs each.
A shorter budget does not make hooks fire more often. It decides how long a
hook that has stopped responding is waited for. A hook killed by its timeout
fails open under bypassPermissions — measured, not assumed — so a hung hook was
never going to protect anyone; before this release it also stalled the session
for up to 166 minutes while failing to.
A broken hook stops looking like a working one
The hook layer holds 333 catch blocks and 188 swallow without a trace. Crashes
are now recorded centrally and surfaced once at the next session start, in a
line that clears itself after 24 hours when the failures stop. Control flow is
untouched, so an uncaught exception is still fatal.
Features that were registered but unreachable now run
Editis covered whereverWriteis — PDCA tracking, template validation and
SKILL.md linting previously skipped the common case of editing an existing file- Four module integrations in bkit's busiest hook — checkpoint creation before a
phase transition, quality-gate recording, the state-machine transition and the
workflow-engine advance — had been unreachable since the v3 state migration - The guard that protects a live
do/check/actcycle from manual compaction
had never once engaged - Sprint archives now write their entry to
MEMORY.md /pdca qahad been permanently blocked by six CRITICAL findings, five of them
the scanner reading its own comments as code
For maintainers
- 21 hook events / 24 blocks across 28 handlers (was 22/25). The reduction is
an audit result, not a scope cut:FileChangedwas retired through an explicit
deprecation-registry.jsonentry, and hook events may now be removed only that
way — a silent removal still fails the contract test. .bkit/runtime/hook-dispatch.ndjsonis a new per-project diagnostic file
(append-only, self-compacting, ~0.69 ms per hook).
BKIT_HOOK_DISPATCH_RECORD=0disables it.- Regenerate the L6 evidence with
node test/qa-harness-full-live.js --layer hooks --record. - Verification: 369 test files, 6,900 assertions, 0 failures. Full-surface
live QA on CC v2.1.226 across 140 real sessions: skills 44/45, agents 34/34,
hook events 23/23, MCP tools 38/38. The one non-pass isqa-phase, measured at
136 s and exit 0 in isolation — slow under 121 sequential sessions, not broken. - Every new guard is proven against a negative control: shown to fail when
the defect is reintroduced, not merely to pass today. - Three defects introduced by this release's own branch are listed in
CHANGELOG.mdrather than quietly fixed, including a raw NUL byte that shipped
inside alib/source file and passed every test. A release about invisible
failure that hides its own would be making the same mistake. - One claim in an earlier draft of these notes was unearned and was withdrawn
before release: two Korean prompts were said to route correctly "for the first
time", and measurement againstmainshowed they already did.
Credits
Issue #145 was reported by @BrightGold70
(Hawk Kim), with an analysis precise enough that the reproduction became a
regression test unchanged.
Full changelog: https://github.com/popup-studio-ai/bkit-claude-code/blob/main/CHANGELOG.md