@yawlabs/ctxlint 0.14.1 → 0.15.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.
@@ -1,10 +1,10 @@
1
- - id: ctxlint
2
- name: ctxlint
3
- description: Lint AI agent context files against your actual codebase
4
- # Version-pinned so a checkout at `rev: vX.Y.Z` runs exactly that release
5
- # of ctxlint — matches the pinning done by `ctxlint init`. release.sh keeps
6
- # this in sync with package.json on each bump.
7
- entry: npx @yawlabs/ctxlint@0.14.1 --strict
8
- language: node
9
- always_run: true
10
- pass_filenames: false
1
+ - id: ctxlint
2
+ name: ctxlint
3
+ description: Lint AI agent context files against your actual codebase
4
+ # Version-pinned so a checkout at `rev: vX.Y.Z` runs exactly that release
5
+ # of ctxlint — matches the pinning done by `ctxlint init`. release.sh keeps
6
+ # this in sync with package.json on each bump.
7
+ entry: npx @yawlabs/ctxlint@0.15.1 --strict
8
+ language: node
9
+ always_run: true
10
+ pass_filenames: false
@@ -190,8 +190,8 @@ Detects canonical configuration files that have drifted between the current proj
190
190
  - **Above 90%** -- close enough. No flag. Minor differences are expected (e.g., different project names).
191
191
 
192
192
  **Notes:**
193
- - Blank lines and comment-only lines should be excluded from the overlap calculation.
194
- - Compare each sibling independently. Report the sibling with the lowest overlap percentage first.
193
+ - Lines are trimmed before comparison; blank lines and very short lines (3 characters or fewer after trimming) are excluded from the overlap calculation. Comment lines count toward overlap -- drift in the comments of a canonical file is still drift.
194
+ - Compare each sibling independently. Report the sibling with the lowest overlap percentage first (the furthest-drifted sibling is the one worth reading first).
195
195
 
196
196
  ### 2.3 session/missing-workflow
197
197
 
@@ -226,7 +226,7 @@ Detects Claude Code memory entries that reference file paths which no longer exi
226
226
  | **Trigger** | A memory file references file paths that no longer exist in the project |
227
227
  | **Message** | `Memory "<name>" references <N> path(s) that no longer exist: <paths>` |
228
228
 
229
- **Scope:** This rule only checks memory files for the current project. Claude Code stores per-project memories in `~/.claude/projects/<encoded-path>/memory/`, where `<encoded-path>` encodes the project's absolute path using `--` as the directory separator.
229
+ **Scope:** This rule only checks memory files for the current project. Claude Code stores per-project memories in `~/.claude/projects/<encoded-path>/memory/`, where `<encoded-path>` encodes the project's absolute path (each of `:`, `/`, `\`, and `.` becomes a single `-` -- see [Section 4](#4-implementing-this-specification)).
230
230
 
231
231
  **Detection algorithm:**
232
232
 
@@ -256,8 +256,8 @@ Detects memory entries from different projects that have significant content ove
256
256
  1. Enumerate all memory files across all projects in `~/.claude/projects/*/memory/*.md`.
257
257
  2. Exclude `MEMORY.md` index files (these are auto-generated summaries, not authored memories).
258
258
  3. Exclude very short entries (fewer than 50 characters after stripping whitespace) -- too short for meaningful overlap comparison.
259
- 4. Perform pairwise comparison of all remaining memory entries. Skip pairs from the same project.
260
- 5. Compute line-level overlap percentage (same algorithm as `session/diverged-file`).
259
+ 4. Perform pairwise comparison of all remaining memory entries. Skip pairs from the same project, and skip pairs where neither side belongs to the current project -- without that scoping, every lint run from any repo would resurface the same unrelated cross-project duplicates.
260
+ 5. Compute line-level overlap percentage (same algorithm as `session/diverged-file`, with a slightly higher trivial-line floor: lines of 5 characters or fewer after trimming are excluded).
261
261
  6. Flag pairs with >60% overlap.
262
262
 
263
263
  **Notes:**
@@ -274,16 +274,19 @@ Detects when an agent runs the same command 3 or more times consecutively, indic
274
274
  |---|---|
275
275
  | **Rule ID** | `session/consecutive-repeat` |
276
276
  | **Severity** | warning |
277
- | **Trigger** | 3+ consecutive history entries with identical `display` values for the current project |
277
+ | **Trigger** | 3+ consecutive history entries with identical `display` values within a single session segment for the current project |
278
278
  | **Message** | `Command run <N> times consecutively: "<command>"` |
279
279
 
280
280
  **Detection algorithm:**
281
281
 
