Releases: ww-w-ai/bkit-claude-code
Release list
v2.1.38 — QA measurement
The QA phase was reporting success while measuring nothing
Every run printed "QA Phase completed". Behind that line, the numbers it exists to produce were never reaching the gate that decides whether your work is good enough to advance.
None of this could show up as a failing test — a component that silently does nothing produces a green run, not a red one. It was found by reading the path from qa-lead through the Stop handlers to gate-manager end to end, and every claim below was reproduced before it was fixed.
What was actually broken
No Stop handler had ever seen its hook payload
unified-stop reads the payload with a reader that destroys stdin on resolve — deliberate, from the fix for the 15-minute hook stall in issue #139 — and then dispatches the per-agent handler with require(), in the same process. Each handler reads stdin again for itself, by which point the bytes are gone.
$ echo '{"hook_event_name":"Stop","transcript_path":"/tmp/t.jsonl"}' | node parent.js
PARENT saw: {"hook_event_name":"Stop","transcript_path":"/tmp/t.jsonl"}
CHILD saw: {}
Six metric-collecting handlers had been reading {} since the day that reader was introduced. The payload is now handed on to the later reader, and handlers resolve it to the assistant's reported text via transcript_path.
Handlers were pattern-matching the wrong text entirely
| handler | what it was reading |
|---|---|
qa-phase-stop |
called .match() on a parsed object — TypeError on its first pattern, swallowed by its own catch. M11–M15 were never written on any run since v2.1.1. |
analysis-stop, qa-stop |
their own hardcoded guidance string ("Next steps: 1. Save report to…"). M2 was always the 75 baseline; M5 was always 0 — "no errors found", every session, measured or not. |
gap-detector-stop, iterator-stop, pdca-skill-stop |
the hook envelope — hook_event_name, session_id, transcript_path, cwd — which contains none of the signals they look for. |
All six now read the agent's report.
The QA gate could never pass
gate-manager's qa gate has required qaCriticalCount === 0 since v2.1.1. No metric ID ever produced that name, and an absent metric does not satisfy a condition — so the pass count fell short on every run, and QA was structurally unable to advance to Report regardless of how your tests went.
Fixed by supplying the missing metric (M16, QA Critical Count). The gate itself is untouched: the threshold was never the problem.
Browser tests were skipped 100% of the time
Chrome MCP detection read process.env.MCP_SERVERS — a variable Claude Code does not set. It was false on every run, so L3, L4 and L5 never executed, while qa-lead's own documentation described that skip as normal fallback. A permanently dark half of the test matrix read as a feature.
Detection is now layered, most authoritative first: a runtime probe qa-lead records after actually calling a Chrome MCP tool, a BKIT_CHROME_MCP=1|0 operator override, the old environment variable, and finally your MCP config files.
A QA failure never went back to QA
act → qa (QA_RETRY) was defined, and emitted by nothing. A rejected feature dropped into the ordinary act → check loop and never returned to the phase that rejected it. Its retry counter also read and rewrote the same value, so the loop-breaker could not fire however many times a feature went round.
An unmeasured rate was recorded as 0%
isMeasured(0) is true, so a parse failure that fell back to 0 passed every "did we measure this?" check downstream — including the efficiency calculation, where improvement = 0 - previous wrote down a regression that never happened.
What changes for you
| Before | Now |
|---|---|
| "QA Phase completed" with no numbers behind it | M1–M16 carry real readings from what the agents reported |
qa gate could not return pass |
The gate passes or fails on your actual results |
| L3–L5 browser tests silently skipped, always | They run when Chrome MCP is connected, and say so when it isn't |
A QA failure quietly rejoined the act → check loop |
It returns to QA, bounded by guardrails.loopBreaker.maxQaRetries (default 3) |
A rate nobody measured displayed as 0% |
It reads "not measured" — and never counts as reaching a threshold |
qa-test-planner could not write a test plan |
It writes docs/05-qa/{feature}.test-plan.md, and the generator stops if it's absent |
qa-lead claimed four coordinated agents, dispatched three |
qa-monitor runs, so test outcomes have runtime log evidence behind them |
| The pre-release scanner scanned bkit, not your project | It scans $CLAUDE_PROJECT_DIR; --root DIR to point elsewhere, --self for the old behaviour |
Nothing here requires action from you. Update the plugin and the QA phase starts reporting what it measured.
One thing to know if you're on Claude Code v2.1.233
v2.1.233 withdrew the Todo/Task tool family — TaskCreate, TaskGet, TaskUpdate, TaskList, TodoWrite — from Opus 4.8 / Sonnet 5 / Fable 5 / Mythos 5+, keeping it for Haiku. CLAUDE_CODE_ENABLE_TODO_TOOLS=1 restores it.
This surfaced here because bkit's live hook harness reported TaskCreated / TaskCompleted as dead hooks. They are not dead — there was simply no tool for the model to call. Isolated against the hook dispatch ledger rather than against what a model says it has: all four of the harness's isolation flags were bisected one at a time and none of them moved the result, while the same trigger under the full flag set fires both hooks with that variable set. The harness now sets it, and live hook coverage went 21/23 → 23/23 with the isolation intact.
If you rely on bkit's TaskCreated hook or on skills that instruct the model to call TaskCreate, they are inert on v2.1.233 unless you set that variable. Recommended Claude Code runtime remains v2.1.220.
Verification
- 5,355 test cases · 0 failures (5,350 pass, 5 skip) — 78 new cases across three regression suites
- Full live QA 145/145 against a real Claude Code v2.1.233: 44 skills, 34 agents, 21 hook events, 19 MCP tools, and fork mode — zero failures
- Live hooks 23/23, with the L6 evidence artifact re-recorded against the shipped
hooks.json - The F-0 payload defect was reproduced on
mainand confirmed fixed on the branch with an independent two-file harness
Also in this release
The philosophy docs described ten metrics where the code has had M11–M15 since v2.1.1 — and a different ten, whose M1 was "Plan accuracy" where the code's M1 is Match Rate. Anyone matching a metric ID against a runtime value was reading two unrelated lists. That table now comes from METRIC_SPECS.
Full detail: CHANGELOG.md · PRs: #151, #152, #153, #154
v2.1.37 — Permission-Mode Awareness
Two responses in one release. bkit stops asking questions nobody is there to
answer, and adapts to a Claude Code default that changed underneath it.
What changes for you
If you run --dangerously-skip-permissions, dontAsk, or acceptEdits,
bkit stops interrupting you.
| You run with | Before | After |
|---|---|---|
--dangerously-skip-permissions |
bkit still asked before scoped deletes, hard resets, pushes to main |
it does not ask |
--permission-mode dontAsk (CI) |
the same asks became silent refusals with no recourse | it does not ask |
--permission-mode acceptEdits |
asked | it does not ask |
| default / plan mode | asked | unchanged — still asks |
| any mode at all | rm -rf /, force push, curl … | sh, DROP TABLE refused |
unchanged — still refused |
Claude Code sends permission_mode on every hook event. bkit read it in zero
places, so a session started with maximum autonomy was interrupted exactly as
often as one that had asked to be. The cost was worse than noise: a PreToolUse
question needs a human answer, and an unattended run has none — so the agent
stalled instead of failing, roughly 15 minutes per incident.
The line is the decision's grade, not the mode. A question can be skipped where
nobody can answer it; a refusal cannot be skipped anywhere — the same line Claude
Code draws by keeping a circuit breaker on rm -rf / in bypass mode. Measured
across 7 modes × 21 commands: benign commands stopped 14 → 0, all 49
negative controls intact.
Three commands that used to interrupt you and no longer do:
grep -rn delete src/ a b c d e # read-only — the rule matched the WORD "delete"
npm remove lodash react vue axios dayjs # a package manager, not a file deletion
npm install --force # only during the phase-9 deployment skill
If you are on Claude Code v2.1.232 or later, bkit tells you what changed and
what it means.
v2.1.232 turns fork mode on by default in interactive sessions. A subagent's
result now arrives as a notification on a later turn, and the Agent tool no
longer accepts run_in_background, so the foreground cannot be requested.
- Your skills are unaffected. The
background: falsebkit added in v2.1.31
still holds — confirmed in the binary, not inferred. - A sprint gate that measures through a subagent reports "not measured" rather
than a score, and names fork mode as the likely cause. A missing number, never
a wrong one. - Set
CLAUDE_CODE_FORK_SUBAGENT=0to get in-turn results back.
bkit surfaces this once at SessionStart and never blocks on it.
Three git commands that destroy work are now guarded, taking the rule set to
19: git clean -f* (deletes untracked files with no reflog entry to recover
from), the three spellings that discard uncommitted work (checkout -f,
restore against the worktree, switch --discard-changes), and expiring the
reflog with an immediate prune. --amend and --no-verify are deliberately not
guarded — an amended commit survives in the reflog, and publishing the rewrite
needs a force push, which was already refused.
Your .env file is guarded on the Bash surface for the first time, and your
JavaScript is not. The rule meant to notice .env had never matched it: \b
cannot express "start of a filename" when the filename starts with a dot. It had
been matching process.env.NODE_ENV instead, so editing any file that reads
configuration prompted for confirmation. Both directions are fixed and asserted.
Compatibility
Breaking changes: 0. The hook contract is byte-identical across Claude Code
v2.1.228, v2.1.229, v2.1.231 and v2.1.232 — 10 markers measured on all four
binaries. Consecutive compatible releases: 171.
Recommended runtime stays v2.1.220. Install floor v2.1.143, runtime floor
v2.1.78, model floor v2.1.170 for the Fable-pinned agents.
Withdrawn
"PostToolUse continueOnBlock" was not unimplemented. It was unimplementable.
continueOnBlock is a configuration field on a prompt-type hook definition,
confirmed at three places in the v2.1.232 binary. All 28 bkit hook handlers are
"type": "command", so no bkit hook can carry the field at all.
It had been advertised on four surfaces, including the marketplace description
you read before installing. bkit now claims five differentiations rather than six.
The test that let it survive three releases mattered more than the claim: it
asserted that a string appeared in a markdown file — which is true of a claim
nobody implemented, true of a claim nobody can implement, and true of a claim
that is simply wrong. A regex over source text does not verify a feature. Its
replacement checks that every hook handler is still command-type, so if that ever
stops being true the claim gets re-examined on evidence instead of shipping again.
Quality
- Suite: 5,272 / 5,277 pass, 0 fail, 5 skipped across 382 files
- Live QA on a real Claude Code v2.1.232 session: 145 / 145, 0 fail — 44 skills,
34 agents, 21 hook events, 19 MCP tools, plus a new fork-mode layer that
proves the gate is live before asserting anything under it - CI gates 22 / 22
- Every new regression suite was verified failing against the pre-fix tree
What this QA did not cover is written down in the report rather than left
implied: binary measurement is macOS x86_64 only, fork mode was reached through
CLAUDE_CODE_FORK_SUBAGENT=1 rather than a genuinely interactive session, and
the stdout visibility of eight hook events was taken from Claude Code's
documentation rather than probed live.
Also fixed
- A regression guard that had never fired once since v2.1.10, because it read a
payload field Claude Code has never sent - Two guards declared
removeWhen(ccVersion)and nothing called it — on v2.1.231
they were still watching for regressions fixed 113 releases earlier git reset --hardwas auto-denied by one bkit surface and merely confirmed by
another; searching for a dangerous string was graded as performing one- Sprint iterate no longer burns up to five auto-fix cycles against a gap list
whose only entry was "no JSON in output" - Three agents now preload the five skills they declared — the field was
skills_preload, which appears nowhere in Claude Code's documentation, and 23
such declarations across six keys were removed or moved to a field that works - Hook messages composed for the model now reach it on the nine events that have
a context channel, instead of being written to a log - 148 test files ran nowhere — neither the local runner nor CI referenced
them. All are now registered scripts/cc-binary-equivalence.js— the binary measurement three analysis
cycles had rebuilt by hand, with each erratum recorded beside the line that
enforces it
Full detail: CHANGELOG.md ·
CC v2.1.228–v2.1.232 impact analysis ·
QA report
v2.1.36 — Guardrail Precision
bkit v2.1.36 — Guardrail Precision
A guard that refuses correct commands is a guard people switch off, and then it
protects nothing. bkit's own code said that in a comment. This release makes it
true.
What changed for you
Your chained commands stop being refused. Until now, every Destructive
Detector rule was matched against your whole input, so a command later in a
chain could implicate an earlier, harmless one. Real examples that were blocked:
git push origin feature-x && rm -f /tmp/scratch/note.txt # denied — rm's -f read as a force push
cp a.txt b.txt && ls / # denied — the trailing / read as a delete target
curl -o pkg.tgz https://example.com/pkg.tgz && cat ./install.sh | sh # denied
git merge-base origin/master HEAD # asked — read-only, and `merge` matched `merge-base`
ls -la ./certs/server.pem # asked — listing a key is not reading oneAll of these now run. Rules are matched against a single command segment, so
one step in a chain can no longer borrow another's tokens.
If you run bkit unattended, this is the release that matters. A guardrail
block asks a question, and an unattended run has nobody to answer it — so the
agent stalled silently rather than failing. That is how this was found: the
reporter lost ~15 minutes twice in one sprint, caught only because an idle-stall
monitor was attached.
A scoped find … -delete now asks instead of refusing. It used to be denied
outright while a scoped rm -rf was merely confirmed — the narrower operation
treated more harshly, with advice to "scope the command" you had already scoped.
The refusal message no longer sends you somewhere that does not exist. It
used to say "adjust guardrail settings in bkit.config.json or use manual
override." Neither route was real. It now lists only what works: narrow the
target, split the chain, or state your intent and confirm.
Your bkit.config.json edits either take effect or say why they don't. Five
settings the code read had no matching key in the file, so your value was
silently ignored. Twenty-seven keys in the file were read by nobody. Twelve are
now wired; the rest state in the file itself whether they are a recommendation,
a reserved name, or a duplicate.
The part we did not expect
The same defect ran in the dangerous direction too. Appending a harmless
command pushed an end-anchor out of reach or satisfied a negative lookahead:
chmod 777 / ; ls # detected by NOTHING
DELETE FROM audit_log; SELECT 1 FROM t WHERE x=1 # unscoped DELETE hidden by a later WHERE
DELETE FROM audit_log -- WHERE # hidden by a commented-out keyword
bash <<'EOF' # detected by NOTHING — the body executes
rm -rf /
EOFThe last one is the clearest illustration of why one module is not enough. The
detector strips heredoc bodies before matching, by design. The heredoc guard
graded a plain bash <<TAG as a warning, which is audited and permitted. Each
module was behaving exactly as written. The payload went between them.
chmod 777 / is the command G-008's own comment cites as its reason to exist.
Appending ; ls defeated it completely. All four are closed.
This is why the release is a correctness fix, not a comfort fix — and why it
shipped with the false-positive work rather than after it.
What the report exposed beyond the three rules
Fixing the reported rules left the detector correct and the product still wrong.
Feeding 28 ordinary developer commands and 8 ordinary file writes to the real
hook processes — the surface you actually meet — turned up seven more defects
that no module-level test could express:
git push origin feature-x && rm -f note.txtwas still refused after the
detector was fixed, by a different guard that scanned the whole line for force
flags.- The refusal advice was identical for every rule and led with "Scope the command
to a specific path" — meaningless aftercurl … | sh,DROP TABLE usersor
dd of=/dev/disk0, none of which have a path to scope. git push origin mainwas refused rather than confirmed: the guard
computed an "ask" and the hook emitted it through the deny call..env.examplewas refused as a secret — the file whose purpose is to be
committed so the next person knows which variables to set.- A force push to your own topic branch was denied as harshly as one to
main.
And one honest correction: an earlier commit in this release claimed to have
fixed the refusal message. It had rewritten a function with no production
callers. The text you actually see is assembled elsewhere, and nothing had
asserted that the "fixed" function was ever reached.
Measured
| before | after | |
|---|---|---|
| False positives (16-rule audit) | 12 | 1 — the intended grading change above |
| Reporter's 12-case harness | 4 FP / 0 missed | 0 FP / 0 missed |
| False negatives | 4 | 0 |
| Config keys read by nobody | 27 of 115 | 13, each documented |
| Config paths that never resolved | 5 | 1 — verified benign |
| Local suite | 3794/3798, 0 FAIL | 4360/4364, 0 FAIL |
Widest suite (qa-aggregate, 375 files) |
6964 PASS / 8 FAIL / 3 errored | 6977 PASS / 0 FAIL / 0 errored |
| Hook handlers probed as processes | — | 27 of 28 clean |
| Live QA, all four layers | — | 138/140 |
New tests were verified against the pre-fix tree first: 19 of 29 regression
assertions and 4 of 12 harness cases failed there. A test written after a fix
passes on arrival and proves nothing.
Decisions worth knowing about
Guardrail rules cannot be switched off at runtime, and that is deliberate
(ADR 0016). disableRule()
never worked, and a test asserts the inertness on purpose: the detector runs
inside the same agent loop whose commands it inspects, so an in-session disable
is a request the agent could issue itself. The pressure for an off-switch came
from false positives — answered here with precision instead.
One live-QA assertion was pinned to wording nobody controls. It expected
bkit's refusal text in the model's own prose. An ask goes to the permission
layer, not the transcript, so the case failed while the hook worked perfectly and
the target survived. It now reads the hook's decision directly.
For contributors
node test/run-all.js and CI disagreed about what "all tests" means: thirteen
contract tests, both host-integration suites among them, ran in CI and nowhere
else. A green local run looked complete and was not — this release found that by
pushing and watching CI fail. All thirteen are now in the local runner too, which
is why the suite total moves 3870 → 4360.
The audit method is locked as test/regression/enh-459-463-hook-path-guards.test.js
(34 cases, 13 of which fail against the pre-fix tree). It spawns the hooks as
processes and reads their JSON, because that is the only layer where several of
this release's defects were visible.
Upgrading
No migration. No configuration change required. If you had worked around a false
positive by splitting a command, you no longer need to.
Credits
@Sinclair-Seo
— reproduction script with negative controls, precise file:line root-cause
analysis for all three reported rules, and the observation that made the severity
clear.
Those negative controls earned their place: while fixing this, an over-eager SQL
comment stripper read the shell flag --command as a comment and silently
removed a real DROP TABLE from the matched text. The controls caught it before
it left the working tree. A "zero false positives" reading means nothing unless
destructive commands are still caught in the same run — a point the reporter made
after first measuring a bogus green themselves.
Their harness now ships as
test/e2e/external-dogfood/sinclair-seo-148-guardrail-precision.test.js.
Full changelog: CHANGELOG.md ·
Recommended Claude Code: v2.1.220 · Install minimum: v2.1.143
v2.1.35 — Correction
bkit v2.1.35 — Correction
An outside contributor sent a one-file security hardening patch. Reproducing its
claim found that the vulnerability it reported was not reachable — and that two
real defects had been shipping in that same file since v2.1.12, one of which had
bkit telling users, in writing, something about Claude Code that is not true.
This release is what happens when you measure a patch instead of merging it.
Highlights
bkit told you its hooks might not work in a git worktree. They work.
Until this release, starting bkit inside a linked worktree printed:
git worktree detected — Claude Code hooks may not fire (issue #46808). Run bkit from the primary repository if hook-driven automation is required.
Nobody had ever measured that. We did, with a matched control — one live
claude -p --plugin-dir session inside a linked worktree, one in the primary
checkout of the same repository, both read back from bkit's own dispatch ledger:
| linked worktree | primary checkout | |
|---|---|---|
| hook events dispatched | SessionStart, InstructionsLoaded, UserPromptSubmit, Stop, SessionEnd |
identical set |
The hooks are not degraded in any observable way. The issue the advisory cited,
anthropics/claude-code#46808,
is closed as not planned, and it is about project-level
.claude/settings.json — a different configuration source from the plugin
hooks/hooks.json that bkit actually ships.
Detection stays, because a worktree really can be missing project-scope
.claude/ configuration when that directory is untracked or gitignored. What is
gone is the claim about bkit's own hooks, the citation of a declined issue as a
live defect, and the advice to leave your worktree.
A subdirectory of an ordinary checkout was reported as a worktree
git rev-parse --git-dir answers with an absolute path. --git-common-dir
answers relative to the current working directory. bkit resolved both against
the repository toplevel — the wrong base — so from repo/sub/deep it compared
/repo/.git against /repo/../../.git and concluded you were in a worktree.
Symlinked checkouts (/tmp → /private/tmp on macOS) failed the same way.
Detection now asks git for absolute paths directly and compares through
realpath. Verified across 8 topologies with zero mismatches, and both
superseded implementations are re-implemented inside the test suite as a negative
control — so the suite provably fails if either one returns.
Every child_process call in shipped code passes an argv array
PR #146 converted one call site. Seven remained, two of which interpolated
variables into a shell string: a remote name parsed out of your own git push,
and a GitHub handle inside a quoted search expression. Neither was exploitable,
and both are now argv — with a -- separator so a leading-dash remote name
cannot be read as a flag.
This was already the project's policy; it had just never been enforced. A
contract test now enforces it mechanically, and Claude Code version detection
went from three implementations (two of them shelling out) to one.
What changes for you
| Before | After |
|---|---|
| Starting bkit from a subdirectory printed a worktree warning and left a stray flag file behind | No warning — a plain checkout is recognized as one |
| Working in a git worktree told you hooks "may not fire" and to go back to the main checkout | You're told what is actually at risk — project-scope .claude/ config — and that bkit's own hooks are unaffected, with the Claude Code version the claim was measured against recorded in the flag file |
| The warning cited an issue that had been declined, as though it were live | The advisory cites only what currently reproduces |
| Nothing else visible | Nothing else changes. No commands, skills, agents, or state formats moved. |
Upgrading is a drop-in: claude plugin update bkit. There are no migrations and
no configuration changes.
For maintainers
Three defects in this release were found by running things, not reading them, and
all three are the same shape — a claim nobody re-measured:
- The worktree advisory asserted a behavior of Claude Code that had never
been tested and had stopped being true. - A quality gate could fail because bkit was running.
SB-011compared two
reads of the developer's live.bkit/state/taken ~80 lines apart, and a bkit
session in the same repository rewritestrust-profile.jsonbetween them.
Observed ascontrol: 38, engine: 50, passing on the next run. Both readings
now come from one child process pinned to an emptyCLAUDE_PROJECT_DIR—
deterministic on a fresh clone, on a dogfooding machine, and in CI. - The test runner listed four files it could not find, and counted them as
skips. v2.1.16's stale-test cleanup deleted the files and left the manifest
entries. For 19 releases the generated report printed them under Failures
while the verdict counted them as skips — a report that listed failures it did
not count. Fixed, and a contract test now fails if the manifest and the
filesystem ever disagree again.
WorktreeCreate / WorktreeRemove registration stays deferred on the reasoning
recorded in v2.1.33 (ENH-396/418): confirmed supported by Claude Code, deferred
for the hook-count cascade. This release removes an incorrect claim about
worktrees; it does not add worktree lifecycle management.
ENH: 424–431.
Credits
@anupamme — PR #146.
A one-file execSync → execFileSync hardening patch that, on reproduction,
turned into this release. The migration is theirs and now covers the whole
repository.
The semgrep finding behind it (javascript.lang.security.detect-child-process,
HIGH) is a true description of the pattern and a false description of the risk at
that call site — all three callers passed module-internal literals. We kept and
extended the migration anyway, because removing the shell means a future caller
cannot reintroduce the primitive. That is the useful half of a scanner finding
that did not reproduce.
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
v2.1.33 — make the defenses actually enforce
bkit had several protections that detected correctly and then did nothing.
| It said | It did | |
|---|---|---|
| Destructive command | audit entry result: 'blocked' |
ran the command |
| Denied file path | described the denial to the model | wrote the file |
| Memory Enforcer block | a full reason with rule and source | sent the model the string "deny" |
| Failing test suite | — | exited 0 |
| Sprint completion | report → archived |
that sprint had never been created |
This release adds no features. It makes the existing ones true.
Found through /bkit:cc-version-analysis cycle #34 while checking Claude Code v2.1.224 → v2.1.225 for breaking changes. Upstream had none. The defects were bkit's own.
Highlights
1. Blocks now say why
The Memory Enforcer called outputBlock('deny', reason, 'PreToolUse') against a one-parameter function. JavaScript bound reason to the literal 'deny' and discarded the rest — the directive text, the rule, the source, the matched pattern. What actually reached the model was:
{"decision":"block","reason":"deny"}This is bkit differentiation #1, and it had been emitting that since v2.1.14. The consequence was not cosmetic: with no stated cause, retrying is the model's rational move, and Claude Code's auto mode pauses after 3 consecutive blocks — aborting outright in headless -p runs.
2. Commands aimed at the filesystem root were invisible
The destructive detector was called with { command } where a string was expected, so its rules matched against {"command":"…"} and every anchored pattern silently failed. Measured before the fix:
detect('Bash', 'chmod 777 /') → G-008 critical
detect('Bash', { command: 'chmod 777 /' }) → not detected
chmod 777 /, chown root / and mv /etc/passwd / were not detected at all in production. And even when a critical rule did match, nothing blocked — the branch wrote an audit entry and returned.
3. The heredoc bypass defense was defeated by an absolute path
… | bash was blocked. … | /bin/bash was not:
| bash → critical (blocked)
| /bin/bash → warning (allowed)
| nice bash → warning (allowed)
| command bash → warning (allowed)
| "bash" → warning (allowed)
| \bash → warning (allowed)
| $SHELL → warning (allowed)
Warning severity is allowed through with an audit entry. Eight literal interpreter rules became three tolerant ones covering paths, quotes, backslashes, wrapper commands, and interpreters resolved at runtime.
4. Secrets were only denied at the repository root
.env*, *.key and *.pem were effectively root-anchored — the glob expands * to [^/]*, so none could match a path containing a slash. At L4, src/.env was allowed. At L0 the same file was refused, but by the automation-level allowlist rather than the deny rule, so widening that allowlist would have opened it silently.
Paths are also matched after resolution now, so docs/../.env no longer presents an allowed spelling for a denied location.
5. Your PDCA backup was being overwritten by other projects
${CLAUDE_PLUGIN_DATA}/backup carried no project segment, while CLAUDE_PLUGIN_DATA is namespaced per plugin install. Projects sharing a marketplace slot wrote to the same file on every savePdcaStatus() — continuously, and silently.
Observed on a real machine: one slot held tene-studio's state, another held bkit-claude-code's. Whichever project lost the race lost its backup permanently. Backups are now namespaced per project.
6. The quality gate could not be failed — or passed
matchRate accepted anything with typeof === 'number', which admits NaN. That does not make the gate fail; it makes it undecidable. Both >= 90 and < 90 evaluate false, so a workflow branching on either falls straight through. This drives the iterate loop's exit condition and M1_matchRate.passed — the gate bkit is built around.
7. CI could not go red
Three independent mechanisms each made a failing suite report success: the aggregator had no process.exit at all; the workflow piped it through | tail -10 without pipefail, so the step's status came from tail; and the plugin-schema release gate carried continue-on-error: true while its own comment had promised strict mode since v2.1.21.
Proven by injecting a deliberate failure and observing both behaviours side by side:
clean exit 0
with failure exit 1 ← the aggregator gates
shell: bash (pipefail) exit 1 ← the CI step propagates
old default (no pipefail) exit 0 ← the defect, reproduced
CI coverage went from 188 to 354 test files. The directory list was hand-maintained and the file walk was not recursive, so 143 files across 13 directories — including the entire test/security suite — had never run in CI. All of them passed; they were simply never wired in.
User experience changes
bkit no longer names your session
ui.sessionTitle.enabled now defaults to false.
Issue #77 reported that bkit overwrote the session title on essentially every turn, so a name set in the Claude app or with /rename came back as [bkit] <PHASE> <feature> moments later. It was closed in v2.1.21 by adding a per-session tag, which fixed parallel windows showing identical titles but not the overwriting. Users kept renaming sessions and watching bkit rename them back.
Two causes, both fixed:
- bkit never read
session_title. Claude Code supplies the current title to hooks and its documentation names this exact use — "A hook that emitssessionTitlecan checksession_titlefirst to avoid overwriting a title the user set explicitly." bkit had zero references to it. - The dedup cache compared
action. A skill Stop hook published withaction: 'PLAN'; the next user prompt published with none; the values differed, so it republished — and again on the next skill stop. Alternating between working and typing was enough to rename your session.
If you want the labels back: set ui.sessionTitle.enabled: true in bkit.config.json. Even then, bkit will not overwrite a title you set with --name or /rename.
When bkit blocks something, you can act on it
Before, a blocked command told the model "deny" and nothing else. Now it names the rule, the source file, the matched pattern, and offers concrete alternatives — so the next attempt is a different command rather than the same one again.
Destructive commands are stopped, not just recorded
If you asked bkit to run rm -rf on a broad target, the audit log said it was blocked and the command ran anyway. It is now stopped, and the blast radius is named in the refusal.
Writes to secrets and VCS internals are refused
.env files anywhere in your tree, private keys, .git/ internals. Previously these were described to the model as a concern and then written. Automation-level scope (NOT_IN_SCOPE) stays advisory on purpose — L0's allowlist is narrow enough that blocking on it would refuse ordinary edits.
Sprint reports you can trust
Running /sprint master-plan on a new project printed Sprint "<id>" — report → archived with another sprint's summary, for a sprint whose state file did not exist. Three causes: the header id was not corrected when the fallback loaded a different sprint, master-plan was missing from the read-only action list, and advancePhase never settled status on reaching the terminal phase — leaving 6 of 7 sprints permanently marked active.
Privacy documentation corrected
PRIVACY.md claimed bkit "does not make network requests of any kind". That stopped being true when the opt-in OpenTelemetry exporter was added. The exporter is inert unless you set OTEL_EXPORTER_OTLP_ENDPOINT yourself, and it sends to a collector you choose — never to POPUP STUDIO. The page now says so, and also notes what Claude Code's feedback survey includes if you consent to it.
Verification
Test suite 354 files · 6,397 assertions · 0 fail · 0 errors
CI 354 files · 6,394 assertions · 0 fail · 0 errors
QA 30/30 — 17 static full-surface + 13 live
Gates 16/16 CI steps pass
Session baseline for comparison: 165 files, 4,308 assertions, 3 failures, 1 error — behind a gate that could not report any of it.
The QA was not sampled. Every one of the 44 skills, 34 agents, 22 hook events with their 26 handlers, both MCP servers (19 tools, real stdio handshake) and all 195 library modules were exercised. Live verification ran real claude -p --plugin-dir . sessions and asserted the effect, not the message — the guarded directory still exists, the secret file was never written.
Two fixes proved themselves by blocking the author mid-release: the destructive detector refused a test command, and the heredoc guard refused a commit message that quoted the bypass forms.
Both harnesses ship with the release (test/qa-harness-full-surface.js, test/qa-harness-live-claude-p.sh) and the full report is at docs/05-qa/v2133-defect-response.qa-report.en.md.
Known limitations
Stated rather than omitted:
- The widened heredoc guard matches a pipe anywhere inside the heredoc body, so writing about these bypass forms in a heredoc trips it.
PATH_TRAVERSALas implemented means "outsideprocess.cwd()", which covers ordinary work in another directory, so it stays advisory rather than blocking.audit-logger.jsvalidates action names withACTION_TYPES.includes(x) ? x : x— both branches are identical, so the check does nothing.- Timing budgets in
test/performance/widen under the aggregator. They are smoke checks against pathological regressions, not benchmarks.
Not done, deliberately
RECOMMENDED_VERSION stays at 2.1.220. npm stable is exactly 2.1.220, Claude Code v2.1.225 resolved none of the upstream iss...
v2.1.32 — Claude Code v2.1.219/220 compatibility
Claude Code v2.1.219 reversed v2.1.217 and made subagents spawn nested subagents at depth 3 by default. Combined with v2.1.218 moving /code-review to a background subagent, the main turn now routinely ends while subagents are still alive — which quietly broke assumptions bkit had carried since v1.5.3.
Every defect in this release was reproduced before it was fixed, using claude -p --plugin-dir . against CC v2.1.220 with a depth-2 probe and real concurrent processes. Nothing here was inferred from a changelog.
What changes for you
Your team panel was lying to you
If you have ever run /pdca team or /sprint start and seen an agent stuck at "spawning", or a teammate marked complete while it was clearly still working — that was this.
The Stop hook cleared the team roster while subagents were still running. In a clean reproduction, 4 out of 4 SubagentStop calls were orphaned: the roster was wiped, re-initialised from scratch by the next spawn, and every status update afterwards had nothing to update.
Claude Code was already telling bkit how to tell the difference — the Stop payload carries background_tasks, documented upstream as "lets hooks distinguish 'session is done' from 'session is paused waiting for background work to wake it'". bkit read it nowhere. It does now, at all three cleanup sites.
You will also notice the roster is finally accurate. Every teammate used to be labelled sonnet regardless of its actual model — 18 of bkit's 34 agents are not sonnet. Names were opaque internal IDs. Both are fixed, because the handlers stopped reading four fields Claude Code never sends and started using the ones it does.
The Claude Code version advisory had never once appeared
The install-floor warning, the ENH-368 model-floor notice for Fable-pinned agents, the recommended-version advisory — none of them had ever rendered for anyone.
Detection ran claude --version under a 200 ms budget. Claude Code now ships as a ~264 MB native binary, and that call measures 302–327 ms (5/5 runs). It could never succeed. Worse, the failure was written to cache and served back as a valid answer for the full hour, then re-cached — so it healed itself into permanent silence. This is also how RECOMMENDED_VERSION drifted twenty releases without anyone noticing.
Detection now reads the native installer's symlink first: 0 ms instead of 302–327 ms, no subprocess at all. Only successful detections earn the long cache; failures expire in a minute and retry.
/btw works again
Bare /btw answered "isn't available in this environment" — a different message from "Unknown command", meaning Claude Code knows the name and gates it. A sweep of all 28 user-invocable skills found this is the only such collision. Use /bkit:btw, which the skill now advertises.
Nested-agent guidance is honest again
cto-lead and pm-lead told you Task() was "blocked by Claude Code's nested spawn restriction", under a heading pinned to v2.1.69 — 150 releases stale. That has not been true since v2.1.219.
Worth knowing: the depth limit resolves through CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH → a remote feature gate → a hardcoded 3. It can change server-side with no Claude Code release. If you want a guaranteed bound, set the environment variable explicitly — that is now documented as the only deterministic control.
The three workflow presets are back
default, hotfix and enterprise were specified in detail by the test suite but had gone missing from the tree, so the workflow engine returned null in every installation. Restored from that specification: default keeps all nine PDCA phases at match rate 90, hotfix skips pm and design and drops to 80, enterprise adds a parallel check fan-out carrying a mandatory security-review branch at 95.
Reliability
A file lock could be stolen from its holder. Eight concurrent writers to the team roster produced seven rows, with every worker reporting success. lock() creates the lock file and then writes it, so a competing process could read zero bytes, classify it as corrupt, delete a live lock, and enter the critical section alongside the holder.
| 8 concurrent writers, 3 trials | |
|---|---|
| before | 7/8, 6/8, 7/8 |
| after | 8/8, 8/8, 8/8 |
This was pre-existing and affected every lockedUpdate consumer, including loop-breaker's cross-process counters. A companion fix: lock() never created its parent directory though write() always has, so on a fresh project the persisted counters silently never accumulated.
The test suite went from 20 failures on main to 0 (347/347). Two were genuine isolation defects that made results depend on execution order — one suite was reading and writing the repository's own token ledger, another was reading live project state — and the rest were assertions that had stopped tracking deliberate changes.
Compatibility
No breaking changes. No migration.
- Recommended Claude Code runtime: 2.1.218 → 2.1.220. v2.1.219 → 2.1.220 was verified a no-op for every surface bkit integrates against (feature gates 1754/1754 identical; hook, subagent, plugin, skill, MCP and fork/background surfaces unchanged).
- Unchanged:
MIN_VERSION2.1.78, install floor 2.1.143, Fable model floor 2.1.170. - Consecutive compatible releases: 163 (v2.1.34 – v2.1.220).
Full detail in CHANGELOG.md. Impact analysis: docs/04-report/features/cc-v2219-v2220-impact-analysis.report.{ko,en}.md.
v2.1.31 — CC v2.1.218 Fork Background-Default Compat
bkit v2.1.31 — CC v2.1.218 Fork Background-Default Compat
Restores the intended behavior of bkit's context: fork skills on Claude Code v2.1.218, which changed forked skills to run in the background by default. Nine bkit fork skills were silently affected. Skill-frontmatter-level fix; architecture counts invariant.
🎯 Highlights
- Fork skills run in the foreground again on CC ≥ 2.1.218. The 8 producer fork skills opt out of CC's new background-by-default with
background: false— no more silently-backgrounded phase scans. qa-phasestays genuinely interactive. Its PRE-SCAN CRITICAL continue/abort gate is now a realAskUserQuestion— becauseAskUserQuestionis stripped at the fork sub-agent boundary regardless of foreground/background,qa-phasedropscontext: forkand runs in the main context instead.- Grounded, not guessed. Root cause verified against the CC CHANGELOG + 4 GitHub issues (#19751 / #34592 / #46654 / #54892), which confirm
background: falserestores scheduling but notAskUserQuestion. - Zero architecture drift. 44 Skills · 34 Agents · 22 Hook Events. Version 2.1.30 → 2.1.31, docs=code 0 drift, all CI-gated contract tests green vs both baselines.
🔬 Root cause
CC v2.1.218 made context: fork skills background by default. Two distinct effects had to be told apart:
- Scheduling — a backgrounded fork no longer runs inline. Fixed by
background: falseon the skill frontmatter. AskUserQuestionavailability — this tool is stripped at the fork sub-agent boundary regardless of foreground/background (CC #34592 / #54892).background: falsedoes not bring it back. The only fix for a skill that must ask the user mid-run is to not fork at all.
So the response is split: 8 non-interactive producers keep context: fork + add background: false; the one interactive skill (qa-phase) leaves context: fork entirely.
🛠️ What changed
| Area | Change |
|---|---|
| 8 producer skills | Add background: false — phase-1-schema, phase-2-convention, phase-3-mockup, phase-4-api, phase-5-design-system, phase-8-review, zero-script-qa, skill-status. Backward-safe on older CC. |
skills/qa-phase/SKILL.md |
Drops context: fork → runs in main context; PRE-SCAN CRITICAL gate upgraded to a real AskUserQuestion continue/abort call. |
test/contract/deprecation-registry.json + contract-test-run.js |
New contextChanges allowance (ADR 0014 pattern) so the intentional qa-phase context removal passes L1-SK against both immutable baselines without rewriting baseline JSON. |
lib/infra/cc-version-checker.js |
MF-2: RECOMMENDED_VERSION 2.1.198 → 2.1.218; README, SessionStart advisory, and marketplace narrative synced. |
lib/cc-regression/registry.js |
MON-CC-06-51165 stale note fixed; invocation-inventory fork detection scoped to frontmatter (no longer false-matches prose). |
docs/ |
Carries in prior CC-version-analysis reports (cc-v2208 → cc-v2218 impact analyses, EN/KO pairs) + refreshed GitHub traffic stats. |
✨ User experience changes
- On CC v2.1.218+, running a phase skill (e.g.
/bkit:phase-4-api) executes in the foreground as before, instead of being quietly moved to a background task. qa-phasePRE-SCAN now surfaces a proper interactive continue/abort prompt when a CRITICAL condition is detected, rather than losing the question at the fork boundary.- SessionStart advisory now recommends CC v2.1.218 (install floor remains v2.1.143; model floor v2.1.170 for Fable-pinned agents).
- No action required for users on CC < 2.1.218 — the added
background: falseis a no-op there.
📋 Compatibility
- CC recommended: v2.1.218 · install floor: v2.1.143 (displayName schema) · model floor: v2.1.170+ (Fable-pinned agents)
- Architecture invariant: 44 Skills · 34 Agents · 22 Hook Events (25 blocks) · 195 Lib Modules · 2 MCP Servers (19 tools)
Resolves the CC v2.1.218 context: fork background-default regression. Full diff in #141.
🤖 Release notes generated with Claude Code
v2.1.30 — Stop-Hook stdin-Block Hardening (#139)
bkit v2.1.30 — Stop-Hook stdin-Block Hardening
Resolves #139 (@thenopen, surfaced via Claude Code's /doctor health-check). A real reliability fix: the Stop event hook occasionally stalled up to ~15.5 minutes — far past its own 10 s timeout — blocking the end of a turn.
🎯 Highlights
- The Stop hook can no longer stall on stdin. Worst case drops from ~15.5 min → a bounded ~2 s (normal case ~1 ms).
- Central fix protects all 36 bkit hook scripts, not just Stop — every hook that reads stdin is now resilient to a slow / held-open stdin close.
- Zero new regressions vs the previous release; the change is internal to existing modules (architecture counts unchanged: 44 Skills · 34 Agents · 22 Hook Events / 25 blocks · 195 Lib Modules).
🔬 Root cause (reproduced, not inferred)
Every bkit hook reads its payload through lib/core/io.js readStdinSync(), which used fs.readFileSync(0, 'utf8') — a blocking read on stdin with no timeout that returns only when stdin reaches EOF, i.e. when Claude Code closes the hook's stdin write-end. If CC keeps that write-end open, the hook blocks for exactly that long.
Reproduced against the real hook: stdin closed immediately → 0.19 s; writer holding the stdin pipe open 4 s → 4.07 s, with user CPU flat at 0.19 s — the process is blocked on I/O, not burning CPU, matching the reporter's aggregate profile (healthy ~0.8 s average, extreme tail, 14 timeout-cancellations across ~50 sessions / 5 days).
A/B against the pre-fix code on a 6 s held-open pipe: old fs.readFileSync(0) blocked 5,893 ms vs new 8 ms.
🛠️ What changed
| Area | Change |
|---|---|
lib/core/io.js — readStdinSync() |
Reads fd 0 incrementally with fs.readSync and returns the instant the buffer holds a complete JSON value — never waits for EOF. Raw fd → the process still exits promptly. Return contract unchanged (empty / malformed → {} unless BKIT_STRICT_STDIN=1). |
lib/core/io.js — readStdinBounded() (new) |
Async parse-early reader with a hard timeout that destroy()s stdin on resolve, so the turn-gating Stop hook is fully bounded even for no-data / truncated payloads on a held-open pipe. |
scripts/unified-stop.js |
Reads via readStdinBounded inside an async IIFE. |
lib/core/state-store.js — lock() |
CPU-burning busy-wait spin → Atomics.wait sleepSync() (no CPU burn) — addresses the issue's lock-wait note. |
lib/core/constants.js |
New STDIN_READ_TIMEOUT_MS (default 2000 ms; env override BKIT_STDIN_TIMEOUT_MS). |
✨ User experience changes
- Turns end when they should. No more multi-minute hangs at the end of a turn caused by a stalled Stop hook — the single most visible symptom the reporter hit via
/doctor. - Resilience is now global. Because the fix is in the shared stdin reader, every bkit hook event (PreToolUse, PostToolUse, Stop, SessionEnd, …) is protected — not only the one that was reported.
- No action required. Behavior is unchanged for normal payloads and there is nothing to configure. If you ever need to tune the bound, set
BKIT_STDIN_TIMEOUT_MS. - Lower background CPU under lock contention. The lock backoff no longer pins a CPU core while waiting.
✅ Verification
- New 16-TC regression test
test/regression/issue-139-stdin-bounded.test.js(stable across 5 runs). - End-to-end payload-consumption proof: the fixed hook, run against a held-open pipe, recorded the exact token values to the ledger (
parseStatus: ok). - Full
run-all.jssuite compared against a cleanmainworktree: 0 new failures (the fix even repaired a pre-existing version-consistency gap inbkit-system/docs). - All CI gates green (contract L1/L4, L5 Invocation Inventory).
- Live
claude -p --plugin-dir .on Claude Code v2.1.208.
🙏 Credits
Thank you @thenopen for the precise, /doctor-sourced report with aggregate timing evidence that scoped the root cause exactly.
🤖 This release was investigated, implemented, QA'd, and shipped with Claude Code — session: https://claude.ai/code/session_01WCr8qz6Acx4uFLXmbcikRJ
v2.1.29 — PDCA Predecessor-Task Completion Chain
bkit v2.1.29 — PDCA Predecessor-Task Completion Chain
A focused, cosmetic-but-real fix for the pdca skill's Task lifecycle, reported by external dogfooder @hslee-cmyk with a full reproduction (issue #137). Same task-lifecycle area as v2.1.27 (#132) and v2.1.28 (#135).
The problem
The pdca skill chains phase Tasks with blockedBy ([Plan]→[Design]→[Do]→[Check]→…), but skills/pdca/SKILL.md documented Task creation only. No step ever told the model to mark the predecessor phase Task completed when advancing — and no hook did it either. So a predecessor left in_progress (e.g. [Design] for the entire Do phase) leaked a stale phase into Claude Code's ambient prompt context on every turn, disagreeing with .bkit/state/pdca-status.json's phase field — the phase source of truth, which was correct the whole time.
Nothing functional broke (PDCA state and deliverables were always correct); the bug was purely the confusing two-sources-of-truth discrepancy the stale Task surfaced.
What changed for you
- The PDCA task list now stays honest. When bkit advances a phase, it first marks the previous phase's Task
completed— so the task list you see in Claude Code matches the actual phase at all times. No more "still in Design" signal while you're already in Do. - A new Phase Transition Rule in the skill's
## Task Integrationsection documents this explicitly, with the rationale, so it's discoverable rather than implicit.
Why we did NOT take the "auto-complete via hook" route
The issue suggested an alternative: have a hook auto-complete the predecessor Task. We verified against the official Claude Code hooks documentation that this is infeasible — command hooks communicate via stdout/exit-code/additionalContext only and cannot call TaskUpdate; only the model can. Any hook approach would still depend on the model acting on a reminder, making it no more reliable than an explicit skill instruction, only noisier. So we chose the deterministic, model-executed fix. No new hook, no new runtime surface, no dead code.
Scope & safety
skills/pdca/SKILL.mdonly. Related skills carry no multi-phaseblockedBychain (plan-plususes a single[Plan]Task,cc-version-analysisuses subtask tracking,sprintuses per-feature Tasks) and needed no change.- No architecture-count or runtime-behavior change — 44 skills / 34 agents / 22 hook events / 195 lib modules unchanged. The
TaskCreated/TaskCompletedaudit + auto-advance handlers are untouched. - Regression-guarded: new
test/regression/issue-137-predecessor-task-completion.test.js(25 assertions) fails if any per-transition completion instruction is removed. - Zero new regressions vs the
mainbaseline (identical failing-file set); verified live viaclaude -p … --plugin-dir ..
Full changelog: see CHANGELOG.md. Thanks again to @hslee-cmyk for a precise, reproducible report. 🙏