claudemd-cli 0.17.0 → 0.17.2
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 +83 -0
- package/bin/claudemd-lint.js +26 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,89 @@ 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.17.2] - 2026-05-14
|
|
12
|
+
|
|
13
|
+
**Patch — fix: 6-bug end-to-end dogfood pass. §8 SAFETY `rm -rf $VAR` whitelist closure (closes bare-`$HOME` Steam-disaster class); `transcript-vocab-scan` multi-paragraph false-negative; CLI `lint` whitespace-in-positional misclassified as path; CLI `audit` silent-OK on non-JSONL files; manifest `spec_version` drift v6.11.12 → v6.11.16; `update.js` raw Node stack trace on bogus env value.**
|
|
14
|
+
|
|
15
|
+
### Background
|
|
16
|
+
|
|
17
|
+
End-to-end agent dogfood across 3 rounds: real user paths through bin CLI, all 17 hooks, install/update/uninstall flows. 411 unit + 2 integration tests as baseline; added 18 regression tests across the 6 fixes. Highest-severity finding: a CRITICAL whitelist gap in `pre-bash-safety-check.sh` that allowed bare `rm -rf $HOME` (and `$PWD`/`$TMPDIR`/`$OLDPWD`) to pass without a `[allow-rm-rf-var]` token. The whitelist was supposed to certify the variable is shell-typed; in practice it also certified the *bare* expansion, which is exactly the Steam-disaster shape (Valve/steam-for-linux#3671: `rm -rf "$STEAM_ROOT/"*` with empty STEAM_ROOT wiped entire home dirs). Spec §8 already forbids `rm -rf $VAR without validating VAR` — the hook was simply not enforcing the spec it was supposed to enforce.
|
|
18
|
+
|
|
19
|
+
### What changed
|
|
20
|
+
|
|
21
|
+
- `[fix CRITICAL]` **`hooks/pre-bash-safety-check.sh`** — whitelisted vars (HOME/PWD/OLDPWD/TMPDIR) now require ≥1 non-`/` character in the literal-path residue (the rm-target with all `$VAR` expansions + quotes stripped). `rm -rf $HOME` / `rm -rf "$HOME"` / `rm -rf ${HOME}` / `rm -rf $HOME/` all DENY post-fix. `rm -rf $HOME/cache` / `rm -rf $HOME/*` / `rm -rf "$HOME/sub"` continue to ALLOW — subpath-bounded targets retain the prior behavior. `BASH_SAFETY_INDIRECT_CALL=1` path (`bash -c '...'` unwrapping) inherits the same check. `tests/fixtures/bash-safety/corpus.tsv` +10 rows: 7 new `deny` (bare-var shapes incl. trailing-slash) + 3 new `pass` (glob / quoted / braced subpath) — corpus-driven test goes 62 → 69.
|
|
22
|
+
|
|
23
|
+
- `[fix HIGH]` **`hooks/transcript-vocab-scan.sh`** — `jq` per-text-block `gsub("[\\r\\n]+"; " ")` before the outer `join(" ")` collapses internal newlines so the whole assistant turn is one scan-friendly line. Pre-fix, an agent turn like `"I significantly improved X.\n\nNext step is Y."` extracted as multi-line text; downstream `tail -n 1` then picked only "Next step is Y." and the §10-V hit in the first paragraph was silently dropped. The hook's docstring comment claimed `join(" ")` made each turn one line, but that only joined CONTENT BLOCKS — embedded `\n` inside a single `.text` block survived. `tests/hooks/transcript-vocab-scan.test.sh` 8 → 10 (+2 multi-paragraph anchors: first-para-only and last-para-only banned word both caught).
|
|
24
|
+
|
|
25
|
+
- `[fix MED]` **`bin/claudemd-lint.js`** — `lint` positional argument path-shape heuristic now requires no whitespace. Pre-fix, `claudemd-cli lint "Fixed crash in scripts/audit.js:42 (12/12 tests pass)"` exit 2 "file not found" because the heuristic only looked for `/`. Real paths are token-shaped; whitespace-containing positionals are inline sentences with file:line citations. `tests/scripts/lint-cli.test.js` +1 test (`sentence with /file:line citation` → text-scan + banned-vocab variant).
|
|
26
|
+
|
|
27
|
+
- `[fix MED]` **`bin/claudemd-lint.js`** — `audit` subcommand pre-flight check: a non-empty JSONL file with zero parseable JSON rows exits 2 with `"audit: no parseable JSON rows in <path> (expected JSONL transcript with one JSON object per line)"`. Pre-fix, pointing audit at a non-JSONL file (plain log, CSV, corrupted transcript) silently exited 0 with `"OK: no §10-V hits across 0 assistant turn(s)"` — CI hooks would falsely greenlight a wrong-format input. Same silent-success class as v0.9.14 / v0.9.21 lint fall-through. `parseTranscript`'s documented per-row silent-skip contract is preserved: the guard fires ONLY when 100% of non-empty lines fail to parse. `tests/scripts/lint-cli.test.js` +3 tests (non-JSONL, empty file degenerate-OK, partial-corruption preserves silent-skip).
|
|
28
|
+
|
|
29
|
+
- `[fix LOW]` **`scripts/update.js`** — `.then().catch()` wrapper translates env-shape errors (unknown `CLAUDEMD_UPDATE_CHOICE` like `YOLO`) into a one-line stderr + exit 1, mirroring the validation-error contract used by audit.js / sparkline.js. Pre-fix, an unknown choice surfaced as a 5-line Node promise-rejection stack trace (`Error: unknown choice: YOLO\n at update (file:.../update.js:41:11)...`) + exit 1 — same exit code but unreadable for users typo-ing the env var. `tests/scripts/update.test.js` +1 test (assert exit 1 + clean stderr + no `at update (file:...` stack lines).
|
|
30
|
+
|
|
31
|
+
- `[fix data]` **`spec/hard-rules.json`** — `spec_version` synced `v6.11.12` → `v6.11.16`. Four prior patch releases (v6.11.13–v6.11.16, all compression/wording-only with `§13.2 budget cost: 0`) did not add or remove HARD rules, so the manifest was never bumped. But `/claudemd-rules` and `safety-coverage-audit` both surface this field at the top of their output — users saw "Spec v6.11.12" against a v6.11.16 spec on disk. Manifest is now bumped with every spec H1 change. `tests/scripts/hard-rules-drift.test.js` +1 test (`hard-rules-7`) asserts `manifest.spec_version === spec/CLAUDE.md H1 version` — future H1 bumps that miss the manifest sync will fail CI before reaching users.
|
|
32
|
+
|
|
33
|
+
### Why patch (not minor)
|
|
34
|
+
|
|
35
|
+
All 6 changes are `fix:` per CHANGELOG convention — each restores intended/documented behavior:
|
|
36
|
+
|
|
37
|
+
- §8 SAFETY explicitly forbids `rm -rf $VAR without validating VAR`; bare `$HOME` falls under that rule. The whitelist was an over-permissive shortcut, not the documented intent.
|
|
38
|
+
- §10-V transcript scanning was documented to scan agent assistant text; truncating to the last line of the last turn was a parser implementation bug, not the documented behavior.
|
|
39
|
+
- CLI `lint` whitespace heuristic and `audit` non-JSONL exit 2 both close silent-success / spurious-error variants of the same parser-discipline class fixed in v0.9.14 / v0.9.16 / v0.9.21.
|
|
40
|
+
- `update.js` clean error contract matches the rest of the script suite.
|
|
41
|
+
- `hard-rules.json` `spec_version` sync is a data fix, no behavior change for hook consumers.
|
|
42
|
+
|
|
43
|
+
No LLM-visible metadata change. No new HARD rules. No new hook surface. No public CLI flag added or removed. Existing `[allow-rm-rf-var]` per-cmd escape hatch unchanged — users with intentional `rm -rf $HOME` patterns (rare) can still bypass with the token.
|
|
44
|
+
|
|
45
|
+
### Tests
|
|
46
|
+
|
|
47
|
+
- `bash tests/run-all.sh`: 411 node-test + 2 integration suites pass. Test count unchanged (the 6 new node-test cases offset 0 deletions; corpus + bash hook test counts grew internally: corpus 62 → 69, transcript-vocab-scan 8 → 10).
|
|
48
|
+
- `bash tests/hooks/pre-bash-safety.test.sh`: 69/69 (was 62/62 + 10 corpus rows wired in).
|
|
49
|
+
- `bash tests/hooks/transcript-vocab-scan.test.sh`: 10/10 (was 8/8).
|
|
50
|
+
- Manual end-to-end:
|
|
51
|
+
- 12 `rm -rf` shape matrix verified: bare `$HOME`/`$PWD`/`$TMPDIR`/`$OLDPWD` + trailing slash all DENY; subpath/glob/quoted/braced all ALLOW.
|
|
52
|
+
- Real CC transcript audit on 30 recent `~/.claude/projects/-mnt-data-ssd-dev-projects-claudemd/*.jsonl`: 13 flagged with §10-V hits (signal validated against production prose).
|
|
53
|
+
|
|
54
|
+
### Operator notes
|
|
55
|
+
|
|
56
|
+
- **Breaking change risk: low.** `rm -rf $HOME` (bare, no subpath) is now denied — but no real workflow runs that intentionally; it's a footgun. Workflows using `rm -rf $HOME/<subpath>` (the normal shape) are unaffected.
|
|
57
|
+
- **Update path:** plugin marketplace update + `/reload-plugins`; the `${CLAUDE_PLUGIN_ROOT}` expansion in hook registration means installed plugin picks up the new hook bodies automatically (`reference_plugin_root_hook_expansion.md`). No manual re-install needed.
|
|
58
|
+
- **If `rm -rf $HOME` is denied for a legitimate use case** (rare — wholesale home wipe in container build, etc.), the existing `[allow-rm-rf-var]` per-cmd escape hatch still works: `rm -rf $HOME [allow-rm-rf-var]`.
|
|
59
|
+
- **`/claudemd-rules` output:** "Spec v6.11.16" header replaces stale "Spec v6.11.12" immediately on first run post-update.
|
|
60
|
+
|
|
61
|
+
## [0.17.1] - 2026-05-14
|
|
62
|
+
|
|
63
|
+
**Patch — fix: `memory-read-check.sh` tag-match phase now sanitizes quoted bodies (and heredoc bodies / line comments) before tag scan; closes the FP class where descriptive text inside `--title "..."` / `-m "..."` / `'release/...'` triggered §11 deny on incidental keyword matches.**
|
|
64
|
+
|
|
65
|
+
### Background
|
|
66
|
+
|
|
67
|
+
User dogfood report: `glab mr create --title "fix(ws): ... 修 Mac packaged ..."` denied by §11 with `feedback_linux_case_audit.md` listed, even though the push had no semantic relationship to the linux-case memory. Root cause: tag `mac` (likely 3-char single-word EN tag, exact-word with declension tolerance) exact-matched `Mac` inside the quoted `--title` body.
|
|
68
|
+
|
|
69
|
+
v0.9.28 fixed the TRIGGER stage (segment-anchor `release|deploy|ship` so `git commit -m "release notes"` no longer fires the scan; Cases 14 + 22 lock). The TAG-match stage downstream was left scanning raw `$CMD` including quoted bodies — same inconsistency class. Title text is user-written description, not topic declaration; treating it as authoritative for tag matching produces FP fan-out on every MR/PR with a descriptive title.
|
|
70
|
+
|
|
71
|
+
### What changed
|
|
72
|
+
|
|
73
|
+
- `[fix]` **`hooks/memory-read-check.sh`** — new `sanitize_for_tagmatch()` function modeled on `pre-bash-safety-check.sh sanitize_cmd()`. Strips heredoc bodies (multi-line state, `<<-?TAG` introducer + bare-TAG terminator), line comments (`# ...` at line start or after whitespace), and ALL quoted-string bodies (both `"..."` and `'...'`). Simpler than `pre-bash-safety` counterpart: tag-match has no `$VAR` expansion sensitivity (the literal `$VAR` string carries no tag-relevant topic info), so `"foo"` and `"$VAR"` strip uniformly. Empty-quote markers preserved to keep token boundaries. Tag-match grep (~L134) switched from `$CMD` to `$CMD_TAGMATCH` (sanitized form). TRIGGER stage unchanged — still reads `$CMD_FLAT` and its v0.9.28 segment-anchor regex is already correct for quoted-body cases.
|
|
74
|
+
|
|
75
|
+
- `[test]` **`tests/hooks/memory-read-check.test.sh`** — 2 new cases (27 → 29 total):
|
|
76
|
+
- **Case 28**: `glab mr create --title "fix macos issue"` with MEMORY tag `[mac, ship]` — pre-fix denies on `mac` exact-match against `macos` inside quoted `--title`, post-fix sanitize strips to `--title ""` so no match → pass.
|
|
77
|
+
- **Case 29**: `git push origin 'release/v1.0'` with MEMORY tag `[release]` — pre-fix denies on `release` matching inside single-quoted branch ref, post-fix single-quote strip eliminates → pass.
|
|
78
|
+
- **Case 21 setup adjusted**: declension-tolerance test originally used `git push # added 2 hooks` form. v0.17.1 sanitize correctly strips line-comments before tag scan (comments are descriptive prose, not topic declaration), so `hooks` keyword no longer survives to match tag `hook`. Test rewritten as `git push origin hooks-fix` — branch ref is real tokenized intent and survives sanitize; locks the declension tolerance via a non-comment carrier.
|
|
79
|
+
|
|
80
|
+
### Why patch (not minor)
|
|
81
|
+
|
|
82
|
+
Per `feedback_claudemd_spec_single_source_of_truth.md` + core §2 release-requirements: this is a bugfix restoring intended/documented hook behavior (TRIGGER stage already anchored quoted-body in v0.9.28; tag-match should too — same inconsistency class). CHANGELOG `fix:` not `change:`. No LLM-visible metadata bump (spec content unchanged, only hook implementation). No contract break for hook consumers — fewer false denies, never more.
|
|
83
|
+
|
|
84
|
+
### Tests
|
|
85
|
+
|
|
86
|
+
- `tests/hooks/memory-read-check.test.sh` 29/29 PASS (was 27/27 + 2 new).
|
|
87
|
+
- All 22 hook test files PASS (no cross-regression): `pre-bash-safety 59/59`, `contract 49/49`, `banned-vocab 20/20`, `memory-coverage-scan 12/12`, etc.
|
|
88
|
+
- Full JS suite + integration test: `OVERALL: all suites passed`.
|
|
89
|
+
|
|
90
|
+
### Operator notes
|
|
91
|
+
|
|
92
|
+
No action required — hook lives via `${CLAUDE_PLUGIN_ROOT}` expansion (per `reference_plugin_root_hook_expansion.md`), so installed plugin picks up the new sanitize on next file Read. Existing `[skip-memory-check]` bypass still works for any residual FPs (e.g. unquoted tokens like `git push origin release/v1.0` without quotes are out of scope for this patch — quote-aware strip only). For unquoted-body FPs, run `/claudemd-doctor memory-tag-specificity` on your project to surface broad single-word EN tags (`mac`, `linux`, `case`) that should be made specific (`macos-shell-portability`, `linux-case-audit`).
|
|
93
|
+
|
|
11
94
|
## [0.17.0] - 2026-05-11
|
|
12
95
|
|
|
13
96
|
**Minor — refactor: spec v6.11.16 §2.1 ROUTE single-source collapse; core spec −470B (headroom 396B → 866B).**
|
package/bin/claudemd-lint.js
CHANGED
|
@@ -197,7 +197,13 @@ function lintCmd(rawArgs) {
|
|
|
197
197
|
// pre-commit-hook ergonomic where `--file` is explicit.
|
|
198
198
|
if (positional.length === 1) {
|
|
199
199
|
const arg = positional[0];
|
|
200
|
-
|
|
200
|
+
// Path-shape heuristic: contains `/`, or is `.`/`..`. Whitespace disqualifies
|
|
201
|
+
// — `lint "Fixed crash in scripts/audit.js:42 (12/12 tests pass)"` is one
|
|
202
|
+
// quoted positional whose `/` came from a file:line citation inside a
|
|
203
|
+
// sentence, not a literal file path. Without this guard, the auto-detect
|
|
204
|
+
// branch saw `/` and exited 2 with "file not found", forcing users to
|
|
205
|
+
// either omit citations from inline text or explicitly switch to --stdin.
|
|
206
|
+
const looksLikePath = (arg.includes('/') || arg === '.' || arg === '..') && !/\s/.test(arg);
|
|
201
207
|
try {
|
|
202
208
|
const st = fs.statSync(arg);
|
|
203
209
|
if (st.isFile()) {
|
|
@@ -287,6 +293,25 @@ function auditCmd(rawArgs) {
|
|
|
287
293
|
}
|
|
288
294
|
|
|
289
295
|
const jsonl = fs.readFileSync(transcriptPath, 'utf8');
|
|
296
|
+
|
|
297
|
+
// Silent-success guard: parseTranscript intentionally skips unparseable rows
|
|
298
|
+
// (matches transcript-vocab-scan.sh). But if the WHOLE file fails to parse
|
|
299
|
+
// — user pointed audit at a CSV, plain log, or corrupted JSONL — `turns.length`
|
|
300
|
+
// is 0 and the CLI happily prints "OK: 0 assistant turn(s)" exit 0. Same
|
|
301
|
+
// silent-OK family as v0.9.14 / v0.9.21. Pre-flight: a non-empty file that
|
|
302
|
+
// yields zero parseable JSON rows is malformed, not clean.
|
|
303
|
+
const nonEmptyLines = jsonl.split('\n').filter(l => l.trim().length > 0);
|
|
304
|
+
if (nonEmptyLines.length > 0) {
|
|
305
|
+
let parsedAny = false;
|
|
306
|
+
for (const l of nonEmptyLines) {
|
|
307
|
+
try { JSON.parse(l); parsedAny = true; break; } catch { /* keep scanning */ }
|
|
308
|
+
}
|
|
309
|
+
if (!parsedAny) {
|
|
310
|
+
process.stderr.write(`audit: no parseable JSON rows in ${transcriptPath} (expected JSONL transcript with one JSON object per line)\n`);
|
|
311
|
+
process.exit(2);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
290
315
|
const turns = parseTranscript(jsonl);
|
|
291
316
|
const patterns = readPatterns();
|
|
292
317
|
const annotated = turns.map(t => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudemd-cli",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
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": {
|