claudemd-cli 0.23.10 → 0.23.12

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 CHANGED
@@ -8,6 +8,65 @@ 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.23.12] - 2026-06-05
12
+
13
+ **Patch — remove the `memory-coverage-scan` Stop hook (17 → 16 hooks).** The save-side "should-have-saved" advisory shipped default-OFF since v0.13.0 and never earned its keep: 9 advisory events in 30 days, all from a single opt-in repo (this one), against a HARD-blocking read-side already in place. Cutting it resolves the only open item from the 2026-06-03 maturity audit and ends the internal-feature backlog. No spec change (`spec/CLAUDE*.md` stays v6.14.1; the hook was never a `hard-rules.json` rule). Removed: `hooks/memory-coverage-scan.sh` + its test; the `hooks.json` Stop registration; the `hook-registry.js` entry (drops the `MEMORY_COVERAGE` kill-switch + `MEMORY_COVERAGE_SCAN` / `DISABLE_MEMORY_COVERAGE_HOOK` env vars — now no-ops if still set); references in README, ARCHITECTURE.md, RULE-HITS-SCHEMA.md, `claudemd-toggle.md`, `mid-spine-yield-scan.sh` header, and the contract / full-lifecycle / hook-registry / install test count assertions (all moved 17 → 16 together).
14
+
15
+ ## [0.23.11] - 2026-06-05
16
+
17
+ **Patch — batched bugfix release from a 5-round end-to-end user-test sweep + an adversarial re-audit.** 39 confirmed bugs fixed across hooks, scripts, and the standalone CLI; spec content (`spec/CLAUDE*.md`) is unchanged (stays v6.14.1). Every fix carries a reproduction + a regression test; suite 484 → 513 node tests + integration, all green. Highlights grouped by class.
18
+
19
+ ### Fixed — §8 SAFETY (immutable) bypasses
20
+
21
+ - `hooks/lib/hook-common.sh` `hook_is_readonly_bash`: removed `env` from the readonly-command allowlist. `env <cmd>` executes an arbitrary command, so `env rm -rf $VAR` / `env npx <pkg>` / `env curl …` hit the readonly fast-path (`exit 0`) and bypassed ALL FOUR PreToolUse:Bash enforcement hooks. First-token matching cannot tell bare `env` (print environment) from `env <cmd>` (exec).
22
+ - `hooks/pre-bash-safety-check.sh` `unwrap_indirect`: matches combined/extra-flag indirect-shell forms (`bash -lc`, `bash -xc`, `sh -lc`, `bash --norc -c`, `bash -x -c`) — previously only bare `-c` was unwrapped, so `bash -lc 'rm -rf $X'` slipped through.
23
+ - `hooks/pre-bash-safety-check.sh` rm-segment detector: strips leading env-var assignments AND transparent exec-wrappers (`FOO=bar rm`, `env rm`, `command rm`, `nohup rm`, `setsid rm`, `time rm`) before the `rm` check — these all execute `rm` but began with a non-`rm` token.
24
+ - `hooks/pre-bash-safety-check.sh` `sanitize_cmd`: line-comment strip runs AFTER the quote strips, and `#` inside `$`-double-quoted bodies is neutralized — so `git commit -m 'msg # note' && rm -rf $X` no longer has its chained `&& rm -rf $X` eaten as a comment.
25
+
26
+ ### Fixed — §7 / §11 enforcement gaps
27
+
28
+ - `hooks/ship-baseline-check.sh`: the `-h`/`--help` exemption is scoped to the `git push` segment and requires a standalone flag token, so a branch named `feature-h` or a `-h` in a commit message no longer exempts a red-CI push.
29
+ - `hooks/memory-read-check.sh` `sanitize_for_tagmatch`: line-comment strip moved after quote strips — `git commit -m "closes #42" && deploy <topic>` no longer has the trigger verb + topic tag eaten by the `#42`, which had silently bypassed the §11 memory gate.
30
+
31
+ ### Fixed — fail-open: hooks must never crash
32
+
33
+ - `hooks/residue-audit.sh`, `hooks/lib/rule-hits.sh`, `hooks/session-start-check.sh`, `hooks/mem-audit.sh`, `hooks/ship-baseline-check.sh`: numeric-guard every arithmetic on a corrupt/external value (`tmp-baseline.txt`, `SPEC_RESIDUE_THRESHOLD`, `CLAUDEMD_LOG_MAX_MB`, a non-numeric `denies` in `last-session-summary.json`, `smtime`). Under `set -u` these were unbound-variable crashes (exit 1) instead of fail-open; a corrupt baseline crashed every subsequent Stop.
34
+ - `hooks/session-start-check.sh` upstream-check: the 24h sentinel is touched BEFORE the network probe, so an offline user / transient git failure / non-semver remote tag no longer re-runs the 3s `git ls-remote` on every SessionStart.
35
+
36
+ ### Fixed — aggregation / audit correctness
37
+
38
+ - `scripts/lib/rule-hits-parse.js`: `readHits` counts JSON-valid-but-bad/missing/null-`ts` rows as `skipped` (was silently dropped with `skipped:0`, hiding log corruption from §13.1 review); `detectCutover` / `groupBySection` / `byTrend` guard null `ts` (`new Date(null).getTime()===0` collapsed the historical/current split to 1970). New `blockingDenyCount` helper.
39
+ - `scripts/doctor.js`, `scripts/hard-rules-audit.js`, `scripts/sparkline.js`: count the full blocking-deny family (`deny` + `deny-repeat` + `deny-prose`) instead of literal `deny` — undercounting inflated the bypass:deny ratio and FALSELY flagged healthy rules (e.g. §11-memory-read) as §0.1 demote candidates.
40
+ - `scripts/hard-rules-audit.js`, `scripts/sparkline.js`: strip hook-unit-test session sentinels (`t`/`test`) before grouping (audit.js already did), so test traffic no longer masks a cold rule or pollutes the release trend.
41
+ - `scripts/version-cascade-check.js`: derive the spec-major token from `spec_version` instead of a hardcoded `/v6\./` — a stale `v7.x` reference after a major bump is no longer silently ignored, while the plugin's own `v0.x` versions stay ignored.
42
+
43
+ ### Fixed — data loss
44
+
45
+ - `scripts/install.js`: never back up spec-over-spec (a byte-identical re-install OR a version upgrade). Pre-fix each re-install/upgrade backed up the spec itself; `restore` picks the newest backup and `pruneBackups(5)` evicts the oldest, so `CLAUDEMD_SPEC_ACTION=restore` returned the spec and enough re-runs permanently evicted the user's original personal `CLAUDE.md`. The personal backup is now the sole backup → restore always returns it. (The first cut fixed only the identical-re-install path; the re-audit completed the upgrade path.)
46
+
47
+ ### Fixed — advisory-scan false positives + cross-platform
48
+
49
+ - `hooks/transcript-structure-scan.sh`: scans only the LAST assistant turn (was concatenating all turns → phantom four-section blocks + stale prior-turn reports re-flagged every Stop).
50
+ - `hooks/transcript-vocab-scan.sh`: per-session content-hash dedup — the same prose turn no longer re-fires the §10-V advisory on every tool call in a chain.
51
+ - `hooks/session-end-check.sh`: VALIDATE detection anchored to command position — `echo "TODO: git commit later"` no longer counts as a validation and suppresses the mid-SPINE checkpoint.
52
+ - `hooks/mem-audit.sh`: drift banner bullets every entry (`IFS=$'\n - '` is a char-set, not a separator string, so it dropped the bullet on all lines but the first).
53
+ - `hooks/banned-vocab.patterns` + `scripts/lib/lint.js`: patterns use POSIX `[[:space:]]` not GNU `\s` (BSD/macOS grep treats `\s` as literal `s`, silently disabling the ratio deny); the JS CLI translates POSIX classes back to JS regex.
54
+ - `hooks/lib/platform.sh`: new `platform_timeout` (timeout → gtimeout → bash watchdog) so the upstream-check / ship-baseline CI gate / bootstrap install no longer silently no-op on a stock macOS without coreutils; `${1:-}` guards on the stat/find helpers.
55
+ - `scripts/spec-coherence-audit.js`: cross-ref regex preserves the full suffix (`-R` / `-V` / `-O`, not only `-EXT`), so a dangling `§10-R` ref no longer matches an unrelated `§10-V` heading on the audit's flagship check.
56
+
57
+ ### Fixed — CLI / lib robustness
58
+
59
+ - `scripts/status.js`: guard a manifest with `version` but no `entries` array (was a `TypeError` crash).
60
+ - `scripts/lib/argv.js` `parsePositiveInt`: numeric flags (`--days` / `--age-days` / `--prune-backups` / `--sample`) reject hex / exponential / non-integer (`0x1e`, `1e2`, `1.5`) that `Number()` silently coerced — applied across `audit.js`, `sparkline.js`, `hard-rules-audit.js`, `sampling-audit.js`, `doctor.js`, `clean-residue.js`, `lesson-bypass-audit.js`.
61
+ - `scripts/lib/backup.js`: `BACKUP_DIR_REGEX` matches same-ms collision dirs (`backup-…Z-1`) so they are listed / sorted / pruned instead of leaking forever.
62
+ - `scripts/lib/settings-merge.js`: the hand-install eviction predicate is anchored to the user's own `~/.claude/hooks/`, so claudemd uninstall no longer evicts a foreign plugin that reuses a claudemd hook basename under a different root.
63
+ - `scripts/lib/memory-tags.js`: resolve the LAST `(...md)` link target (matches the hook's greedy sed) so doctor/scan report the same file the hook enforces against.
64
+
65
+ ### Notes
66
+
67
+ - Documented best-effort limits of the §8 wrapper handling (flag-bearing `nice -n10 rm` / `timeout 5 rm`, `xargs`, `sudo` are not unwrapped) and added static guards: `tests/scripts/spec-pattern-drift.test.js` drift-7 + `tests/scripts/hook-portability.test.js` reject GNU-only `\s/\d/\w` in hook sources / patterns.
68
+ - 8 spec-content quality items (prose clarity / cross-ref) were identified but deferred to a future v6.14.2 spec patch; spec files are untouched here.
69
+
11
70
  ## [0.23.10] - 2026-06-03
12
71
 
13
72
  **Patch — fix: `memory-read-check` sanitizer leaked multi-line quoted command bodies into tag matching.** `sanitize_for_tagmatch` strips quoted `--notes` / `--title` / `-m` bodies before MEMORY.md tag matching (so release-note prose does not spuriously match memory tags), but its quote-strip `sed` was line-based and silently failed on MULTI-LINE quoted strings. A multi-paragraph `gh release create --notes "..."` leaked its prose, matching unrelated memory tags and forcing a spurious `§11 MEMORY.md read-the-file` deny + bypass — live-reproduced on the v0.23.8 / v0.23.9 release notes (their "self-dogfood" matched a `dogfood` tag).
package/README.md CHANGED
@@ -55,10 +55,10 @@ Verify in one command (Linux): `node --version && jq --version && gh --version &
55
55
 
56
56
  | Layer | Contents |
57
57
  |---|---|
58
- | 17 shell hooks | `banned-vocab-check` · `pre-bash-safety-check` · `ship-baseline-check` · `residue-audit` · `memory-read-check` · `memory-prompt-hint` · `memory-coverage-scan` · `mid-spine-yield-scan` · `sandbox-disposal-check` · `session-start-check` · `session-extended-read` · `session-summary` · `session-end-check` · `transcript-vocab-scan` · `transcript-structure-scan` · `version-sync` · `mem-audit` |
58
+ | 16 shell hooks | `banned-vocab-check` · `pre-bash-safety-check` · `ship-baseline-check` · `residue-audit` · `memory-read-check` · `memory-prompt-hint` · `mid-spine-yield-scan` · `sandbox-disposal-check` · `session-start-check` · `session-extended-read` · `session-summary` · `session-end-check` · `transcript-vocab-scan` · `transcript-structure-scan` · `version-sync` · `mem-audit` |
59
59
  | 13 slash commands | `/claudemd-install` · `/claudemd-status` · `/claudemd-update` · `/claudemd-audit` · `/claudemd-toggle` · `/claudemd-doctor` · `/claudemd-analyze` · `/claudemd-uninstall` · `/claudemd-rules` · `/claudemd-clean-residue` · `/claudemd-sparkline` · `/claudemd-sampling-audit` · `/claudemd-bypass-audit` |
60
60
  | 1 standalone CLI | `claudemd-cli lint` · `claudemd-cli audit` ([npm: `claudemd-cli`](https://www.npmjs.com/package/claudemd-cli)) |
61
- | Spec v6.14.0 | `~/.claude/CLAUDE.md` · `CLAUDE-extended.md` · `CLAUDE-changelog.md` · `OPERATOR.md` (backup-before-overwrite) |
61
+ | Spec v6.14 | `~/.claude/CLAUDE.md` · `CLAUDE-extended.md` · `CLAUDE-changelog.md` · `OPERATOR.md` (backup-before-overwrite) |
62
62
 
63
63
  Install moves any existing `~/.claude/CLAUDE*.md` to `~/.claude/backup-<ISO>/` (last 5 kept automatically). Uninstall offers `keep / restore / delete`; `delete` requires an extra confirmation.
64
64
 
@@ -177,7 +177,6 @@ export DISABLE_TRANSCRIPT_STRUCTURE_SCAN_HOOK=1 # v0.9.10+ — Stop §10 four-s
177
177
  export DISABLE_MEM_AUDIT_HOOK=1 # v0.9.4+ — Stop Why:-less citation advisory
178
178
  export DISABLE_SESSION_END_CHECK_HOOK=1 # v0.9.27+ — SessionEnd §11-session-exit mid-SPINE check
179
179
  export DISABLE_SESSION_EXTENDED_READ_HOOK=1 # v0.10.1+ — PreToolUse:Read §13.1-extended-read denominator signal
180
- export DISABLE_MEMORY_COVERAGE_HOOK=1 # v0.13.0+ — Stop §11-mem-coverage advisory (opt-in via MEMORY_COVERAGE_SCAN=1)
181
180
  export DISABLE_MID_SPINE_YIELD_HOOK=1 # v0.15.0+ — Stop §11-mid-spine-yield advisory (opt-in via MID_SPINE_YIELD_SCAN=1)
182
181
  ```
183
182
 
@@ -324,7 +323,7 @@ claudemd/
324
323
  ├── .claude-plugin/
325
324
  │ ├── plugin.json # minimal manifest (name, version, author, license, keywords)
326
325
  │ └── marketplace.json # marketplace catalog entry
327
- ├── hooks/ # 17 shell hooks + hooks/lib/ (hook-common, rule-hits, platform)
326
+ ├── hooks/ # 16 shell hooks + hooks/lib/ (hook-common, rule-hits, platform)
328
327
  │ └── hooks.json # authoritative hook registration (v0.1.5+); CC expands ${CLAUDE_PLUGIN_ROOT} here
329
328
  ├── commands/ # 12 slash-command markdown files
330
329
  ├── bin/ # standalone CLI entrypoint (claudemd-lint.js → `npx claudemd-cli` on npmjs.org)
@@ -4,6 +4,12 @@
4
4
  # Empty lines and lines starting with # are ignored.
5
5
  # Patterns are matched case-insensitive via grep -iE.
6
6
  #
7
+ # PORTABILITY (v0.23.11): use POSIX bracket classes ([[:space:]], [[:digit:]]),
8
+ # NOT the GNU escapes \s / \d / \w — BSD/macOS grep treats those as literal
9
+ # letters, silently disabling the pattern there. `\b` is fine (BSD-supported).
10
+ # The JS CLI (scripts/lib/lint.js) translates the POSIX classes back to \s/\d.
11
+ # tests/scripts/spec-pattern-drift.test.js guards against \s/\d/\w reappearing.
12
+ #
7
13
  # Scope: applied to extracted `-m` / `--message` body when present (the
8
14
  # common case); falls back to the ENTIRE command string when no message is
9
15
  # captured (editor-mode `git commit`, `git commit --file=PATH`, `--amend
@@ -62,7 +68,7 @@
62
68
  显著改善|值述无具体数字 — 改写成 before → after 的具体数字
63
69
 
64
70
  # ─── Baseline-less ratios (EN) ──────────────────────────────────────────────
65
- \b[0-9]+%\s+(faster|slower|better|more efficient)\b|@ratio ratio without baseline — cite before → after numbers
71
+ \b[0-9]+%[[:space:]]+(faster|slower|better|more efficient)\b|@ratio ratio without baseline — cite before → after numbers
66
72
 
67
73
  # ============================================================================
68
74
  # region: prophylactic (kept for §10-V coverage; 0 hits in last audit window)
@@ -91,7 +97,7 @@
91
97
  相当不错|评价性形容词 — 描述具体属性
92
98
 
93
99
  # ─── Baseline-less ratios (EN) ──────────────────────────────────────────────
94
- \b[0-9]+x\s+(faster|slower|better)\b|@ratio ratio without baseline — cite before → after numbers
100
+ \b[0-9]+x[[:space:]]+(faster|slower|better)\b|@ratio ratio without baseline — cite before → after numbers
95
101
 
96
102
  # ─── Baseline-less ratios (中文) ────────────────────────────────────────────
97
103
  [0-9]+%(更快|更慢|更好|更高效)|@ratio 无基线的比率 — 给出 before → after 数字
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudemd-cli",
3
- "version": "0.23.10",
3
+ "version": "0.23.12",
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,8 @@
12
12
  // JS regex notes:
13
13
  // * Patterns were authored for grep -iE (POSIX ERE). Most carry over to
14
14
  // JS regex unchanged. `\b` and `[0-9]` are equivalent. POSIX char classes
15
- // like [[:space:]] are NOT in the .patterns file.
15
+ // like [[:space:]] ARE used (they're BSD-grep-safe; `\s` is not) and are
16
+ // translated to JS equivalents by posixClassesToJs() in scan() below.
16
17
  // * `\s` means whitespace in JS — also fine.
17
18
  // * Case-insensitive matching is the contract; we always pass /i flag.
18
19
  // * Invalid regex (a future bad pattern checked in by mistake) is skipped
@@ -58,6 +59,27 @@ export function readPatterns(patternsFile = DEFAULT_PATTERNS_FILE) {
58
59
  // (commit-message context is the most common use); CLI `audit` defaults
59
60
  // to excludeRatio=true to mirror transcript-vocab-scan.sh behavior.
60
61
  // opts.patterns: pre-loaded patterns array (lets callers cache the read).
62
+ // Translate the POSIX bracket classes the patterns file uses (grep -E canonical)
63
+ // into their JS-regex equivalents. The .patterns file is authored for grep -iE
64
+ // and MUST stay BSD-grep-safe: `\s`/`\d`/`\w` are GNU-grep extensions that BSD
65
+ // (macOS) grep treats as literal letters, so the file uses `[[:space:]]` etc.
66
+ // But JS regex has no POSIX classes — `[[:space:]]` there is a char class of
67
+ // `[`,`:`,`s`,`p`,`a`,`c`,`e` — so the CLI/JS scan would silently mis-match
68
+ // without this translation. `\b` is universal (BSD grep + JS both support it).
69
+ const POSIX_TO_JS = [
70
+ [/\[\[:space:\]\]/g, '\\s'],
71
+ [/\[\[:digit:\]\]/g, '\\d'],
72
+ [/\[\[:alnum:\]\]/g, 'A-Za-z0-9'], // typically already inside a [...]
73
+ [/\[\[:alpha:\]\]/g, 'A-Za-z'],
74
+ [/\[\[:upper:\]\]/g, 'A-Z'],
75
+ [/\[\[:lower:\]\]/g, 'a-z'],
76
+ ];
77
+ function posixClassesToJs(regex) {
78
+ let out = regex;
79
+ for (const [re, repl] of POSIX_TO_JS) out = out.replace(re, repl);
80
+ return out;
81
+ }
82
+
61
83
  export function scan(text, { excludeRatio = false, patterns } = {}) {
62
84
  if (!text) return [];
63
85
  const pats = patterns || readPatterns();
@@ -66,7 +88,7 @@ export function scan(text, { excludeRatio = false, patterns } = {}) {
66
88
  if (excludeRatio && p.isRatio) continue;
67
89
  let re;
68
90
  try {
69
- re = new RegExp(p.regex, 'i');
91
+ re = new RegExp(posixClassesToJs(p.regex), 'i');
70
92
  } catch {
71
93
  continue; // bad regex — skip (fail-open)
72
94
  }