282
- 1. Filter history entries to the current project path (normalized).
283
- 2. Sort entries by timestamp.
284
- 3. Slide a window over the sorted entries. For each run of 3+ entries with identical `display` values, emit a warning.
282
+ 1. Filter history entries to the current project path (normalized). Drop entries with no associated project path, and entries with no timestamp (readers default a missing timestamp to 0; the gap split in step 3 keys off real timestamps, and an all-zero pseudo-session would never split, so a routine daily one-shot command would read as a 3+ repeat).
283
+ 2. Sort entries by timestamp. Implementations may bound the scan to the most recent entries (the reference implementation keeps the latest 5,000) -- a live loop is always captured in the tail, and the cycle scan in `session/cyclic-pattern` is O(N²).
284
+ 3. Group entries into sessions keyed by provider + session ID (session IDs are only unique within a provider). Split each session's sequence wherever the gap between consecutive timestamps exceeds 30 minutes -- providers that omit session IDs would otherwise pool unrelated working stints into one pseudo-session, and a routine daily one-shot command would read as a 3+ repeat.
285
+ 4. Merge single-command sessions (sessions whose entire history is one entry) per provider into one chronological stream, split at the same >30-minute gaps. A rapid respawn loop (a headless one-shot command re-spawned every few seconds) produces N one-command sessions that are each below the threshold on their own; the merge keeps that pathology detectable, while runs more than 30 minutes apart (daily routine reuse) still split into separate below-threshold segments. The merged stream feeds only this rule, not `session/cyclic-pattern`.
286
+ 5. Within each segment (per-session and merged one-shot), slide a window over the entries. For each run of 3+ entries with identical `display` values, emit a warning.
285
287
 
286
288
  **Notes:**
