claudemd-cli 0.47.0 → 0.47.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/package.json +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,55 @@ All notable changes to the `claudemd` plugin. This changelog tracks plugin artif
|
|
|
8
8
|
- **Canonical spec version source**: `spec/CLAUDE.md` top-line title (`# AI-CODING-SPEC vX.Y.Z — Core`) + `spec/CLAUDE-changelog.md` top `##` entry.
|
|
9
9
|
- **Plugin semver vs spec semver** are independent: plugin patch (0.2.0 → 0.2.1) may ship when spec is unchanged (this release); plugin minor (0.1.9 → 0.2.0) ships when spec minor updates (v0.2.0 shipped spec v6.10.0).
|
|
10
10
|
|
|
11
|
+
## [0.47.4] - 2026-07-15
|
|
12
|
+
|
|
13
|
+
**Patch — `package.json` had been stale since v0.47.0, and it is the file that decides what the installed manifest reports.** No hook-code change; §8 behavior identical to v0.47.3.
|
|
14
|
+
|
|
15
|
+
- **Bug**: v0.47.1, v0.47.2 and v0.47.3 each bumped `.claude-plugin/plugin.json` + `.claude-plugin/marketplace.json` and left `package.json` at **0.47.0**. `scripts/lib/paths.js#readPluginVersion` reads **`package.json`** — not `plugin.json` — so `install.js` stamped `0.47.0` into `~/.claude/.claudemd-manifest.json` even when running from the `0.47.3` cache root (the manifest's own `pluginRoot` correctly said `.../0.47.3`, its `version` said `0.47.0`). Symptoms: `/claudemd-status` and `/claudemd-doctor` report a version three releases behind a correct install — which reads as "you forgot to refresh" and invites a pointless reinstall loop — and the v0.36.0 stale-root guard, which compares exactly this number, could not distinguish 0.47.0 from 0.47.3, so its protection was degraded across those releases.
|
|
16
|
+
- **Why it recurred three times**: the ship runbook's step-2 grep list is `spec/ tests/ scripts/ README.md .claude-plugin/`. `package.json` is not in it. All three releases followed the runbook faithfully.
|
|
17
|
+
- **Fix**: all four sites now at 0.47.4, and `scripts/version-cascade-check.js` gains **check 3 — plugin semver agreement** across `package.json#version`, `.claude-plugin/plugin.json#version`, `.claude-plugin/marketplace.json#metadata.version`, and `#plugins[0].version`. `package.json` is the reference site (it is what gets stamped). A missing file or key yields `null`, which cannot match, so the check fails loudly rather than skipping. Distinct from check 1, which covers the **spec** version (v6.X) — plugin semver and spec semver are independent by policy. The runbook memory now points at the script rather than the grep: mechanical beats remembered, which is the whole point — the rule was already written down and got skipped three times anyway.
|
|
18
|
+
- **Tests**: +5 (`tests/scripts/version-cascade-check.test.js`) — four-way agreement, the exact stale-`package.json` shape from this bug, a stale marketplace entry, missing-`package.json` fails-loudly-not-open, and a live assertion that the real repo agrees. Verified RED against the drifted tree (exit 1, offender listed) → GREEN after (exit 0). Suite 676 → 681 node tests; full `npm test` green.
|
|
19
|
+
|
|
20
|
+
## [0.47.3] - 2026-07-15
|
|
21
|
+
|
|
22
|
+
**Patch — three §8 false-negatives closed. Strictly deny-only: 0 of 37 probed commands went from deny to allow, 11 went from allow to deny.** Hook-code only; spec text unchanged (stays v6.19.0). One of the three is an author-introduced regression from v0.47.2, shipped four hours earlier; the other two are older. Live enforcement regressions, so shipped standalone per `OPERATOR.md §13.1`.
|
|
23
|
+
|
|
24
|
+
- **F16 — every §8 danger inside a same-line control structure was unguarded** (`hooks/pre-bash-safety-check.sh`). Pre-existing since per-segment iteration landed in v0.21.4, and the widest gap found this week. Segments split on `;`, so `if true; then rm -rf $X; fi` produces the segment `then rm -rf $X`; its first word is `then`, `rm_canon != rm`, and the whole segment was skipped. Measured ALLOW on v0.47.2: `if [ -d "$X" ]; then rm -rf "$X"; fi` (the most ordinary cleanup idiom in shell), `for x in a; do rm -rf $X; done`, `while true; do rm -rf $X; done`, `if false; then :; else rm -rf $X; fi`, and the same shapes on the npx gate (`if true; then npx unknown-pkg; fi`). Fix: `do` / `then` / `else` / `!` are shell reserved words, not command names — they join the wrapper-strip loop in both gates. The newline-separated form always denied (the keyword lands on its own segment); only the same-line form slipped. No FP risk: stripping a reserved word can only expose the real command behind it.
|
|
25
|
+
- **F17 — mktemp provenance ignored every rebind that does not spell `VAR=`** (pre-existing since v0.46.0). `S=$(mktemp -d); unset S; rm -rf "$S/build"` → ALLOW → `rm -rf /build` with S unset: the steam-for-linux#3671 class the recognizer sits next to. Same for `read S`, `S+=$EVIL`, `printf -v S`, `for S in $EVIL`, `mapfile -t S`, `declare -n r=S`, `source ./cfg.sh`, `eval "$CODE"`. Enumerating rebind syntax is a denylist that cannot be completed, so the guard inverts it: a rebind that spells the name must mention it, so count bare (non-`$`) mentions of the var and require the count to equal the number of `VAR=` assignments actually classified — a surplus is an unseen rebind. `source` / `.` / `eval` run current-shell code the scan cannot see and are rejected outright (matched pre-unwrap: `unwrap_indirect` rewrites `eval "$CODE"` to `; $CODE ;`, so a post-unwrap grep for `eval` never fires — a corpus row caught this).
|
|
26
|
+
- **Known limit, stated plainly**: the bare-count is an allowlist on the *shape of the name*, so an INDIRECT-name rebind still defeats it — `S=$(mktemp -d); unset "$T"; rm -rf "$S/build"` and `trap 'S=' DEBUG` remain ALLOW (both also allowed before this release; not regressions, not closed). §8 is a guardrail against ordinary mistakes, not a boundary against a crafted command — `DISABLE_*` and `[allow-rm-rf-var]` are bypassable by design.
|
|
27
|
+
- **Regression closed (author-introduced in v0.47.2)**: `SP=; rm -rf "$SP/build"` → ALLOW → `rm -rf /build`. The F14 loop's `[[ -n "$prov_rhs" ]] || continue` was meant to skip blank grep lines and also skipped genuine empty assignments, so "every assignment must be safe" passed vacuously. Now denied by construction — an empty RHS matches no safe class.
|
|
28
|
+
- **Corpus**: +30 rows (F16: 10 control-structure denies incl. the guarded-cleanup idiom, wrapper/assignment-prefix nesting, npx forms + 5 FP controls; F17: 11 rebind denies anchored on mktemp + 4 FP controls). Suite 307 → 337.
|
|
29
|
+
- **Evidence**: 23 rows RED against the pre-fix hook → GREEN after (337/337). Deny-only confirmed by a 37-case HEAD-vs-WIP differential: 0 loosened, 11 tightened. Full `npm test` green; shellcheck clean.
|
|
30
|
+
- **Not shipped**: literal-assignment provenance (`SP=/lit; rm -rf "$SP"` stays denied). It was built, reviewed, and rejected — see `tasks/specs/s8-literal-provenance.md`. Use `${VAR:?}`.
|
|
31
|
+
- **Known open, NOT fixed here** (`tasks/s8-sanitize-brace-heredoc.md`): two pre-existing bypasses upstream of all three gates. `rm -rf "${SP}/build"` and `rm -rf "${HOME}/"` are ALLOW (`canon_cmd_words` treats `{` as a command-position boundary and basenames the expansion apart); `SHIFT=$((1<<bits)); rm -rf "$X"` is ALLOW (`sanitize_cmd`'s heredoc regex matches the arithmetic left-shift and eats the rest of the text). Both live in the shared pre-detector pipeline and are being handled as their own change.
|
|
32
|
+
|
|
33
|
+
## [0.47.2] - 2026-07-15
|
|
34
|
+
|
|
35
|
+
**Patch — F14: the v0.46.0 mktemp-provenance recognizer had three false-negatives, each reaching the exact disaster class the gate exists for.** Hook-code only; spec text unchanged (stays v6.19.0). Deny-direction only — nothing that was blocked before is allowed now. Live enforcement regression (opened by SEC-4 two releases ago), so shipped standalone per `OPERATOR.md §13.1` rather than batched.
|
|
36
|
+
|
|
37
|
+
- **F14 — mktemp-provenance is now position-aware and reassignment-aware** (`hooks/pre-bash-safety-check.sh`): SEC-4 (v0.46.0) recognized temp-dir cleanup with a single grep for `VAR=$(mktemp` **anywhere** in the flattened command. Three FNs, all landing on the empty-var/subpath class the gate was built for (ValveSoftware/steam-for-linux#3671, cited in the whitelist branch two blocks above):
|
|
38
|
+
1. **Reassignment** — `S=$(mktemp -d); S=$EVIL; rm -rf "$S/build"` matched the *first* assignment and allowed. `$EVIL` is env-supplied and invisible; empty `$EVIL` makes the command `rm -rf /build`. The control (`rm -rf "$EVIL/build"`, no mktemp mention) correctly denied — the stray mktemp is what opened it.
|
|
39
|
+
2. **Position-blindness** — `rm -rf "$S"; S=$(mktemp -d)` allowed, but bash runs the rm **first**, with `$S` still inherited from the environment. (The `${VAR:?}` guard below it is position-agnostic for a documented reason that does not transfer: an unset var makes `rm -rf ""` a harmless no-op, whereas provenance asserts the value *is* a known temp dir.)
|
|
40
|
+
3. **Unbounded target** — `rm -rf "$S/$SUB"` rode S's provenance while `$SUB` stayed unknown.
|
|
41
|
+
Replaced with three textual, conservative conditions: (1) ≥1 assignment to the var strictly **before** this rm segment; (2) **every** assignment to that var anywhere in the command is a mktemp one (position-blind on purpose — an assignment after the rm cannot retroactively make it safe, and refusing on one over-denies rather than under-allows); (3) the rm target expands no var other than that one. The §8.V4 disposal idiom (`S=$(mktemp -d); … ; rm -rf "$S"`) is unaffected.
|
|
42
|
+
- **Accepted residual**: `S=$( mktemp -d)` (space after the paren) now denies — the RHS capture stops at the space. Deny-direction; `[allow-rm-rf-var]` is the escape.
|
|
43
|
+
- **`scripts/safety-coverage-audit.js`** — `splitClauses` no longer splits on `;`/`→` inside a backticked code span. Hook comments illustrate shell shapes (`S=$(mktemp -d); S=$EVIL; …`) whose semicolons are shell syntax, not clause separators; splitting them minted a phantom clause (`S=$EVIL`) whose only keyword can never appear in code, so a **fixed** bug's own repro was reported as an unimplemented rule. Same self-referential-heuristic class as `feedback_self_referential_marker_regex`. Verified no detection loss: claim sites 47 → 47, real gap clauses 0 → 0; the only removed gap was the phantom, and the 6 merged clauses were all backtick-span fragments (5 previously `covered`).
|
|
44
|
+
- **Corpus**: +15 rows (F14 — 7 FN denies incl. reassign-to-`/`, cmd-sub reassign, assignment-after-rm, unknown-var subpath; 8 FP controls incl. backtick mktemp, `mktemp -p`, two mktemp assignments, intervening commands). Suite 292 → 307.
|
|
45
|
+
- **Tests**: the 7 FN rows verified RED against the pre-fix hook (300/307) → GREEN after (307/307); full `npm test` green (node 676 pass / 0 fail); shellcheck clean.
|
|
46
|
+
- **Known false-deny, deliberately NOT addressed here**: `SP=/literal/path; rm -rf "$SP"` still denies even though the value is fully visible and provably non-empty. Widening allow is a separate decision from this deny-only fix — tracked in `tasks/s8-literal-provenance.md`.
|
|
47
|
+
|
|
48
|
+
## [0.47.1] - 2026-07-15
|
|
49
|
+
|
|
50
|
+
**Patch — three §8 gate defects found by probing a user's field reports: two silent bypasses (F10/F11) + one false deny (F13).** Hook-code only; spec text unchanged (stays v6.19.0). Live-enforcement regressions, so shipped same-day standalone per `OPERATOR.md §13.1` rather than batched. All three verified RED against the pre-fix hook (279/292) → GREEN after (292/292).
|
|
51
|
+
|
|
52
|
+
- **F10 — env-assignment value with a slash disabled the rm AND npx gates** (`hooks/pre-bash-safety-check.sh`): `canon_cmd_words` (added v0.42.0 SEC-2) basenames the command-position word, but a leading env-var **assignment** is not a command name. `DEBUG=/tmp/x rm -rf $EVIL` canon'd to `x rm -rf $EVIL`, so the assignment-strip loop broke at `x`, `rm_canon != rm`, and the segment was skipped entirely → **ALLOW**. Same for `PREFIX=/opt/app npx unknown-pkg`. This reopened the exact bypass class the v0.21.x wrapper-strip closed (`DEBUG=1 rm -rf $HOME`); the slash-free form still denied, which is why it survived SEC-2 review and the corpus (whose assignment rows all use slash-free values). Fix: emit assignments verbatim and keep `cmdpos=1`, so the real command word still gets canonicalized (`FOO=/a/b /usr/bin/npx pkg` → `FOO=/a/b npx pkg`).
|
|
53
|
+
- **F11 — apostrophes in double-quoted prose ate the danger token** (`hooks/pre-bash-safety-check.sh`): the single-quote strip was a line-based `sed -E "s/'[^']*'/''/g"` that ran **before**, and blind to, the double-quote state machine. `echo "it's fine" && rm -rf $X && echo "don't"` paired the two apostrophes across the `rm`, deleting it before any detector ran → **ALLOW**. Trivially reachable by accident (`git commit -m "don't panic" && rm -rf $BUILD`). Identical failure mode to the double-quote regex bug fixed earlier with a state machine — the single-quote pass was simply never converted. Fix: **one** state machine walks both quote types, so a `'` inside `"…"` and a `"` inside `'…'` are literals.
|
|
54
|
+
- **F12 — same root cause, false-deny direction**: line-based sed left multi-line `'…'` literals unstripped, so `python -c 'print(1)\nrm -rf $X'` false-denied. The unified machine reads the whole buffer (`RS="\004"`), so multi-line bodies strip like one-liners.
|
|
55
|
+
- **F13 — `(cd sub && npx tool)` subshell form false-denied a locally-installed package** (`hooks/pre-bash-safety-check.sh`): `effective_npx_cwd`'s cd-extractor anchor class was `(^|[[:space:];&|])`, which `(cd` matches neither way, so the cd was invisible, the effective cwd fell back to the parent, and `npx_pkg_locally_resolved` missed the subdir install. The v0.45.x fix covered the bare `cd sub && npx tool` shape but not the subshell idiom. Field repro: `(cd daagu/frontend && npx vue-tsc --noEmit)` denied with `vue-tsc` present in `frontend/node_modules/`. Anchor now includes `(`/`{` (matching the rm gate and npx splitter, which already treat them as separators); target class excludes `)`/`}`.
|
|
56
|
+
- **Corpus + tests**: +20 corpus rows (F10 slash-value × rm/npx/bunx/path-npx + FP controls; F11 apostrophe-straddle × rm/npx/curl-sh + quote-nesting; F12 multi-line literals) and +8 inline cwd cases (F13 subshell/brace/piped forms + 4 FP controls incl. unresolvable `$VAR` and failed `cd`). Suite 264 → 292.
|
|
57
|
+
- **Coverage note**: the corpus already had assignment-prefixed rows (`FOO=bar rm`, `FOO=bar npx`) — all slash-free. The uncovered shape was specifically a **slash-bearing value in the same segment as the rm/npx**, which is why a "precision" refactor could reopen an older FN unnoticed. Per `feedback_s8_false_negative_audit`, the FN matrix is now locked in the corpus rather than re-derived per audit.
|
|
58
|
+
- **Tests**: full `npm test` green; shellcheck clean; bash-3.2/BSD-safe (awk `\047` octal escape, no `declare -A`, no GNU-only flags).
|
|
59
|
+
|
|
11
60
|
## [0.47.0] - 2026-07-13
|
|
12
61
|
|
|
13
62
|
**Minor — B-1: npx/runner gate moves to command-position detection, closing the runner-word-in-quoted-message false positive** (the deferred item from v0.46.0). Hook-code only; spec text unchanged (stays v6.19.0). Sandbox-verified against a 32-case FN-matrix + the full corpus before landing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudemd-cli",
|
|
3
|
-
"version": "0.47.
|
|
3
|
+
"version": "0.47.4",
|
|
4
4
|
"description": "Standalone CLI for §10-V banned-vocab + transcript scanning. Companion to the claudemd Claude Code plugin (github.com/sdsrss/claudemd) for use in git pre-commit hooks, GitHub Actions, and other agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,15 @@
|
|
|
12
12
|
},
|
|
13
13
|
"homepage": "https://github.com/sdsrss/claudemd#standalone-cli-v090-r-n7",
|
|
14
14
|
"bugs": "https://github.com/sdsrss/claudemd/issues",
|
|
15
|
-
"keywords": [
|
|
15
|
+
"keywords": [
|
|
16
|
+
"lint",
|
|
17
|
+
"claude",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"banned-vocab",
|
|
20
|
+
"commit-hook",
|
|
21
|
+
"transcript-audit",
|
|
22
|
+
"spec-enforcement"
|
|
23
|
+
],
|
|
16
24
|
"scripts": {
|
|
17
25
|
"test": "bash tests/run-all.sh",
|
|
18
26
|
"test:scripts": "node --test tests/scripts/*.test.js",
|