@yawlabs/ctxlint 0.14.0 → 0.15.0
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/.pre-commit-hooks.yaml +10 -10
- package/AGENT_SESSION_LINT_SPEC.md +37 -28
- package/AGENT_SKILL_LINT_SPEC.md +2 -2
- package/CONTEXT_LINT_SPEC.md +73 -25
- package/MCP_CONFIG_LINT_SPEC.md +416 -410
- package/README.md +16 -8
- package/action.yml +11 -1
- package/agent-session-lint-rules.json +3 -3
- package/agent-skill-lint-rules.json +3 -3
- package/context-lint-rules.json +136 -9
- package/dist/index.js +4521 -3837
- package/mcp-config-lint-rules.json +50 -34
- package/package.json +13 -3
- package/schemas/ctxlint-catalog.schema.json +1 -1
package/.pre-commit-hooks.yaml
CHANGED
|
@@ -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.
|
|
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.0 --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
|
-
-
|
|
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
|
|
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.
|
|
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.
|
|
306
|
-
2.
|
|
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.
|
|
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
|
-
|
|
350
|
+
See the JSON file for the full catalog.
|
|
347
351
|
|
|
348
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
package/AGENT_SKILL_LINT_SPEC.md
CHANGED
|
@@ -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
|
|
package/CONTEXT_LINT_SPEC.md
CHANGED
|
@@ -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
|
|
18
|
-
-
|
|
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
|
|
46
|
-
- [3.6
|
|
47
|
-
- [3.7
|
|
48
|
-
- [3.8
|
|
49
|
-
- [3.9 ci-
|
|
45
|
+
- [3.5 tier-tokens — tier-aware token accounting](#35-tier-tokens--tier-aware-token-accounting)
|
|
46
|
+
- [3.6 redundancy — inferable content](#36-redundancy--inferable-content)
|
|
47
|
+
- [3.7 contradictions — cross-file conflicts](#37-contradictions--cross-file-conflicts)
|
|
48
|
+
- [3.8 frontmatter — client 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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|