289
+ - Looping is an intra-session pathology. Pooling full sessions would flag routine reuse across days, and concurrently interleaved sessions (including cross-provider ones, since multiple providers' histories are merged) would produce phantom patterns no session actually ran. The single-command-session merge in step 4 is the deliberate exception: N identical one-shots inside a 30-minute window are a respawn loop, not reuse.
287
290
  - Truncates long command strings to 80 characters in the message for readability.
288
291
  - This rule helps surface cases where an agent is stuck retrying a failing command instead of changing approach.
289
292
 
@@ -297,15 +300,16 @@ Detects short repeating cycles of commands, indicating an agent stuck in a loop.
297
300
  |---|---|
298
301
  | **Rule ID** | `session/cyclic-pattern` |
299
302
  | **Severity** | warning |
300
- | **Trigger** | A sequence of 2-3 distinct commands repeating 2+ times consecutively (e.g. A,B,A,B) |
303
+ | **Trigger** | A sequence of 2-3 distinct commands repeating 2+ times consecutively (e.g. A,B,A,B) within a single session segment |
301
304
  | **Message** | `Cyclic pattern repeated <N> times: <cycle>` |
302
305
 
303
306
  **Detection algorithm:**
304
307
 
305
- 1. Filter history entries to the current project, sorted by timestamp.
306
- 2. For cycle lengths 2 and 3, slide a window checking if the next `cycleLen` entries match the current cycle.
308
+ 1. Build per-session segments exactly as in `session/consecutive-repeat` steps 1-3 (current project only, sorted by timestamp, grouped by provider + session ID, split at >30-minute gaps).
309
+ 2. Within each segment, for cycle lengths 2 and 3, slide a window checking if the next `cycleLen` entries match the current cycle.
307
310
  3. Cycles where every element is the same are excluded (already caught by `session/consecutive-repeat`).
308
- 4. Subsumption: if a shorter cycle is fully contained within an already-reported longer cycle at the same position, skip it.
311
+ 4. A cycle whose span overlaps a run already reported by `session/consecutive-repeat` is suppressed -- those commands were already reported once.
312
+ 5. Subsumption: if a shorter cycle is fully contained within an already-reported longer cycle at the same position, skip it.
309
313
 
310
314
  **Notes:**
311
315
  - A cycle like "edit file → run tests → edit file → run tests" is a common pattern when an agent is making iterative fixes. This rule flags when the cycle repeats enough times to suggest the agent isn't making progress.
@@ -341,20 +345,26 @@ Detects when `MEMORY.md` exceeds Claude Code's session-load cap. Claude Code loa
341
345
 
342
346
  ## 3. Rule Catalog (machine-readable)
343
347
 
344
- A machine-readable JSON catalog of all rules is available at [`agent-session-lint-rules.json`](./agent-session-lint-rules.json).
348
+ A machine-readable JSON catalog of all rules is available at [`agent-session-lint-rules.json`](./agent-session-lint-rules.json). It conforms to the shared catalog schema ([`schemas/ctxlint-catalog.schema.json`](./schemas/ctxlint-catalog.schema.json)) used by all four pillars: each rule entry carries `id`, `category`, `severity`, `description`, `trigger`, `message`, `fixable`, and `stability`, plus rule-specific extras (e.g. `canonicalFiles` on `session/diverged-file`).
345
349
 
346
- The catalog schema follows the same structure as the sibling specs' catalogs (`context-lint-rules.json`, `mcp-config-lint-rules.json`). Each rule entry includes:
350
+ See the JSON file for the full catalog.
347
351
 
348
- | Field | Type | Description |
349
- |---|---|---|
350
- | `id` | string | Rule ID in `category/rule-name` format |
351
- | `severity` | `"error"` \| `"warning"` \| `"info"` | Default severity level |
352
- | `description` | string | One-line description of what the rule checks |
353
- | `messageTemplate` | string | Message template with `<placeholder>` variables |
354
- | `category` | string | Rule category (`session`) |
355
- | `crossProject` | boolean | Whether the rule compares across sibling repos |
352
+ ### Catalog rule IDs vs. reference-implementation ruleIds
356
353
 
357
- See the JSON file for the full catalog.
354
+ Catalog rule IDs use the pillar-stable `session/<slug>` form -- these are the cross-tool names to use in documentation, configuration, and issue reports. The reference implementation namespaces the `ruleId` it emits (in `--format json` output) by check module instead -- `<check>/<slug>` -- and splits `session/memory-index-overflow` into one emitted slug per cap dimension. The full correspondence (pinned by a consistency test in the reference implementation):
355
+
356
+ | Catalog rule ID | Emitted `ruleId` (reference implementation) |
357
+ |---|---|
358
+ | `session/missing-secret` | `session-missing-secret/missing-secret` |
359
+ | `session/diverged-file` | `session-diverged-file/diverged-file` |
360
+ | `session/missing-workflow` | `session-missing-workflow/missing-workflow` |
361
+ | `session/stale-memory` | `session-stale-memory/stale-memory` |
362
+ | `session/duplicate-memory` | `session-duplicate-memory/duplicate-memory` |
363
+ | `session/consecutive-repeat` | `session-loop-detection/consecutive-repeat` |
364
+ | `session/cyclic-pattern` | `session-loop-detection/cyclic-pattern` |
365
+ | `session/memory-index-overflow` | `session-memory-index-overflow/line-overflow`, `session-memory-index-overflow/byte-overflow` |
366
+
367
+ Other implementations of this specification may emit either form; when interoperating, treat the catalog IDs as canonical and map implementation-specific ruleIds onto them as above.
358
368
 
359
369
  ---
360
370
 
@@ -388,7 +398,7 @@ For each line:
388
398
 
389
399
  ### Claude Code project directory encoding
390
400
 
391
- Claude Code encodes project paths in its directory structure using `--` as the path separator. The encoding replaces each directory separator with `--` and strips the leading separator.
401
+ Claude Code encodes a project's absolute path into a directory name under `~/.claude/projects/` by replacing **each of `:`, `/`, `\`, and `.` with a single `-`**. Nothing is stripped: a leading `/` on Unix paths is preserved as a leading `-`. The familiar `--` run in Windows-derived names like `C--Users-...` is not a separator of its own -- it is the drive letter's `:` and the adjacent `/` each becoming `-`. Hyphens already present in a path component are preserved as-is.
392
402
 
393
403
  **Examples:**
394
404
 
@@ -397,10 +407,9 @@ Claude Code encodes project paths in its directory structure using `--` as the p
397
407
  | `C:/Users/jeff/yaw/ctxlint` | `C--Users-jeff-yaw-ctxlint` |
398
408
  | `/home/dev/projects/my-app` | `-home-dev-projects-my-app` |
399
409
  | `/Users/dev/work/api-server` | `-Users-dev-work-api-server` |
410
+ | `/home/dev/repo.js` | `-home-dev-repo-js` |
400
411
 
401
- Note: each `/` or `\` in the path becomes a single `-` in the encoded name, except when the path component itself contains a hyphen (which is preserved). The `--` separator replaces `:/` on Windows drive letters.
402
-
403
- To decode: implementors should reverse the encoding by reading the actual directory names from `~/.claude/projects/` and matching against the current project's absolute path.
412
+ The encoding is **lossy**: `-`, `.`, `/`, `\`, and `:` all collapse to the same output character, so distinct paths can encode to the same directory name (`/home/dev/my-app` and `/home/dev/my.app` collide). There is no decode step. Implementors must compare encoded-to-encoded: encode the current project's absolute path with the same substitution rules and match the result against the directory names actually present in `~/.claude/projects/` -- never attempt to reconstruct a path from an encoded name.
404
413
 
405
414
  ### Scope of v1
406
415
 
@@ -65,13 +65,13 @@ Severity levels:
65
65
  | `skill/broken-ref` | warning | A `./` or `../` path reference in the body (outside example code blocks) does not exist relative to the skill directory | `{file}: references "{path}" which does not exist relative to the skill directory` |
66
66
  | `skill/trigger-collision` | warning | A normalized trigger phrase (quoted phrase in the description, or a `trigger`/`triggers` field) is declared by more than one distinct skill/agent | `Trigger phrase "{trigger}" is declared by {count} skills/agents — only one will win` |
67
67
  | `skill/orphaned` | warning | A `~/.claude/skills/<name>/` directory contains no `SKILL.md` | `{dir}: skill directory has no SKILL.md — Claude Code has nothing to load` |
68
- | `skill/dead-tool-restriction` | warning | An agent's `tools` / `allowed-tools` frontmatter lists a non-MCP, non-wildcard tool name that is not a known Claude Code built-in tool | `{file}: tool restriction lists "{tool}" which is not a known Claude Code tool` |
68
+ | `skill/dead-tool-restriction` | warning (info when the unknown name is PascalCase) | An agent's `tools` / `allowed-tools` frontmatter lists a non-MCP, non-wildcard tool name that is not a known Claude Code built-in tool | `{file}: tool restriction lists "{tool}" which is not a known Claude Code tool` |
69
69
 
70
70
  **Notes:**
71
71
 
72
72
  - **`skill/broken-ref`** reuses the path-reference detection shape from the context-file pillar. Only explicitly-relative references (`./`, `../`) are verified, resolved against the skill/agent file's own directory; bare `foo/bar` tokens in prose are too ambiguous to resolve without false positives, so they are skipped. References inside example code blocks (`ts`, `py`, `json`, ...) are excluded.
73
73
  - **`skill/trigger-collision`** extracts triggers from quoted phrases inside the `description` (e.g. `"ship 1.3.X"`) and from an optional `trigger`/`triggers` field. Phrases are lowercased and whitespace-collapsed before comparison.
74
- - **`skill/dead-tool-restriction`** validates only against the known built-in tool set. MCP-namespaced tools (`mcp__server__tool`) and wildcard entries are skipped because their validity depends on the loaded MCP servers, which the linter cannot see statically.
74
+ - **`skill/dead-tool-restriction`** validates only against the known built-in tool set. MCP-namespaced tools (`mcp__server__tool`) and wildcard entries are skipped because their validity depends on the loaded MCP servers, which the linter cannot see statically. Severity is split by name shape: an unknown **PascalCase** name is reported as *info* -- it may be a built-in newer than the linter's known-tool list, which drifts across Claude Code versions; anything else (lowercase, separators) doesn't match Claude Code's tool naming and keeps the *warning* (far more likely a typo).
75
75
 
76
76
  All v1 rules are marked **experimental** in the catalog -- the heuristics are conservative and may broaden as more skill/agent shapes are observed. Experimental rules bump patch; promotion to stable bumps minor (see `CHANGELOG.md` versioning policy).
77
77
 
@@ -14,8 +14,8 @@ AI coding agents are guided by context files — markdown documents like `CLAUDE
14
14
  This specification defines a standard set of lint rules for validating AI agent context files across all major AI coding clients. It is tool-agnostic: any linter, IDE extension, CI check, or AI agent can implement these rules.
15
15
 
16
16
  The specification includes:
17
- - A complete reference of context file formats across 17 AI coding clients (21+ file patterns)
18
- - 28 lint rules organized into 11 categories with defined severities
17
+ - A complete reference of context file formats across 16 AI coding clients (21+ file patterns)
18
+ - 39 lint rules organized into 12 categories with defined severities
19
19
  - A machine-readable rule and format catalog ([`context-lint-rules.json`](./context-lint-rules.json))
20
20
  - Auto-fix definitions for rules that support automated correction
21
21
  - Frontmatter schema requirements per client
@@ -42,12 +42,14 @@ The specification includes:
42
42
  - [3.2 commands — build/script validation](#32-commands--buildscript-validation)
43
43
  - [3.3 staleness — freshness detection](#33-staleness--freshness-detection)
44
44
  - [3.4 tokens — context window budget](#34-tokens--context-window-budget)
45
- - [3.5 redundancyinferable content](#35-redundancy--inferable-content)
46
- - [3.6 contradictionscross-file conflicts](#36-contradictions--cross-file-conflicts)
47
- - [3.7 frontmatterclient metadata validation](#37-frontmatter--client-metadata-validation)
48
- - [3.8 ci-coverageCI workflow documentation](#38-ci-coverage--ci-workflow-documentation)
49
- - [3.9 ci-secrets — CI secrets documentation](#39-ci-secrets--ci-secrets-documentation)
45
+ - [3.5 tier-tokenstier-aware token accounting](#35-tier-tokens--tier-aware-token-accounting)
46
+ - [3.6 redundancyinferable content](#36-redundancy--inferable-content)
47
+ - [3.7 contradictionscross-file conflicts](#37-contradictions--cross-file-conflicts)
48
+ - [3.8 frontmatterclient metadata validation](#38-frontmatter--client-metadata-validation)
49
+ - [3.9 ci-coverage — CI workflow documentation](#39-ci-coverage--ci-workflow-documentation)
50
+ - [3.10 ci-secrets — CI secrets documentation](#310-ci-secrets--ci-secrets-documentation)
50
51
  - [3.11 hook-coverage — hook enforcement coverage](#311-hook-coverage--hook-enforcement-coverage)
52
+ - [3.12 content-secrets — inline secret detection](#312-content-secrets--inline-secret-detection)
51
53
  - [4. Rule Catalog (machine-readable)](#4-rule-catalog-machine-readable)
52
54
  - [5. Implementing This Specification](#5-implementing-this-specification)
53
55
  - [6. Contributing](#6-contributing)
@@ -115,7 +117,7 @@ Every major AI coding client reads one or more context file formats. Some client
115
117
  | `.windsurfrules` | Project root | Legacy format. Plain text. |
116
118
  | `.windsurf/rules/*.md` | Rule-based | Markdown rules with frontmatter. |
117
119
 
118
- **Frontmatter fields:** `trigger` (required, one of: `always_on`, `glob`, `manual`, `model`).
120
+ **Frontmatter fields:** `trigger` (required, one of: `always_on`, `glob`, `manual`, `model`, `model_decision`).
119
121
 
120
122
  #### Gemini CLI
121
123
 
@@ -245,7 +247,7 @@ trigger: always_on
245
247
 
246
248
  | Field | Required | Type | Description |
247
249
  |---|---|---|---|
248
- | `trigger` | Yes | enum | When the rule activates. One of: `always_on`, `glob`, `manual`, `model`. |
250
+ | `trigger` | Yes | enum | When the rule activates. One of: `always_on`, `glob`, `manual`, `model`, `model_decision`. |
249
251
 
250
252
  ---
251
253
 
@@ -293,7 +295,9 @@ Context files reference build and test commands (e.g., `npm run build`, `make te
293
295
 
294
296
  Context files consume an agent's context window. Counting tokens helps teams understand and optimize their context budget.
295
297
 
296
- **Recommended approach:** Use the `cl100k_base` tokenizer (GPT-4 family). If unavailable, estimate at ~4 characters per token.
298
+ **Recommended approach:** Use the `cl100k_base` tokenizer (GPT-4 family). If unavailable, estimate with a charset-aware fallback: count CJK codepoints (Han, Hiragana, Katakana, Hangul) at ~1 token each and everything else at ~4 characters per token — a flat characters/4 estimate undercounts CJK content by roughly 4x.
299
+
300
+ **Accuracy:** all counts are soft estimates. `cl100k_base` diverges from Claude's (unpublished) tokenizer by roughly 10-20% on prose and more on code- or whitespace-heavy content, and the character-based fallback is coarser still. Thresholds built on these counts ([Section 3.4](#34-tokens--context-window-budget) / [3.5](#35-tier-tokens--tier-aware-token-accounting)) should be treated as budget guidance with tolerance, not exact accounting.
297
301
 
298
302
  **Track per file:** total token count and total line count.
299
303
 
@@ -301,7 +305,7 @@ Context files consume an agent's context window. Counting tokens helps teams und
301
305
 
302
306
  ## 3. Lint Rules
303
307
 
304
- 28 rules organized into 11 categories.
308
+ 39 rules organized into 12 categories.
305
309
 
306
310
  Severity levels:
307
311
  - **error** — the context file has a verifiably incorrect reference or invalid metadata. Should fail CI.
@@ -335,11 +339,13 @@ Validates that commands referenced in context files are actually available in th
335
339
  | `commands/no-makefile` | error | `make` command used but no Makefile exists | `"{cmd}" — no Makefile found in project` |
336
340
  | `commands/npx-not-in-deps` | warning | `npx` package is not in dependencies or `node_modules/.bin` | `"{cmd}" — "{pkg}" not found in dependencies` |
337
341
  | `commands/tool-not-found` | warning | Common tool (`vitest`, `jest`, `eslint`, etc.) is not in dependencies or `node_modules/.bin` | `"{cmd}" — "{tool}" not found in dependencies or node_modules/.bin` |
342
+ | `commands/package-json-missing` | info | `package.json` is missing or unparseable AND the file references at least one command that would otherwise have been validated | `package.json missing or unparseable — command checks skipped` |
338
343
 
339
344
  **Notes:**
340
345
  - For `script-not-found`, include available scripts in the suggestion when possible.
341
346
  - For `npx-not-in-deps`, suggest adding to `devDependencies`.
342
347
  - Shorthand commands (`npm test`, `pnpm build`) should be validated against scripts as well.
348
+ - When `package.json` can't be loaded, all script/shorthand/npx/tool validation silently skips. Surface that once per file via `package-json-missing` so the skip isn't invisible.
343
349
 
344
350
  ### 3.3 staleness — freshness detection
345
351
 
@@ -384,6 +390,11 @@ Monitors context file size to help teams manage context window consumption.
384
390
  | `tierBreakdown` | 1000 | Triggers `tier-tokens/section-breakdown` on an always-loaded file |
385
391
  | `tierAggregate` | 4000 | Triggers `tier-tokens/aggregate` across always-loaded files |
386
392
 
393
+ **Suggestions:**
394
+ - For `excessive`: `Consider splitting into focused sections or removing redundant content.`
395
+ - For `large`: `Consider trimming — research shows diminishing returns past ~300 lines.`
396
+ - For `aggregate`: `Consider consolidating or trimming to reduce per-session context cost.`
397
+
387
398
  ### 3.5 tier-tokens — tier-aware token accounting
388
399
 
389
400
  Reports token cost attributable to the **always-loaded** tier: files Claude Code (and similar agents) load into every session regardless of request. Complements `tokens` by surfacing which sections / files are costing budget every turn — and which inviolable rules need hook-based enforcement to actually bind.
@@ -392,19 +403,14 @@ Reports token cost attributable to the **always-loaded** tier: files Claude Code
392
403
 
393
404
  | Rule ID | Severity | Trigger | Message |
394
405
  |---|---|---|---|
395
- | `tier-tokens/section-breakdown` | info | Always-loaded file exceeds `tierBreakdown` tokens AND has H1/H2 sections | `{N} tokens loaded every session — heaviest top-level section(s): ...` |
396
- | `tier-tokens/aggregate` | warning | Two or more always-loaded files combined exceed `tierAggregate` tokens | `{count} always-loaded files total {N} tokens — loaded every session` |
397
- | `tier-tokens/hard-enforcement-missing` | info | Line in an always-loaded file uses inviolable framing (NEVER/ALWAYS/DO NOT/MUST NOT) with a backticked command, and no matching PreToolUse hook or `permissions.deny` entry exists in `.claude/settings.json` or `~/.claude/settings.json` | `Inviolable framing ("{line}") without a hook to back it up` |
406
+ | `tier-tokens/section-breakdown` | info | Always-loaded file reaches or exceeds `tierBreakdown` tokens (inclusive — a file at exactly the threshold fires) AND has H1/H2 sections | `{N} tokens loaded every session — heaviest top-level section(s): ...` |
407
+ | `tier-tokens/aggregate` | warning | Two or more always-loaded files total `tierAggregate` tokens or more (inclusive boundary) | `{count} always-loaded files total {N} tokens — loaded every session` |
408
+ | `tier-tokens/hard-enforcement-missing` | info | Line in an always-loaded file uses inviolable framing (NEVER/ALWAYS/DO NOT/MUST NOT) with a backticked command, and no matching PreToolUse or Stop hook, `permissions.deny` entry, or `permissions.ask` entry exists in the project's `.claude/settings.json` / `.claude/settings.local.json` (ask gates the command behind a human prompt, so it counts as enforcement; the user-global `~/.claude/settings.json` is consulted only on explicit opt-in) | `Inviolable framing ("{line}") without a hook to back it up` |
398
409
 
399
410
  **Note on overlap with `tokens`:** `tokens/info` and `tier-tokens/section-breakdown` both fire on a large CLAUDE.md. They're complementary — `tokens` is tier-agnostic ("this file is large"), `tier-tokens` adds the always-loaded attribution and demotion guidance. Use `--ignore tokens` or `--ignore tier-tokens` to pick one.
400
411
 
401
412
  **Source:** [Claude Code memory docs](https://code.claude.com/docs/en/memory). Rule behavior is grounded in the documented loading model ("there's no guarantee of strict compliance" → hard-enforcement-missing; section-demotion-to-skills → structurally reduces per-session cost).
402
413
 
403
- **Suggestions:**
404
- - For `excessive`: `Consider splitting into focused sections or removing redundant content.`
405
- - For `large`: `Consider trimming — research shows diminishing returns past ~300 lines.`
406
- - For `aggregate`: `Consider consolidating or trimming to reduce per-session context cost.`
407
-
408
414
  ### 3.6 redundancy — inferable content
409
415
 
410
416
  Detects content that the agent can already infer from project metadata, reducing unnecessary context window consumption.
@@ -543,9 +549,10 @@ Validates YAML frontmatter required by specific clients. Only applies to file fo
543
549
 
544
550
  | Rule ID | Severity | Trigger | Message |
545
551
  |---|---|---|---|
546
- | `frontmatter/missing` | warning | File format requires frontmatter but none is present | `{format} file is missing frontmatter` |
552
+ | `frontmatter/missing` | warning (Cursor `.mdc`); info (Copilot, Windsurf) | File format requires or recommends frontmatter but none is present | `{format} file is missing frontmatter` |
553
+ | `frontmatter/unclosed` | error | Frontmatter opens with `---` but is never closed (every parsed field is suspect; the host loads the file with no frontmatter at all) | ``Frontmatter opens with `---` but is never closed`` |
547
554
  | `frontmatter/missing-field` | warning | A required or recommended field is absent | `Missing "{field}" field in {format} frontmatter` |
548
- | `frontmatter/invalid-value` | error | A field has an invalid value | `Invalid {field} value: "{value}"` |
555
+ | `frontmatter/invalid-value` | error (invalid `alwaysApply` / Windsurf `trigger`); warning (malformed `globs`) | A field has an invalid value | `Invalid {field} value: "{value}"` |
549
556
  | `frontmatter/no-activation` | info | File has frontmatter but no activation mechanism (no globs/alwaysApply/trigger) | `No activation field — rule may not be applied automatically` |
550
557
 
551
558
  **Validation per format:**
@@ -554,7 +561,11 @@ Validates YAML frontmatter required by specific clients. Only applies to file fo
554
561
  |---|---|---|
555
562
  | Cursor `.mdc` | `description` (required), `alwaysApply` (boolean), `globs` (pattern) | `alwaysApply`: `true` or `false` |
556
563
  | Copilot `instructions/*.md` | `applyTo` (recommended) | Any glob pattern |
557
- | Windsurf `rules/*.md` | `trigger` (required) | `always_on`, `glob`, `manual`, `model` |
564
+ | Windsurf `rules/*.md` | `trigger` (required) | `always_on`, `glob`, `manual`, `model`, `model_decision` |
565
+
566
+ **Notes:**
567
+ - `frontmatter/missing` severity is per-format: warning for Cursor `.mdc` (frontmatter is required there), info for Copilot instructions and Windsurf rules (frontmatter is optional/recommended).
568
+ - The `globs` branch of `frontmatter/invalid-value` only flags unmistakably malformed YAML (unbalanced brackets or quotes), at warning severity — Cursor accepts bare directory names (`globs: src`) and bare extensions, so a value isn't flagged merely for lacking `*` or `/`.
558
569
 
559
570
  ---
560
571
 
@@ -566,6 +577,8 @@ Checks that release/deploy CI workflows are documented in context files. When ag
566
577
  |---|---|---|---|
567
578
  | `ci/no-release-docs` | info | `.github/workflows/` contains release/deploy/publish workflow(s) but no context file mentions the release process | `Release workflow(s) found but no context file documents the release process` |
568
579
 
580
+ > **Rule-ID note:** `ci/no-release-docs` is the published catalog ID — a legacy shared `ci/` prefix that predates the prefix-equals-category convention (see CONTRIBUTING.md "Rule ID format"). The reference implementation emits this finding with ruleId `ci-coverage/no-release-docs` in JSON output.
581
+
569
582
  **Detection algorithm:**
570
583
 
571
584
  1. Check if `.github/workflows/` exists. If not, skip.
@@ -585,6 +598,8 @@ Checks that secrets referenced in CI workflow files are mentioned in context fil
585
598
  |---|---|---|---|
586
599
  | `ci/undocumented-secret` | info | `${{ secrets.NAME }}` found in workflow YAML but `NAME` not mentioned in any context file | `CI secret "{name}" is used in {workflow} but not mentioned in any context file` |
587
600
 
601
+ > **Rule-ID note:** `ci/undocumented-secret` is the published catalog ID — the same legacy shared `ci/` prefix as `ci/no-release-docs`. The reference implementation emits this finding with ruleId `ci-secrets/undocumented-secret` in JSON output.
602
+
588
603
  **Detection algorithm:**
589
604
 
590
605
  1. Check if `.github/workflows/` exists. If not, skip.
@@ -603,13 +618,46 @@ The inverse of `tier-tokens/hard-enforcement-missing`. Where `tier-tokens` flags
603
618
 
604
619
  **Detection algorithm:**
605
620
 
606
- 1. Load settings from project `.claude/settings.json`, project `.claude/settings.local.json`, and user `~/.claude/settings.json` (parsed as JSONC; missing files are skipped).
621
+ 1. Load settings from project `.claude/settings.json` and project `.claude/settings.local.json` (parsed as JSONC; missing files are skipped). The user-global `~/.claude/settings.json` is loaded only on explicit opt-in (`--hooks-global` in the reference implementation) so a default run never reads files outside the project directory.
607
622
  2. For each hook command and each `permissions` list entry, tokenize on whitespace (respecting quotes) and keep tokens that look like script paths (a path separator + a script extension such as `.sh`/`.js`/`.py`/`.ps1`, or an explicit `./` `~/` `/` `$VAR/` `C:\` prefix). Inline tool matchers like `Bash(npm login)` yield no path tokens.
608
623
  3. Resolve each path token: expand a leading `~` and the env vars Claude Code documents for settings paths — `$CLAUDE_PROJECT_DIR`, `$CLAUDE_CONFIG_DIR`, `$HOME`, `$USERPROFILE`. A token that still contains an unresolvable `$VAR` is skipped (it cannot be verified, and a false "dead hook" is worse than a missed one).
609
624
  4. Emit one warning per resolved path that does not exist on disk, with the source file's line number for project files (the user-global file is noted inline).
610
625
 
611
626
  **Stability:** experimental — the path-extraction heuristic is conservative by design (it prefers a missed dead hook over a false positive) and may broaden as more hook-command shapes are observed.
612
627
 
628
+ ### 3.12 content-secrets — inline secret detection
629
+
630
+ Detects secrets pasted directly into context files. Context files usually end up committed to git, so an inline `AKIA...` or `sk-ant-...` in a heading or code block is a leak. This is the context-file counterpart of the MCP-config secret rules (`mcp-security/*` in the [MCP Config Linting Spec](./MCP_CONFIG_LINT_SPEC.md)) — same threat, different paste surface.
631
+
632
+ | Rule ID | Severity | Trigger | Message |
633
+ |---|---|---|---|
634
+ | `content-secrets/private-key-header` | error | Line contains `-----BEGIN [RSA \| EC \| DSA \| OPENSSH \| PGP ]PRIVATE KEY-----` | `Private key header detected in {file}` |
635
+ | `content-secrets/aws-access-key` | error | `AKIA` or `ASIA` (STS) prefix + 16 uppercase alphanumeric chars | `AWS access key detected in {file} ({prefix}...)` |
636
+ | `content-secrets/github-pat` | error | `ghp_`, `github_pat_`, or `ghs_`/`gho_`/`ghu_`/`ghr_` token shapes | `GitHub personal access token detected in {file} ({prefix}...)` |
637
+ | `content-secrets/anthropic-key` | error | `sk-ant-` + 20+ key chars | `Anthropic API key detected in {file} ({prefix}...)` |
638
+ | `content-secrets/openai-key` | error | `sk-` or `sk-proj-` + 20+ key chars | `OpenAI API key detected in {file} ({prefix}...)` |
639
+ | `content-secrets/npm-token` | error | `npm_` + 36+ alphanumeric chars | `npm token detected in {file} ({prefix}...)` |
640
+ | `content-secrets/slack-token` | error | `xox[bpoasr]-` + 10+ token chars | `Slack token detected in {file} ({prefix}...)` |
641
+ | `content-secrets/google-api-key` | error | `AIza` + exactly 35 key chars | `Google API key detected in {file} ({prefix}...)` |
642
+ | `content-secrets/stripe-secret` | error | `sk_live_` + 24+ alphanumeric chars | `Stripe live secret key detected in {file} ({prefix}...)` |
643
+
644
+ **Design principles:**
645
+
646
+ - **Precision over recall.** Patterns are well-defined vendor prefixes only; random high-entropy detection is deliberately omitted — it bites build IDs, commit SHAs, and version strings. A missed exotic format is fine; a noisy false positive that trains users to ignore the check is not.
647
+ - **Never leak the secret in output.** Emitted messages contain at most a 6-character redacted prefix plus an ellipsis — never the full matched value (the value landing in linter stderr/SARIF would itself be a leak vector).
648
+ - **Anthropic before OpenAI.** `sk-ant-` must be checked before the generic `sk-` pattern so the same substring isn't flagged twice (implementations may use per-line dedup keyed by match offset).
649
+
650
+ **Suppression rules (all reduce false positives):**
651
+
652
+ 1. **Placeholder lines** — any line containing a placeholder token (`example`, `placeholder`, `your-key`, `<replace`, `redacted`, `xxxx`, `****`) is skipped entirely (line-scoped on purpose; see the implementation notes in `content-secrets.ts` for the recall trade-off).
653
+ 2. **Placeholder wrappers** — a match wrapped in `${...}` or a hugging `<...>` placeholder is skipped.
654
+ 3. **Commented examples** — a comment line (`#`, `//`, `--`, `<!--`) containing `fake` or `example` is skipped.
655
+ 4. **Illustrative code fences** — content inside fences explicitly tagged `text`, `txt`, `example`, `pseudocode`, or `none` is skipped. Untagged fences are still scanned: a bare ``` fence is the most common way real `.env` contents get pasted into a context file.
656
+
657
+ **Suggestion:** `Move the secret to a .env or secret manager and reference it by name. If this token is real, rotate it immediately.`
658
+
659
+ **Stability:** experimental — the suppression heuristics (placeholder tokens, fence tags) may broaden as more paste shapes are observed; the vendor prefix patterns themselves are stable.
660
+
613
661
  ---
614
662
 
615
663
  ## 4. Rule Catalog (machine-readable)
@@ -645,9 +693,9 @@ For each discovered file:
645
693
 
646
694
  ### Checking
647
695
 
648
- Run per-file checks (paths, commands, staleness, tokens, redundancy, frontmatter) independently per file. These can be parallelized.
696
+ Run per-file checks (paths, commands, staleness, tokens, tier-tokens, redundancy, frontmatter, content-secrets) independently per file. These can be parallelized.
649
697
 
650
- Run cross-file checks (aggregate tokens, duplicate content, contradictions, ci-coverage, ci-secrets) after all per-file parsing is complete. These need the full set of parsed files.
698
+ Run cross-file checks (aggregate tokens, duplicate content, contradictions, ci-coverage, ci-secrets, hook-coverage) after all per-file parsing is complete. These need the full set of parsed files.
651
699
 
652
700
  ### Reporting
653
701