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 issues bkit tracks, and raising it would import the #84892 (hook env stripped silently) and #84925 (conditional hooks misfiring) regressions.
Upgrading
claude plugin update bkitNo migration steps beyond the session-title default described above. Hook payload schemas, frontmatter, MCP protocol and the plugin manifest are unchanged.
Full changelog: https://github.com/popup-studio-ai/bkit-claude-code/blob/main/CHANGELOG.md
Pull request: #143