claudemd-cli 0.9.1
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 +1386 -0
- package/LICENSE +21 -0
- package/README.md +298 -0
- package/bin/claudemd-lint.js +148 -0
- package/hooks/banned-vocab.patterns +98 -0
- package/package.json +18 -0
- package/scripts/lib/lint.js +128 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sds
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# claudemd
|
|
2
|
+
|
|
3
|
+
Claude Code plugin that enforces **AI-CODING-SPEC v6.11 HARD rules** through shell hooks and ships the spec as part of the plugin.
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What it installs
|
|
10
|
+
|
|
11
|
+
| Layer | Contents |
|
|
12
|
+
|---|---|
|
|
13
|
+
| 10 shell hooks | `banned-vocab-check` · `pre-bash-safety-check` · `ship-baseline-check` · `residue-audit` · `memory-read-check` · `sandbox-disposal-check` · `session-start-check` · `session-summary` · `transcript-vocab-scan` · `version-sync` |
|
|
14
|
+
| 9 slash commands | `/claudemd-status` · `/claudemd-update` · `/claudemd-audit` · `/claudemd-toggle` · `/claudemd-doctor` · `/claudemd-uninstall` · `/claudemd-rules` · `/claudemd-clean-residue` · `/claudemd-sparkline` |
|
|
15
|
+
| 1 standalone CLI | `claudemd-cli lint` · `claudemd-cli audit` (v0.9.0+ — for git pre-commit / CI / cross-agent use; npm package: [`claudemd-cli`](https://www.npmjs.com/package/claudemd-cli); same `banned-vocab.patterns` source as the in-CC hook) |
|
|
16
|
+
| Spec v6.11.3 | `~/.claude/CLAUDE.md` · `CLAUDE-extended.md` · `CLAUDE-changelog.md` (backup-before-overwrite) |
|
|
17
|
+
|
|
18
|
+
If you already have `~/.claude/CLAUDE.md`, install moves your existing files to `~/.claude/backup-<ISO>/` (last 5 kept automatically) before writing the plugin version. Uninstall offers `keep / delete / restore`; `delete` requires an extra confirmation.
|
|
19
|
+
|
|
20
|
+
> ⚠️ **`~/.claude/CLAUDE.md` is shared real estate.** Claude Code reads this file as your user-global instructions across every project. If you've hand-written personal instructions there (`Always reply in 中文`, `My name is X`, etc.), install will back them up to `~/.claude/backup-<ISO>/CLAUDE.md` and replace them with the spec. Since v0.5.3, install prints a `[claudemd] WARN: …` line to stderr when the existing file does not look like a claudemd spec. To bring your personal instructions back on uninstall, run `CLAUDEMD_SPEC_ACTION=restore /claudemd-uninstall`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Prerequisites
|
|
25
|
+
|
|
26
|
+
| Tool | Required | Why |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `node >= 20` | yes | install / status / doctor / update scripts (`package.json` engines) |
|
|
29
|
+
| `jq` | yes | every hook parses Claude Code event JSON via `jq` — without it hooks silently fail-open |
|
|
30
|
+
| `git` | yes | `ship-baseline-check` reads HEAD body; `session-start-check` runs `git ls-remote`; manual upgrade fallback |
|
|
31
|
+
| `gh` | recommended | `ship-baseline-check` calls `gh run list` — if absent, the hook fail-opens silently and shipping on red CI is no longer blocked |
|
|
32
|
+
| `coreutils` | macOS only | hooks need GNU `timeout`. Install with `brew install coreutils`, then prepend `$(brew --prefix coreutils)/libexec/gnubin` to your `PATH` |
|
|
33
|
+
|
|
34
|
+
Verify in one command (Linux): `node --version && jq --version && gh --version && git --version && timeout --version | head -1`. macOS users can swap `timeout` for `gtimeout` if `coreutils` is bottle-installed without the `gnubin` shim.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
Run **both** slash commands inside Claude Code. First registers the GitHub marketplace, second installs the plugin:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
/plugin marketplace add sdsrss/claudemd
|
|
44
|
+
/plugin install claudemd@claudemd
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That's it for normal use. The plugin's own `hooks/hooks.json` is registered by Claude Code immediately, and the `SessionStart` hook bootstraps `install.js` in the background on your **next** Claude Code session — copying `spec/CLAUDE*.md` into `~/.claude/` (backup-before-overwrite) and writing the install manifest. Verify with `/claudemd-status` after the next session.
|
|
48
|
+
|
|
49
|
+
**Optional fast-path — activate in the current session without restarting.** If you want the spec files in `~/.claude/` immediately (e.g. you opened Claude Code specifically to install this plugin and don't want to `/exit` first), run the install script directly. Find `<version>` with `ls ~/.claude/plugins/cache/claudemd/claudemd/ | sort -V | tail -1`:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
node ~/.claude/plugins/cache/claudemd/claudemd/<version>/scripts/install.js
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> Since v0.1.5, hook registration lives in the plugin's own `hooks/hooks.json` — the Claude Code harness expands `${CLAUDE_PLUGIN_ROOT}` there automatically on every invocation, so hooks track the active plugin version without manual re-registration. `install.js`'s remaining jobs are (1) copy `spec/CLAUDE*.md` into `~/.claude/` (with backup-before-overwrite), (2) evict any legacy claudemd hook entries from prior installs (≤0.1.1 absolute-path form, 0.1.2-0.1.4 `${CLAUDE_PLUGIN_ROOT}`-in-settings.json form), and (3) write the installed manifest. It never touches other-plugin hooks. Claude Code's plugin-lifecycle `postInstall` field is not honored, so the script runs from `SessionStart` instead.
|
|
56
|
+
|
|
57
|
+
### Verify
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
/claudemd-status
|
|
61
|
+
/claudemd-doctor
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`status` reports plugin version, shipped vs installed spec version, kill-switch state, and rule-hits row count. `doctor` runs 9+ health checks with `[✓] / [△] / [✗]` markers.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Daily use
|
|
69
|
+
|
|
70
|
+
Once installed, the hooks run silently in the background:
|
|
71
|
+
|
|
72
|
+
| Trigger | Hook | What happens |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `git commit` with banned vocab (e.g. `significantly`, `70% faster`, `should work`) | `banned-vocab-check` | Blocks the commit with a message pointing to §10-V spec rule. |
|
|
75
|
+
| Bash command with `rm -rf $VAR` (unvalidated expansion) or unpinned `npx <pkg>` | `pre-bash-safety-check` (v0.5.0+) | Blocks at PreToolUse:Bash per §8 SAFETY. Bypass via `[allow-rm-rf-var]` / `[allow-npx-unpinned]` token in the command, or pin/validate the variable. |
|
|
76
|
+
| `git push` while base-branch CI is red | `ship-baseline-check` | Blocks the push (2-second `gh run list` timeout; fail-open if `gh` absent or times out). |
|
|
77
|
+
| Session end with `~/.claude/tmp/` growth > 20 entries | `residue-audit` | Advisory stderr warning; never blocks. |
|
|
78
|
+
| Bash command matching ship/push/deploy/release with an unread matched `MEMORY.md` entry | `memory-read-check` | Blocks the command with a list of memory files to Read first. |
|
|
79
|
+
| Session end with fresh `tmp.XXXXXX`-style directories | `sandbox-disposal-check` | Advisory stderr warning. |
|
|
80
|
+
| New session start with GitHub remote tag newer than local cache max version | `session-start-check` (v0.4.0+) | Injects an "upgrade available" banner via `additionalContext` listing the 4-step upgrade sequence. Rate-limited to once per 24h via `~/.claude/.claudemd-state/upstream-check.lastrun` sentinel. 3-second `git ls-remote` timeout, fail-open. |
|
|
81
|
+
| First `UserPromptSubmit` after a mid-session `/plugin install` upgrade | `version-sync` (v0.3.1+) | Backgrounds `install.js` once per session when the manifest version diverges from the active plugin's `package.json`, so `~/.claude/CLAUDE*.md` syncs without `/exit`. Sentinel-gated; fail-open. |
|
|
82
|
+
|
|
83
|
+
### Commands
|
|
84
|
+
|
|
85
|
+
| Command | Purpose |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `/claudemd-status` | Plugin version + spec version + kill-switch state + logs line count. |
|
|
88
|
+
| `/claudemd-update` | Interactive diff against plugin-shipped spec, then apply-all or cancel (spec trio is lockstep — per-file select would break §EXT cross-references). |
|
|
89
|
+
| `/claudemd-audit [--days N]` | Aggregate rule-hits over last N days (default 30). Top banned-vocab patterns, per-hook deny counts. |
|
|
90
|
+
| `/claudemd-toggle <hook-name>` | Enable/disable a specific hook by toggling `DISABLE_*_HOOK` in `settings.json` env. |
|
|
91
|
+
| `/claudemd-doctor [--prune-backups=N]` | Health checks; optionally prune `~/.claude/backup-*` dirs older than N. v0.7.1+ also flags rule sections whose bypass:deny ratio > 50% (R-N6 §0.1 demotion candidates). |
|
|
92
|
+
| `/claudemd-rules [N]` | v0.8.0+ — audit `spec/hard-rules.json` manifest over last N days (default 90, matches §13.1 quarterly cadence). Surfaces `demoteCandidates` (hook-enforced rules with 0 hits) and `staleReviews` (rules whose `last_demote_review` is null/old). |
|
|
93
|
+
| `/claudemd-sparkline [--days=A,B,C]` | v0.8.4+ R-N9 — per-`spec_section` cumulative counts of signal events across 3 windows (default 30/60/90d). Trend arrow compares per-period rate; `(newly active)` / `(silenced)` annotations flag activation/deactivation transitions. Markdown block suitable for CHANGELOG header pre-release. |
|
|
94
|
+
| `/claudemd-clean-residue [--apply]` | Dry-run-by-default cleanup of stale `claudemd-sync-*` sentinels and historical `claudemd-(mockgh\|work).*` test sandboxes. |
|
|
95
|
+
| `/claudemd-uninstall` | Pre-uninstall cleanup: clears manifest + state + log + legacy `settings.json` hook entries. Run BEFORE `/plugin uninstall claudemd@claudemd` (see [Uninstall](#uninstall)). |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Standalone CLI (v0.9.0+ R-N7)
|
|
100
|
+
|
|
101
|
+
The same `banned-vocab.patterns` source the in-CC hook uses is also exposed as a standalone Node CLI for **git pre-commit hooks, GitHub Actions, and other agents** (Codex, Cursor, OpenClaw) — i.e. anywhere outside the Claude Code process.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Dev mode (this repo, before npm publish):
|
|
105
|
+
node bin/claudemd-lint.js lint "your commit message here"
|
|
106
|
+
node bin/claudemd-lint.js audit ~/.claude/projects/<encoded>/<session>.jsonl
|
|
107
|
+
|
|
108
|
+
# After npm publish (operator-driven, not part of plugin install):
|
|
109
|
+
npx claudemd-cli lint "your commit message here"
|
|
110
|
+
npx claudemd-cli lint --stdin < message.txt
|
|
111
|
+
npx claudemd-cli audit transcript.jsonl
|
|
112
|
+
npx claudemd-cli audit transcript.jsonl --json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| Subcommand | Purpose |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `lint <text>` / `--stdin` | Scan commit-message text for §10-V banned vocab. Exit 0 clean / 1 hits. |
|
|
118
|
+
| `audit <jsonl-path>` | Scan all assistant-text turns in a Claude Code transcript jsonl. Skips `@ratio` patterns by default (chat prose has different baseline conventions); pass `--include-ratio` to include them. |
|
|
119
|
+
| `--json` | JSON output (machine-readable for CI). |
|
|
120
|
+
| `--version` / `--help` | Standard. |
|
|
121
|
+
|
|
122
|
+
**Pre-commit example (`.git/hooks/commit-msg`)**:
|
|
123
|
+
```bash
|
|
124
|
+
#!/usr/bin/env bash
|
|
125
|
+
npx claudemd-cli lint --stdin < "$1" || exit 1
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The CLI does NOT depend on `~/.claude/` state — pure stateless input → stdout/stderr + exit code. Same enforcement, anywhere Node 20+ runs.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Kill-switches (three tiers)
|
|
133
|
+
|
|
134
|
+
All visible in `/claudemd-status`.
|
|
135
|
+
|
|
136
|
+
**1. Plugin-wide.** All 9 hooks short-circuit before any logic:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
export DISABLE_CLAUDEMD_HOOKS=1
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**2. Per-hook.** Disable one hook, leave others active:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
export DISABLE_BANNED_VOCAB_HOOK=1 # or
|
|
146
|
+
export DISABLE_PRE_BASH_SAFETY_HOOK=1 # or
|
|
147
|
+
export DISABLE_SHIP_BASELINE_HOOK=1 # or
|
|
148
|
+
export DISABLE_RESIDUE_AUDIT_HOOK=1 # or
|
|
149
|
+
export DISABLE_MEMORY_READ_HOOK=1 # or
|
|
150
|
+
export DISABLE_SANDBOX_DISPOSAL_HOOK=1 # or
|
|
151
|
+
export DISABLE_SESSION_START_HOOK=1 # or
|
|
152
|
+
export DISABLE_SESSION_SUMMARY_HOOK=1 # or (v0.8.0+, Stop hook writing summary)
|
|
153
|
+
export DISABLE_USER_PROMPT_SUBMIT_HOOK=1
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**2a. Per-sub-feature** (v0.4.0+). Sub-flags inside an enabled hook, named without the `_HOOK` suffix:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
export DISABLE_UPSTREAM_CHECK=1 # only the upstream-tag-check sub-feature
|
|
160
|
+
# of session-start-check; bootstrap-on-mismatch
|
|
161
|
+
# behavior remains active.
|
|
162
|
+
|
|
163
|
+
export DISABLE_SESSION_SUMMARY_BANNER=1 # v0.8.0+ — only the SessionStart banner-emit
|
|
164
|
+
# half of session-summary; the Stop-side write
|
|
165
|
+
# of last-session-summary.json continues so
|
|
166
|
+
# the data is captured for /claudemd-audit
|
|
167
|
+
# but no additionalContext line is injected.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**3. Per-invocation escape hatches** (no env var needed; embed in the command itself):
|
|
171
|
+
|
|
172
|
+
| Escape | Where | Bypasses |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| `[allow-banned-vocab]` | commit message | `banned-vocab-check` |
|
|
175
|
+
| `known-red baseline: <reason>` | commit body | `ship-baseline-check` |
|
|
176
|
+
| `[skip-memory-check]` | bash command string | `memory-read-check` |
|
|
177
|
+
| `[allow-rm-rf-var]` | bash command string | `pre-bash-safety-check` (rm-with-var path only) |
|
|
178
|
+
| `[allow-npx-unpinned]` | bash command string | `pre-bash-safety-check` (unpinned npx path only) |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Uninstall
|
|
183
|
+
|
|
184
|
+
CC marketplace lifecycle does not fire `preUninstall`, so `/plugin uninstall claudemd@claudemd` alone leaves orphan state behind (`~/.claude/.claudemd-manifest.json`, `~/.claude/.claudemd-state/`, `~/.claude/logs/claudemd.jsonl`). Use the **two-step flow**:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
/claudemd-uninstall # clear manifest + state + log (plugin still installed)
|
|
188
|
+
/plugin uninstall claudemd@claudemd # CC removes plugin cache itself
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Reversing the order is the orphan-state vector — `${CLAUDE_PLUGIN_ROOT}` and `scripts/uninstall.js` are gone after `/plugin uninstall`, with no in-tree tool to clean up afterwards. `/claudemd-doctor` flags `[△] plugin cache: orphan manifest …` if you've already hit this.
|
|
192
|
+
|
|
193
|
+
### Spec disposition
|
|
194
|
+
|
|
195
|
+
`/claudemd-uninstall` defaults to `keep` (leaves `~/.claude/CLAUDE*.md` in place). Override via env vars before the slash command:
|
|
196
|
+
|
|
197
|
+
| Option | Env vars | Behavior |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| `keep` (default) | (none) | `~/.claude/CLAUDE*.md` left in place; settings.json hook entries cleared. |
|
|
200
|
+
| `restore` | `CLAUDEMD_SPEC_ACTION=restore` | Copies the most recent `~/.claude/backup-<ISO>/*.md` back to `~/.claude/`. Use this if your install-time stderr showed `[claudemd] WARN: existing ~/.claude/CLAUDE.md does not look like a claudemd spec` — it means your hand-written user-global instructions are sitting in the backup waiting to be brought back. |
|
|
201
|
+
| `delete` | `CLAUDEMD_SPEC_ACTION=delete CLAUDEMD_CONFIRM=1` | Hard-AUTH: removes the three spec files. |
|
|
202
|
+
|
|
203
|
+
`CLAUDEMD_PURGE=1` (env var) on `/claudemd-uninstall` also drops `~/.claude/.claudemd-state/` and your rule-hits log.
|
|
204
|
+
|
|
205
|
+
### Direct script invocation (advanced fallback)
|
|
206
|
+
|
|
207
|
+
If `/claudemd-uninstall` is unavailable (you already ran `/plugin uninstall` first and want to clean up by reaching into the cache before it gets pruned, or you need to script the uninstall outside CC):
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
CLAUDEMD_SPEC_ACTION=keep node ~/.claude/plugins/cache/claudemd/claudemd/<version>/scripts/uninstall.js
|
|
211
|
+
CLAUDEMD_SPEC_ACTION=restore node ~/.claude/plugins/cache/claudemd/claudemd/<version>/scripts/uninstall.js
|
|
212
|
+
CLAUDEMD_SPEC_ACTION=delete CLAUDEMD_CONFIRM=1 node ~/.claude/plugins/cache/claudemd/claudemd/<version>/scripts/uninstall.js
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The slash command and the script are equivalent — the slash command just supplies `${CLAUDE_PLUGIN_ROOT}` for you.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Update
|
|
220
|
+
|
|
221
|
+
Claude Code has **no** `/plugin update` slash command — it's silently ignored as unrecognized. The canonical upgrade sequence is:
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
/plugin marketplace update claudemd # refresh local marketplace clone (git fetch)
|
|
225
|
+
/plugin uninstall claudemd@claudemd # remove old plugin version
|
|
226
|
+
/plugin install claudemd@claudemd # install latest from refreshed clone
|
|
227
|
+
/reload-plugins # apply changes to current session
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Or open the interactive UI via `/plugin` → **Installed** tab → select `claudemd` → follow upgrade prompts.
|
|
231
|
+
|
|
232
|
+
After the plugin upgrade, sync the shipped spec into `~/.claude/`:
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
/claudemd-update
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The command prints per-file diff summary, then prompts `apply-all` or `cancel`. Per-file select is intentionally not supported — the spec trio (`CLAUDE.md` + `CLAUDE-extended.md` + `CLAUDE-changelog.md`) evolves lockstep, and mixing versions would dangle `§EXT §X-EXT` cross-references in Core. Backup is automatic (retained to 5). `/claudemd-update` never fetches from GitHub — it only diffs the plugin-cache spec against your `~/.claude/CLAUDE*.md`. The network fetch is Claude Code's job (via `/plugin marketplace update`).
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Troubleshooting
|
|
243
|
+
|
|
244
|
+
**`Plugin "claudemd" not found in any marketplace`** — you forgot the `/plugin marketplace add sdsrss/claudemd` step. Re-run it, then retry install.
|
|
245
|
+
|
|
246
|
+
**Hooks don't fire / `~/.claude/CLAUDE*.md` not present after install** — Claude Code's `postInstall` lifecycle is not honored, so `install.js` runs from the `SessionStart` hook on your next session, not at install time. Either start a fresh Claude Code session, or run the script manually right now (replace `<version>` with the installed version dir — see the [Install](#install) section):
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
node ~/.claude/plugins/cache/claudemd/claudemd/<version>/scripts/install.js
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Verify with `/claudemd-status` — the "log.lines" count should increment after the next hook fires.
|
|
253
|
+
|
|
254
|
+
**`/plugin update claudemd` does nothing / empty stdout** — `/plugin update` is not a valid Claude Code slash command; CC silently ignores unrecognized commands. Use the canonical sequence instead (see **Update** section above): `/plugin marketplace update claudemd` → `/plugin uninstall claudemd@claudemd` → `/plugin install claudemd@claudemd` → `/reload-plugins`. If that also fails (marketplace clone refuses to refresh), manually `git -C ~/.claude/plugins/marketplaces/claudemd fetch origin main --tags && git merge --ff-only origin/main`, then `git archive v<version> | tar -x -C ~/.claude/plugins/cache/claudemd/claudemd/<version>/`, then run that version's `scripts/install.js`.
|
|
255
|
+
|
|
256
|
+
**`Hook command references ${CLAUDE_PLUGIN_ROOT} but the hook is not associated with a plugin`** (5 errors on every `Bash` tool call + every session end) — you're on claudemd 0.1.2 / 0.1.3 / 0.1.4. Those releases wrote hook commands into `~/.claude/settings.json` under the literal `${CLAUDE_PLUGIN_ROOT}` token, but the CC harness only expands that variable for hooks defined in a plugin's own `hooks/hooks.json` — never in `settings.json`. The fix is v0.1.5+, which moves hook registration into the plugin's `hooks/hooks.json` (where the token expands correctly) and evicts the stale settings.json entries on install. Upgrade via the canonical sequence in the **Update** section above, then restart the Claude Code session to clear the cached hook registry.
|
|
257
|
+
|
|
258
|
+
**`ship-baseline-check` silently passes on red CI** — `gh` CLI is not installed, or authentication failed. Install with `brew install gh` / `apt-get install gh` and run `gh auth login`. Check with `/claudemd-doctor` — it reports `gh: missing` if absent.
|
|
259
|
+
|
|
260
|
+
**CI matrix fails on macOS** — our own CI installs `coreutils` for GNU `timeout`. If you're running hooks outside the bundled CI, ensure `timeout` is on PATH (`brew install coreutils && export PATH="$(brew --prefix coreutils)/libexec/gnubin:$PATH"`).
|
|
261
|
+
|
|
262
|
+
**`/claudemd-doctor` reports backup growth** — run `/claudemd-doctor --prune-backups=5` to keep only the 5 most recent.
|
|
263
|
+
|
|
264
|
+
**`PreToolUse:Bash hook error ... No such file or directory`** pointing at `~/.claude/hooks/banned-vocab-check.sh` — Claude Code loaded `settings.json` at session start and cached the old hand-install hook entry in memory. `install.js` migrated the on-disk entry to the cache path and moved the original shell file to `~/.claude/backup-*/hooks/`, but the running session's hook registry is still stale. Exit and restart the Claude Code session — settings.json is re-read from disk, and the error stops. (This applies to any mid-session `settings.json` change, not just claudemd.)
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Extending
|
|
269
|
+
|
|
270
|
+
- **Add a 6th hook**: see `docs/ADDING-NEW-HOOK.md` for the 5-step guide (hook script + test + plugin registration + doc + version bump).
|
|
271
|
+
- **Rule-hits log schema**: see `docs/RULE-HITS-SCHEMA.md` for the JSONL row format used by `/claudemd-audit`.
|
|
272
|
+
- **Design rationale + decisions log**: `docs/superpowers/specs/2026-04-21-claudemd-plugin-design.md`.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Project layout
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
claudemd/
|
|
280
|
+
├── .claude-plugin/
|
|
281
|
+
│ ├── plugin.json # minimal manifest (name, version, author, license, keywords)
|
|
282
|
+
│ └── marketplace.json # marketplace catalog entry
|
|
283
|
+
├── hooks/ # 10 shell hooks + hooks/lib/ (hook-common, rule-hits, platform)
|
|
284
|
+
│ └── hooks.json # authoritative hook registration (v0.1.5+); CC expands ${CLAUDE_PLUGIN_ROOT} here
|
|
285
|
+
├── commands/ # 9 slash-command markdown files
|
|
286
|
+
├── bin/ # standalone CLI entrypoint (claudemd-lint.js → `npx claudemd-cli` on npmjs.org)
|
|
287
|
+
├── scripts/ # 10 Node.js management scripts + scripts/lib/ (single-source registry, lint, etc.)
|
|
288
|
+
├── spec/ # shipped v6.11.3 CLAUDE*.md trio
|
|
289
|
+
├── tests/ # hook shell tests + Node.js tests + integration + fixtures
|
|
290
|
+
├── docs/ # ADDING-NEW-HOOK.md + RULE-HITS-SCHEMA.md + superpowers/
|
|
291
|
+
└── .github/workflows/ci.yml # ubuntu + macOS × node 20
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// claudemd-lint — CLI surface for §10-V banned-vocab + transcript scanning.
|
|
3
|
+
// Built for use OUTSIDE Claude Code: git pre-commit hooks, GitHub Actions,
|
|
4
|
+
// other agent integrations (Codex / Cursor / OpenClaw). Reuses the same
|
|
5
|
+
// pattern file (hooks/banned-vocab.patterns) the in-CC bash hooks read,
|
|
6
|
+
// so enforcement is consistent across surfaces.
|
|
7
|
+
//
|
|
8
|
+
// Once published to npm:
|
|
9
|
+
// npx claudemd lint "your commit message here"
|
|
10
|
+
// npx claudemd lint --stdin < message.txt
|
|
11
|
+
// npx claudemd audit ~/.claude/projects/.../session.jsonl
|
|
12
|
+
//
|
|
13
|
+
// Pre-publish (this repo, dev mode):
|
|
14
|
+
// node bin/claudemd-lint.js lint "..."
|
|
15
|
+
// node bin/claudemd-lint.js audit transcript.jsonl
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import {
|
|
21
|
+
scan,
|
|
22
|
+
readPatterns,
|
|
23
|
+
parseTranscript,
|
|
24
|
+
formatHumanReadable,
|
|
25
|
+
formatJSON,
|
|
26
|
+
} from '../scripts/lib/lint.js';
|
|
27
|
+
|
|
28
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const REPO_ROOT = path.resolve(HERE, '..');
|
|
30
|
+
|
|
31
|
+
const USAGE = `claudemd-lint — §10-V banned-vocab + transcript scanner
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
claudemd lint <text> Scan text for banned-vocab.
|
|
35
|
+
claudemd lint --stdin Read text from stdin.
|
|
36
|
+
claudemd audit <jsonl-path> Scan all assistant turns in a CC transcript.
|
|
37
|
+
claudemd --version Print plugin version.
|
|
38
|
+
claudemd --help Print this message.
|
|
39
|
+
|
|
40
|
+
Flags:
|
|
41
|
+
--json Emit machine-readable JSON instead of text.
|
|
42
|
+
--include-ratio (audit only) Include @ratio patterns.
|
|
43
|
+
Default OFF — chat prose has different
|
|
44
|
+
baseline conventions from commit messages.
|
|
45
|
+
|
|
46
|
+
Exit codes:
|
|
47
|
+
0 no hits
|
|
48
|
+
1 one or more hits
|
|
49
|
+
2 usage error (bad args, missing file)
|
|
50
|
+
|
|
51
|
+
Pattern source: <REPO>/hooks/banned-vocab.patterns
|
|
52
|
+
Spec: §10 Honesty rules — Specificity (HARD).`;
|
|
53
|
+
|
|
54
|
+
function readPackageVersion() {
|
|
55
|
+
try {
|
|
56
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf8'));
|
|
57
|
+
return pkg.version;
|
|
58
|
+
} catch {
|
|
59
|
+
return 'unknown';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function lintCmd(args) {
|
|
64
|
+
const json = args.includes('--json');
|
|
65
|
+
const stdin = args.includes('--stdin');
|
|
66
|
+
const positional = args.filter(a => !a.startsWith('--'));
|
|
67
|
+
|
|
68
|
+
let text;
|
|
69
|
+
if (stdin) {
|
|
70
|
+
try {
|
|
71
|
+
text = fs.readFileSync(0, 'utf8');
|
|
72
|
+
} catch (e) {
|
|
73
|
+
process.stderr.write(`lint: failed to read stdin: ${e.message}\n`);
|
|
74
|
+
process.exit(2);
|
|
75
|
+
}
|
|
76
|
+
} else if (positional.length > 0) {
|
|
77
|
+
text = positional.join(' ');
|
|
78
|
+
} else {
|
|
79
|
+
process.stderr.write('lint: text required (positional arg or --stdin)\n');
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const hits = scan(text);
|
|
84
|
+
if (json) {
|
|
85
|
+
process.stdout.write(formatJSON({ scope: 'lint', text, hits }) + '\n');
|
|
86
|
+
} else {
|
|
87
|
+
const out = formatHumanReadable({ scope: 'lint', hits });
|
|
88
|
+
if (hits.length === 0) process.stdout.write(out + '\n');
|
|
89
|
+
else process.stderr.write(out + '\n');
|
|
90
|
+
}
|
|
91
|
+
process.exit(hits.length === 0 ? 0 : 1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function auditCmd(args) {
|
|
95
|
+
const json = args.includes('--json');
|
|
96
|
+
const includeRatio = args.includes('--include-ratio');
|
|
97
|
+
const positional = args.filter(a => !a.startsWith('--'));
|
|
98
|
+
const transcriptPath = positional[0];
|
|
99
|
+
|
|
100
|
+
if (!transcriptPath) {
|
|
101
|
+
process.stderr.write('audit: <jsonl-path> required\n');
|
|
102
|
+
process.exit(2);
|
|
103
|
+
}
|
|
104
|
+
if (!fs.existsSync(transcriptPath)) {
|
|
105
|
+
process.stderr.write(`audit: file not found: ${transcriptPath}\n`);
|
|
106
|
+
process.exit(2);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const jsonl = fs.readFileSync(transcriptPath, 'utf8');
|
|
110
|
+
const turns = parseTranscript(jsonl);
|
|
111
|
+
const patterns = readPatterns();
|
|
112
|
+
const annotated = turns.map(t => ({
|
|
113
|
+
...t,
|
|
114
|
+
hits: scan(t.text, { excludeRatio: !includeRatio, patterns }),
|
|
115
|
+
}));
|
|
116
|
+
const flaggedCount = annotated.reduce((n, t) => n + (t.hits.length > 0 ? 1 : 0), 0);
|
|
117
|
+
|
|
118
|
+
if (json) {
|
|
119
|
+
process.stdout.write(formatJSON({ scope: 'audit', transcript: transcriptPath, turns: annotated }) + '\n');
|
|
120
|
+
} else {
|
|
121
|
+
const out = formatHumanReadable({ scope: 'audit', turns: annotated });
|
|
122
|
+
if (flaggedCount === 0) process.stdout.write(out + '\n');
|
|
123
|
+
else process.stderr.write(out + '\n');
|
|
124
|
+
}
|
|
125
|
+
process.exit(flaggedCount === 0 ? 0 : 1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function main() {
|
|
129
|
+
const argv = process.argv.slice(2);
|
|
130
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
131
|
+
process.stdout.write(USAGE + '\n');
|
|
132
|
+
process.exit(argv.length === 0 ? 2 : 0);
|
|
133
|
+
}
|
|
134
|
+
if (argv[0] === '--version' || argv[0] === '-v') {
|
|
135
|
+
process.stdout.write(readPackageVersion() + '\n');
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
const sub = argv[0];
|
|
139
|
+
switch (sub) {
|
|
140
|
+
case 'lint': return lintCmd(argv.slice(1));
|
|
141
|
+
case 'audit': return auditCmd(argv.slice(1));
|
|
142
|
+
default:
|
|
143
|
+
process.stderr.write(`unknown subcommand: ${sub}\n${USAGE}\n`);
|
|
144
|
+
process.exit(2);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
main();
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# ~/.claude/hooks/banned-vocab.patterns
|
|
2
|
+
# §10-V banned vocabulary — strict subset, scanned by banned-vocab-check.sh
|
|
3
|
+
# Format: <extended-regex>|<human-readable reason>
|
|
4
|
+
# Empty lines and lines starting with # are ignored.
|
|
5
|
+
# Patterns are matched case-insensitive via grep -iE.
|
|
6
|
+
#
|
|
7
|
+
# Scope: applied to extracted `-m` / `--message` body when present (the
|
|
8
|
+
# common case); falls back to the ENTIRE command string when no message is
|
|
9
|
+
# captured (editor-mode `git commit`, `git commit --file=PATH`, `--amend
|
|
10
|
+
# --no-edit`, unusual quoting). False positives possible in the fallback
|
|
11
|
+
# path only — e.g. `git commit --file=/tmp/banned-significantly.txt` scans
|
|
12
|
+
# the filename. Mitigation: add `[allow-banned-vocab]` to the commit
|
|
13
|
+
# message to bypass, OR rewrite so the hook takes the happy path
|
|
14
|
+
# (use `-m "real message"`).
|
|
15
|
+
#
|
|
16
|
+
# Design principle: only include words whose primary usage in a commit
|
|
17
|
+
# message is "agent describing its own work in vague positive terms".
|
|
18
|
+
# Skip common legitimate English/中文 (e.g. "usually", "often", "likely").
|
|
19
|
+
#
|
|
20
|
+
# Deliberately conservative — miss some, false-positive low. Ratio-class
|
|
21
|
+
# patterns (marked `@ratio` in the reason column) honor a baseline-context
|
|
22
|
+
# exemption: when the commit message carries an explicit baseline anchor
|
|
23
|
+
# (numbers on both sides of `→` / `->` / `=>`, or the literal word
|
|
24
|
+
# `baseline`), the ratio hit is suppressed. This matches §10 "ratio with
|
|
25
|
+
# baseline" and keeps spec-compliant commits like
|
|
26
|
+
# perf: rendering 240ms → 72ms (70% faster)
|
|
27
|
+
# from being denied. See banned-vocab-check.sh for the baseline check.
|
|
28
|
+
#
|
|
29
|
+
# ─── Layout policy (v0.7.0, R4) ──────────────────────────────────────────────
|
|
30
|
+
# Patterns are split into two regions by 30-day fire history. `/claudemd-audit
|
|
31
|
+
# --days=30` `topPatterns` table is the authoritative signal; this file mirrors
|
|
32
|
+
# it visually so the operator can see "what's actively gating" vs "what's
|
|
33
|
+
# carried as prophylactic spec coverage".
|
|
34
|
+
#
|
|
35
|
+
# region: high-fire ≥1 deny in the most recent 30d audit window.
|
|
36
|
+
# Scanned first (grep order matters for early-exit on
|
|
37
|
+
# common matches; ~6 patterns scan ~3× faster than the
|
|
38
|
+
# full set on a clean message).
|
|
39
|
+
# region: prophylactic 0 hits in the most recent 30d audit window.
|
|
40
|
+
# Kept for §10-V coverage (the spec lists these as
|
|
41
|
+
# banned regardless of empirical fire rate); §0.1
|
|
42
|
+
# demotion candidates if they remain at zero across
|
|
43
|
+
# 3 consecutive 30d windows. Move to §EXT §10-V
|
|
44
|
+
# reference list before deletion.
|
|
45
|
+
#
|
|
46
|
+
# Re-stratify on each /claudemd-audit review (§13.1 cadence: ~50 L2+ tasks or
|
|
47
|
+
# 4 weeks). `byBypass` field — added v0.7.0 — flags any pattern whose deny is
|
|
48
|
+
# routinely overridden via `[allow-banned-vocab]`; that's a strong demotion
|
|
49
|
+
# signal independent of fire count.
|
|
50
|
+
|
|
51
|
+
# ============================================================================
|
|
52
|
+
# region: high-fire (last audit window)
|
|
53
|
+
# ============================================================================
|
|
54
|
+
|
|
55
|
+
# ─── EN adjectives ──────────────────────────────────────────────────────────
|
|
56
|
+
\bsignificantly\b|vague magnitude without number — cite absolute or baseline ratio
|
|
57
|
+
\brobust\b|evaluative adjective — describe the property (covers X branches, handles Y input)
|
|
58
|
+
\bcomprehensive\b|vague scope — cite what is actually covered
|
|
59
|
+
\bshould work\b|hedge — verify and state, or drop the claim (§8.V1 Anti-hallucination)
|
|
60
|
+
|
|
61
|
+
# ─── 中文 adjectives ────────────────────────────────────────────────────────
|
|
62
|
+
显著改善|值述无具体数字 — 改写成 before → after 的具体数字
|
|
63
|
+
|
|
64
|
+
# ─── Baseline-less ratios (EN) ──────────────────────────────────────────────
|
|
65
|
+
\b[0-9]+%\s+(faster|slower|better|more efficient)\b|@ratio ratio without baseline — cite before → after numbers
|
|
66
|
+
|
|
67
|
+
# ============================================================================
|
|
68
|
+
# region: prophylactic (kept for §10-V coverage; 0 hits in last audit window)
|
|
69
|
+
# ============================================================================
|
|
70
|
+
|
|
71
|
+
# ─── EN adjectives ──────────────────────────────────────────────────────────
|
|
72
|
+
\bproduction[- ]ready\b|evaluative adjective — describe what you verified (tests, thresholds) instead
|
|
73
|
+
\bbest practice\b|appeal to authority — cite the specific practice + source if invoking
|
|
74
|
+
\bindustry standard\b|appeal to authority — same as best-practice
|
|
75
|
+
\bcleaner code\b|evaluative — describe the concrete change (N fewer LOC, removed helper X)
|
|
76
|
+
\bseems to work\b|hedge — verify and state, or drop the claim
|
|
77
|
+
\bappears correct\b|hedge — verify and state, or drop the claim
|
|
78
|
+
\bin principle\b|hedge — state the specific case
|
|
79
|
+
\bin theory\b|hedge — state the specific case
|
|
80
|
+
\bpresumably\b|hedge — verify and state
|
|
81
|
+
\bit should be fine\b|hedge — verify and state
|
|
82
|
+
|
|
83
|
+
# ─── 中文 adjectives ────────────────────────────────────────────────────────
|
|
84
|
+
显著提升|值述无具体数字 — 改写成 baseline → target 数字
|
|
85
|
+
显著优于|评价性形容词 — 描述具体属性 (覆盖 N 分支 / 处理 X 输入)
|
|
86
|
+
大幅提升|值述无基线 — 给出 baseline → target 数字
|
|
87
|
+
大幅改善|值述无基线 — 给出 before → after 数字
|
|
88
|
+
更高效|未给基线的比较 — 给出具体对比数据
|
|
89
|
+
明显优于|评价性形容词 — 描述具体属性或覆盖范围
|
|
90
|
+
基本可用|软肯定 — 改为具体覆盖范围或 [PARTIAL]
|
|
91
|
+
相当不错|评价性形容词 — 描述具体属性
|
|
92
|
+
|
|
93
|
+
# ─── Baseline-less ratios (EN) ──────────────────────────────────────────────
|
|
94
|
+
\b[0-9]+x\s+(faster|slower|better)\b|@ratio ratio without baseline — cite before → after numbers
|
|
95
|
+
|
|
96
|
+
# ─── Baseline-less ratios (中文) ────────────────────────────────────────────
|
|
97
|
+
[0-9]+%(更快|更慢|更好|更高效)|@ratio 无基线的比率 — 给出 before → after 数字
|
|
98
|
+
[0-9]+倍(提升|加速|改善)|@ratio 无基线的比率 — 给出 before → after 数字
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claudemd-cli",
|
|
3
|
+
"version": "0.9.1",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claudemd-cli": "./bin/claudemd-lint.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "bash tests/run-all.sh",
|
|
11
|
+
"test:scripts": "node --test tests/scripts/*.test.js",
|
|
12
|
+
"test:hooks": "bash tests/hooks/*.test.sh"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT"
|
|
18
|
+
}
|