@chimix/agentscan 1.3.0 → 2.0.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +300 -0
  2. package/README.md +337 -171
  3. package/dist/cli.js +5938 -5500
  4. package/package.json +4 -2
package/README.md CHANGED
@@ -5,19 +5,19 @@
5
5
  </p>
6
6
 
7
7
  <p align="center">
8
- <img src="https://img.shields.io/badge/checks-99-111111?style=flat-square" alt="99 checks">
9
- <img src="https://img.shields.io/badge/tests-457%20passing-111111?style=flat-square" alt="457 tests">
8
+ <img src="https://img.shields.io/badge/checks-143-111111?style=flat-square" alt="143 checks">
9
+ <img src="https://img.shields.io/badge/tests-647%20passing-111111?style=flat-square" alt="647 tests">
10
10
  <img src="https://img.shields.io/badge/network-none-111111?style=flat-square" alt="No network">
11
11
  <img src="https://img.shields.io/badge/writes-none-111111?style=flat-square" alt="Writes nothing">
12
12
  <img src="https://img.shields.io/badge/runs%20on-node%20%C2%B7%20bun-111111?style=flat-square" alt="Node or Bun">
13
13
  <img src="https://img.shields.io/npm/v/@chimix/agentscan?style=flat-square&color=111111&label=npm" alt="npm version">
14
14
  <img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
15
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-1.3.0-111111?style=flat-square" alt="Changelog"></a>
15
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-2.0.0-111111?style=flat-square" alt="Changelog"></a>
16
16
  </p>
17
17
 
18
18
  <p align="center">
19
- <strong>99 checks &middot; offline &middot; 0 network calls on <code>check</code> &middot; provenance on every rule</strong><br>
20
- <sub>An offline linter for Claude Code, Command Code, Grok Build, Windsurf, portable Agent Skills, nested AGENTS.md, Copilot CLI hooks, and the MCP / hooks / rules surfaces that 1.3.0 actually implements. Spec-required checks cite a published line in <a href="docs/spec/">docs/spec/</a>. Heuristics stay at <code>info</code> and say so. The coverage matrix below is the honesty contract — five dimensions, and a documented global location that is not scanned stays unread.</sub>
19
+ <strong>143 checks &middot; offline &middot; 0 network calls on <code>check</code> &middot; provenance on every rule</strong><br>
20
+ <sub>An offline linter for Claude Code, Command Code, Grok Build, Windsurf, Kiro, Cline, Roo, Kilo, Junie, portable Agent Skills, nested AGENTS.md, Copilot CLI hooks (including inline settings), and the MCP / hooks / rules surfaces the registry actually implements. Spec-required checks cite a published line in <a href="docs/spec/">docs/spec/</a>. Heuristics stay at <code>info</code> and say so. The coverage matrix below is the honesty contract — five dimensions, and a documented global location that is not scanned stays unread.</sub>
21
21
  </p>
22
22
 
23
23
  ---
@@ -65,7 +65,7 @@ No AI, no network on `check`. Read the config, read the disk, compare:
65
65
  ```
66
66
  1. Discover .claude/ .commandcode/ .grok/ .agents/ .vscode/ .cursor/ .codex/ .gemini/ .github/ .continue/ AGENTS.md skills-lock.json
67
67
  2. Extract immutable facts — never re-read during checking
68
- 3. Check 99 checks, each labeled spec-required, vendor-recommendation,
68
+ 3. Check 143 checks, each labeled spec-required, vendor-recommendation,
69
69
  security, internal-consistency, or heuristic
70
70
  4. Report text · --json · --output prompt (handoff for a fixing agent)
71
71
  ```
@@ -164,7 +164,7 @@ bunx agentscan check # text report (cwd)
164
164
  bunx agentscan check ./my-app # explicit root
165
165
  bunx agentscan check --json # machine-readable
166
166
  bunx agentscan check --quiet # summary only
167
- bunx agentscan check --verbose # include KEEP, info findings, and each finding's id
167
+ bunx agentscan check --verbose # include info findings and each finding's id
168
168
  bunx agentscan check --fail-on warning
169
169
  bunx agentscan check --global # also scan global skill dirs
170
170
 
@@ -183,10 +183,10 @@ Flags for `check`:
183
183
  | `--copy` | Also copy the report to the system clipboard |
184
184
  | `--no-color` | Never colour, even on a terminal (`NO_COLOR=1` does the same) |
185
185
  | `--quiet` | Summary line only |
186
- | `--verbose` | Show KEEP + info-severity findings, and print each finding's id |
186
+ | `--verbose` | Show info-severity findings, and print each finding's id |
187
187
  | `--fail-on <level>` | `never` (default) · `warning` · `error` |
188
188
  | `--fail-under <0-100>` | Fail when the score drops below this floor |
189
- | `--global` | Also scan `~/.claude/skills`, `~/.codex/skills`, `$CODEX_HOME` / `~/.codex/config.toml`, `~/.commandcode/skills`, `~/.agents/skills`, and `$GROK_HOME` / `~/.grok` (see below) |
189
+ | `--global` | Also scan user skills (`$CLAUDE_CONFIG_DIR/skills` or `~/.claude/skills`, `~/.codex/skills`, `~/.commandcode/skills`, `~/.agents/skills`, `$GROK_HOME/skills`, `~/.codeium/windsurf/skills`), Claude user settings / agents / `CLAUDE.md` / rules / commands and `$CLAUDE_CONFIG_DIR/.claude.json` or `~/.claude.json` (user + matching local `mcpServers`), Copilot `$COPILOT_HOME` / `~/.copilot/hooks` and `settings.json`, Codex `$CODEX_HOME` / `~/.codex/hooks.json` and `config.toml` (MCP plus inline `[hooks]`), Grok `$GROK_HOME` / `~/.grok` (hooks + `config.toml`), Command Code user MCP/agents/memory, Antigravity `~/.gemini/config/hooks.json`, `rules/`, and `skills/`, plus `~/.gemini/AGENTS.md` and `GEMINI.md`, Kiro `~/.kiro/agents`, and Windsurf `~/.codeium/windsurf` MCP + `global_rules.md` + `hooks.json` (see below) |
190
190
  | `--config <path>` | Config file path |
191
191
 
192
192
  **v1 does not write the tree** — no `apply`, no skill delete/install. Findings may *suggest* shell commands; you run them yourself.
@@ -199,7 +199,7 @@ Flags for `check`:
199
199
  │ ⌒ │ 6 error · 2 warning · 11 info hidden (--verbose)
200
200
  └─────┘
201
201
 
202
- agentscan v1.3.0 — touchagency
202
+ agentscan v2.0.0 — touchagency
203
203
 
204
204
  Scanned: 46 deps · 108 skills · 1 mcp · 2 agents · packageManager=bun
205
205
 
@@ -231,7 +231,7 @@ JSON shape (abridged):
231
231
 
232
232
  ```json
233
233
  {
234
- "version": "1.3.0",
234
+ "version": "2.0.0",
235
235
  "root": "/path/to/project",
236
236
  "factsSummary": {
237
237
  "packageManager": "bun",
@@ -259,18 +259,27 @@ Same tree → same sorted findings, with stable unique `id`s.
259
259
 
260
260
  ### `--global`
261
261
 
262
- Adds `~/.claude/skills`, `~/.codex/skills`, `~/.commandcode/skills`,
263
- `~/.agents/skills`, `$CODEX_HOME` / `~/.codex` (`config.toml` MCP plus
264
- `AGENTS.override.md` / `AGENTS.md`), and `$GROK_HOME` / `~/.grok` (skills,
265
- hooks, `config.toml`) to the structural checks: a `SKILL.md` that is malformed
266
- is malformed wherever it lives, and those findings carry a `source: global`
267
- evidence entry so you can tell them apart. Codex and Command Code skills use
268
- the Agent Skills contract (`name` and `description` required). Grok skills do
269
- not — `name` and `description` are optional. `--global` also inventories user
270
- Command Code agents (`~/.commandcode/agents`), MCP (`~/.commandcode/mcp.json`
271
- and user settings `mcp.servers`), and memory (`~/.commandcode/AGENTS.md`). It
262
+ Adds user skill dirs (`$CLAUDE_CONFIG_DIR/skills` or `~/.claude/skills`, `~/.codex/skills`,
263
+ `~/.commandcode/skills`, `~/.agents/skills`, `$GROK_HOME/skills` or
264
+ `~/.grok/skills`), Claude user settings / agents / `CLAUDE.md` / rules /
265
+ commands and `~/.claude.json` (top-level user `mcpServers` plus local
266
+ `projects.<scanned-path>.mcpServers`), Copilot `~/.copilot/hooks`, Codex `$CODEX_HOME` /
267
+ `~/.codex` (`hooks.json`, `config.toml` MCP and inline `[hooks]`, plus `AGENTS.override.md` / `AGENTS.md`),
268
+ Grok `$GROK_HOME` / `~/.grok` (hooks + `config.toml`), Antigravity
269
+ `~/.gemini/config/hooks.json`, `rules/`, and `skills/`, plus
270
+ `~/.gemini/AGENTS.md` and `GEMINI.md`, Kiro `~/.kiro/agents`, Command Code user
271
+ agents / MCP / memory, and Windsurf `~/.codeium/windsurf/mcp_config.json`,
272
+ `memories/global_rules.md`, `hooks.json`, and `skills` to the structural checks: a `SKILL.md` that
273
+ is malformed is malformed wherever it lives, and those findings carry a
274
+ `source: global` evidence entry so you can tell them apart. Codex and
275
+ Command Code skills use the Agent Skills contract (`name` and `description`
276
+ required). Grok skills do not — `name` and `description` are optional. It
272
277
  never opens `auth.json`, `mcp-tokens.json`, Codex `auth.json`, or Grok
273
- `auth.json` / `mcp_credentials.json`.
278
+ `auth.json` / `mcp_credentials.json`. A `~/.claude.json` without user or
279
+ matching local `mcpServers` is skipped, not reported as unreadable.
280
+ `CLAUDE_CONFIG_DIR` relocates the `~/.claude` directory and, when set,
281
+ `~/.claude.json` to `$CLAUDE_CONFIG_DIR/.claude.json`. Unset, that file
282
+ stays in the home directory.
274
283
 
275
284
  Lockfile checks stay project-scoped — a project lockfile cannot pin a skill that
276
285
  lives in your home directory, so reporting one as "not in the lockfile" could
@@ -350,7 +359,7 @@ Optional `.agentscanrc.json` (create with `agentscan init`):
350
359
 
351
360
  ```json
352
361
  {
353
- "skillPaths": [".agents/skills", ".claude/skills", "skills", ".cursor/skills", ".codex/skills", ".commandcode/skills"],
362
+ "skillPaths": [".agents/skills", ".claude/skills", "skills", ".cursor/skills", ".codex/skills", ".commandcode/skills", ".grok/skills", ".windsurf/skills"],
354
363
  "mcpPaths": [".mcp.json", ".claude/mcp.json", "mcp.json", ".vscode/mcp.json", ".cursor/mcp.json", ".agents/mcp_config.json", ".codex/config.toml", ".gemini/settings.json", "opencode.json", "opencode.jsonc", ".opencode/opencode.json", ".opencode/opencode.jsonc", ".continue/config.yaml", ".continue/mcpServers"],
355
364
  "policyFiles": ["AGENTS.md", "CLAUDE.md"],
356
365
  "ignoreSkills": [],
@@ -427,113 +436,181 @@ Every check lives in `src/checks/` and runs on every `check`. `agentscan rules`
427
436
  lists all of them with their ids; `agentscan explain <id>` details any finding.
428
437
 
429
438
  Most validate one discovered item against its own file on disk. Budget and
430
- hygiene checks are **info** (hidden unless `--verbose`). Each registry entry
431
- carries `provenance`: spec-required, vendor-recommendation, security,
432
- internal-consistency, or heuristic. `agentscan rules` lists them all.
433
-
434
- | id | Severity | Catches |
435
- |----|----------|---------|
436
- | `config.unreadable` | error | A config file that is not valid JSON, so whatever it declares is silently not in effect |
437
- | `scan.truncated` | info | A file past the scan cap, so the checks that read its body saw only a prefix — about this tool's reach, not about your project |
438
- | `claude.hook.missing-script` | error | A registered Claude command hook whose script does not exist — it never runs |
439
- | `claude.hook.unknown-event` | error | A Claude hook registered under an event name that is never dispatched |
440
- | `vscode.hook.missing-script` | error | A VS Code command hook whose script does not exist |
441
- | `vscode.hook.unknown-event` | error | A VS Code hook registered under an event name that is never dispatched |
442
- | `claude.agent.missing-frontmatter` | error | A Claude agent definition with no `---` block |
443
- | `claude.agent.missing-description` | error | Claude agent frontmatter has no `description` |
444
- | `claude.agent.missing-name` | error | Claude agent frontmatter has no `name` |
445
- | `claude.agent.invalid-name` | warning | Claude agent name is not lowercase letters and hyphens (error if it starts with `-` or contains `:`; filename is not compared) |
446
- | `claude.agent.duplicate-name` | error | Multiple Claude agent files declare the same name |
447
- | `claude.mcp.no-launch` | error | A Claude MCP server with neither `command` nor `url` |
448
- | `claude.mcp.url-without-type` | error | A Claude remote MCP server with a `url` but no `type` — read as stdio and skipped. On shared `.mcp.json`, a Command Code `transport` field satisfies the other consumer and this check does not fire |
449
- | `claude.mcp.reserved-name` | error | A Claude-consumed MCP server named with a reserved identifier (`workspace`, `claude-in-chrome`, `computer-use`, `Claude Preview`, `Claude Browser`) — skipped at load |
450
- | `vscode.mcp.no-launch` | error | A VS Code MCP server (`servers`) with neither `command` nor `url` |
451
- | `cursor.mcp.no-launch` | error | A Cursor MCP server with neither `command` nor `url` |
452
- | `antigravity.mcp.no-launch` | error | An Antigravity MCP server with neither `command` nor `serverUrl` |
453
- | `codex.mcp.no-launch` | error | A Codex `[mcp_servers.*]` entry with neither `command` nor `url` |
454
- | `gemini.mcp.no-launch` | error | A Gemini MCP server with neither `command`, `url`, nor `httpUrl` |
455
- | `gemini.mcp.underscore-alias` | warning | A Gemini MCP server alias contains `_`, which can make policies fail silently |
456
- | `opencode.mcp.no-launch` | error | An OpenCode MCP server with neither `command` nor `url` |
457
- | `opencode.mcp.missing-type` | error | An OpenCode MCP server missing `type: local` or `type: remote` |
458
- | `opencode.mcp.local-without-command` | error | An OpenCode `type: local` server with no `command` |
459
- | `opencode.mcp.remote-without-url` | error | An OpenCode `type: remote` server with no `url` |
460
- | `opencode.mcp.invalid-launch-for-type` | error | An OpenCode server whose launch field does not match its `type` |
461
- | `opencode.mcp.command-not-array` | error | An OpenCode V2 local `command` that is not an argv array |
462
- | `continue.mcp.no-launch` | error | A Continue MCP server with neither `command`, `url`, nor `uses` |
463
- | `continue.mcp.missing-block-metadata` | error | A standalone `.continue/mcpServers/*.yaml` block missing `name`, `version`, or `schema` (not applied to copied JSON) |
464
- | `commandcode.mcp.no-launch` | error | A Command Code settings / user MCP server with neither `command` nor `url` |
465
- | `commandcode.mcp.invalid-transport` | error | A Command Code MCP `transport` / `type` that is not `http` or `stdio` (Claude `sse` / `ws` on shared `.mcp.json` is skipped) |
466
- | `commandcode.mcp.http-without-url` | error | A Command Code HTTP MCP server with no `url` |
467
- | `commandcode.mcp.stdio-without-command` | error | A Command Code stdio MCP server with no `command` |
468
- | `claude.hook.command-without-command` | error | A Claude `type: command` hook with no `command` |
469
- | `claude.hook.http-without-url` | error | A Claude `type: http` hook with no `url` |
470
- | `claude.hook.mcp-tool-without-server-or-tool` | error | A Claude `type: mcp_tool` hook missing `server` or `tool` |
471
- | `claude.hook.unknown-handler-type` | error | A Claude hook whose required `type` is missing or not one of command, http, mcp_tool, prompt, agent |
472
- | `claude.hook.prompt-without-prompt` | error | A Claude `type: prompt` or `type: agent` hook with no `prompt` |
473
- | `claude.hook.incompatible-handler` | error | A Claude handler type that the event does not support |
474
- | `claude.hook.invalid-group` | error | A Claude hook matcher group with no nested `hooks` array (flat handler arrays are invalid) |
475
- | `vscode.hook.invalid-group` | error | A VS Code hook group that is not a command handler array |
476
- | `vscode.hook.command-without-command` | error | A native VS Code `type: command` hook with no `command` |
477
- | `vscode.hook.unknown-handler-type` | error | A native VS Code hook whose `type` is not `command` |
478
- | `copilot.hook.unknown-event` | error | A Copilot CLI hook registered under an event that is never dispatched |
479
- | `copilot.hook.missing-script` | error | A Copilot CLI command hook whose script does not exist |
480
- | `copilot.hook.command-without-command` | error | A Copilot CLI command hook with none of `bash`, `powershell`, or `command` |
481
- | `copilot.hook.http-without-url` | error | A Copilot CLI `type: http` hook with no `url` |
482
- | `copilot.hook.prompt-without-prompt` | error | A Copilot CLI `type: prompt` hook with no `prompt` |
483
- | `copilot.hook.unknown-handler-type` | error | A Copilot CLI hook whose `type` is not command, http, or prompt |
484
- | `copilot.hook.incompatible-handler` | error | A Copilot CLI prompt hook registered on an event other than `sessionStart` |
485
- | `commandcode.hook.unknown-event` | error | A Command Code hook registered under an event that is never dispatched (four events only) |
486
- | `commandcode.hook.missing-script` | error | A Command Code command hook whose script does not exist |
487
- | `commandcode.hook.invalid-group` | error | A Command Code hook group missing the required nested `hooks` array, or a non-string `matcher` |
488
- | `commandcode.hook.command-without-command` | error | A Command Code `type: command` hook whose `command` is missing, empty, or not a string |
489
- | `commandcode.hook.unknown-handler-type` | error | A Command Code hook whose `type` is missing, not a string, or not `"command"` |
490
- | `commandcode.hook.timeout-out-of-bounds` | error | A Command Code hook `timeout` outside 0–600 seconds |
491
- | `commandcode.agent.reserved-name` | error | A Command Code custom agent named `explore`, `plan`, `review`, or `general` — ignored at load |
492
- | `commandcode.agent.invalid-permission-mode` | error | A Command Code agent `permissionMode` that is not a documented value |
493
- | `commandcode.agent.invalid-field-type` | error | A Command Code agent field whose type the spec does not accept |
494
- | `grok.mcp.no-launch` | error | A Grok `[mcp_servers.*]` entry with neither `command` nor `url` |
495
- | `grok.hook.unknown-event` | error | A Grok hook registered under an event that is never dispatched (14 events) |
496
- | `grok.hook.missing-script` | error | A Grok command hook whose script does not exist |
497
- | `grok.hook.invalid-group` | error | A Grok hook group missing the required nested `hooks` array, or a non-string `matcher` |
498
- | `grok.hook.command-without-command` | error | A Grok `type: command` hook with no `command` |
499
- | `grok.hook.http-without-url` | error | A Grok `type: http` hook with no `url` |
500
- | `grok.hook.unknown-handler-type` | error | A Grok hook whose `type` is missing or not `command` / `http` |
501
- | `grok.skill.missing-frontmatter` | warning | Grok `SKILL.md` with no `---` block |
502
- | `mcp.command-missing` | error | An MCP `command` that is a path-like value whose file does not exist on disk |
503
- | `security.hardcoded-secret` | error | A token-shaped literal in MCP config (the value is never echoed back) |
504
- | `mcp.literal-env` | warning | Secret-named `env` values that are literals instead of interpolation |
505
- | `skill.missing-skill-md` | warning | A directory under a skill path with no `SKILL.md` |
506
- | `claude.skill.missing-frontmatter` | warning | Claude `SKILL.md` with no `---` block |
507
- | `claude.skill.missing-description` | info | Claude frontmatter has no `description` and no first markdown paragraph |
508
- | `agent-skills.skill.missing-frontmatter` | error | Portable skill `SKILL.md` has no YAML frontmatter |
509
- | `agent-skills.skill.missing-name` | error | Portable skill frontmatter has no required `name` |
510
- | `agent-skills.skill.missing-description` | error | Portable skill frontmatter has no required `description` |
511
- | `agent-skills.skill.invalid-name` | error | Portable skill name is not `[a-z0-9]` + hyphens |
512
- | `agent-skills.skill.name-does-not-match-directory` | error | Portable skill `name` does not match its directory |
513
- | `agent-skills.skill.name-too-long` | error | Portable skill name exceeds 64 characters |
514
- | `agent-skills.skill.description-too-long` | error | Portable skill description exceeds 1024 characters |
515
- | `agent-skills.skill.invalid-compatibility` | error | Portable skill `compatibility` is present but is not a 1–500 character string |
516
- | `agent-skills.skill.invalid-metadata` | error | Portable skill `metadata` is present but is not `map<string, string>` |
517
- | `agent-skills.skill.invalid-allowed-tools` | error | Portable skill `allowed-tools` is present but is not a string |
518
- | `agent-skills.skill.body-too-large` | info | Portable skill `SKILL.md` exceeds the 500-line recommendation |
519
- | `skill.broken-reference` | warning | The body points at a bundled file that does not exist |
520
- | `skill.duplicate-description` | info | Two or more skills carry an identical description (heuristic) |
521
- | `skill.locked-not-installed` | warning | `skills-lock.json` pins a skill that is not on disk |
522
- | `skill.not-in-lock` | info | A skill on disk that the lockfile does not track (installer policy) |
523
- | `skill.description-budget` | info | Claude skill listing text (description or first paragraph, plus `when_to_use`) exceeds the 1% context-window budget, with an 8000-character fallback and a 1536-character per-entry cap |
524
- | `skill.no-lockfile` | info | Skills present with no lockfile at all (only with `requireLock`) |
525
- | `budget.agents-md` | info | `AGENTS.md` past a secondary 150-line hint (heuristic; not an AGENTS.md requirement) |
526
- | `budget.claude-md` | info | `CLAUDE.md` past the official “target under 200 lines” recommendation |
527
- | `budget.agents` | info | More agent definitions than a focused set (>8) — heuristic proxy |
528
- | `budget.mcp` | info | More MCP servers than a tool-selection hint (>5) — heuristic proxy |
529
- | `codex.budget.instructions` | info | Codex root→cwd `AGENTS.md` chain exceeds the default 32 KiB |
530
- | `cursor.rule.too-large` | info | A `.cursor/rules/*.mdc` file exceeds the 500-line recommendation |
531
- | `windsurf.rule.too-large` | info | A Windsurf workspace rule exceeds 12,000 characters |
532
- | `windsurf.rule.global-too-large` | info | `~/.codeium/windsurf/memories/global_rules.md` exceeds 6,000 characters |
533
- | `windsurf.rule.missing-trigger` | warning | A `.devin/rules` / `.windsurf/rules` file omits frontmatter `trigger` |
534
- | `windsurf.mcp.no-launch` | error | Windsurf MCP server declares neither `command`, `serverUrl`, nor `url` |
535
-
536
- ### Coverage in 1.3.0
439
+ hygiene checks are **info** (hidden unless `--verbose`).
440
+
441
+ Every rule states where its assertion comes from, and every report says so:
442
+ a `provenance` label (spec-required, vendor-recommendation, security,
443
+ internal-consistency, or heuristic) and either the vendor page it was read
444
+ from, with the date, or an explicit note that no vendor page states it.
445
+
446
+ ```
447
+ ERROR rule:cursor.hook.unknown-event
448
+ "bogusEventName" is not a hook event that gets dispatched
449
+ bogusEventName @ .cursor/hooks.json · sessionStart, sessionEnd, ...
450
+ spec-required · cursor.com/docs/hooks · read 2026-09-03
451
+
452
+ ERROR rule:cursor.hook.missing-script
453
+ Hook points at a script that does not exist
454
+ afterFileEdit @ .cursor/hooks.json · ./hooks/format.sh
455
+ internal-consistency · no vendor page · agentscan inference
456
+ ```
457
+
458
+ `agentscan rules` lists every rule with its severity, provenance and source;
459
+ `agentscan rules --json` adds the full URL and the `docs/spec` capture holding
460
+ the quoted lines; `agentscan explain <id>` prints both for one finding, and
461
+ `--json` carries them on every finding. Every cited page is one
462
+ `bun run spec:check` re-fetches and hash-compares, so a vendor rewriting it
463
+ fails CI instead of quietly turning a quoted rule into a false positive.
464
+
465
+ <!-- rules:start -->
466
+ | id | Severity | Provenance | Source | Catches |
467
+ |----|----------|------------|--------|---------|
468
+ | `config.unreadable` | error | internal-consistency | _parser result_ | Config file is not valid JSON or cannot be read |
469
+ | `scan.truncated` | info | internal-consistency | _scan cap_ | File is larger than the scan cap, so body checks read only a prefix of it |
470
+ | `claude.hook.unknown-event` | error | spec-required | [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks) | Hook registered under an event name that is never dispatched |
471
+ | `claude.hook.missing-script` | error | internal-consistency | _agentscan inference_ | Hook points at a script that does not exist, so it never runs |
472
+ | `vscode.hook.unknown-event` | error | spec-required | [code.visualstudio.com/docs/agents/reference/hooks-reference](https://code.visualstudio.com/docs/agents/reference/hooks-reference) | VS Code hook registered under an event name that is never dispatched |
473
+ | `vscode.hook.missing-script` | error | internal-consistency | _agentscan inference_ | VS Code hook points at a script that does not exist, so it never runs |
474
+ | `claude.hook.invalid-group` | error | internal-consistency | [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks) | Claude hook group has a matcher but no hooks array |
475
+ | `claude.hook.command-without-command` | error | spec-required | [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks) | Claude command hook is missing the command field |
476
+ | `claude.hook.http-without-url` | error | spec-required | [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks) | Claude http hook is missing the url field |
477
+ | `claude.hook.mcp-tool-without-server-or-tool` | error | spec-required | [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks) | Claude MCP tool hook is missing required server and tool fields |
478
+ | `claude.hook.unknown-handler-type` | error | spec-required | [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks) | Claude hook uses a handler type that is not command, http, mcp_tool, prompt, or agent, or omits the required type |
479
+ | `claude.hook.prompt-without-prompt` | error | spec-required | [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks) | Claude prompt or agent hook is missing the required prompt field |
480
+ | `claude.hook.incompatible-handler` | error | spec-required | [code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks) | Claude hook handler type is not valid for its event |
481
+ | `vscode.hook.invalid-group` | error | internal-consistency | [code.visualstudio.com/docs/agents/reference/hooks-reference](https://code.visualstudio.com/docs/agents/reference/hooks-reference) | VS Code hook group has a matcher but no hooks array |
482
+ | `vscode.hook.command-without-command` | error | spec-required | [code.visualstudio.com/docs/agents/reference/hooks-reference](https://code.visualstudio.com/docs/agents/reference/hooks-reference) | VS Code command hook is missing the command field |
483
+ | `vscode.hook.unknown-handler-type` | error | spec-required | [code.visualstudio.com/docs/agents/reference/hooks-reference](https://code.visualstudio.com/docs/agents/reference/hooks-reference) | VS Code hook uses a handler type other than the required type command |
484
+ | `copilot.hook.unknown-event` | error | spec-required | [docs.github.com/en/copilot/reference/hooks-reference](https://docs.github.com/en/copilot/reference/hooks-reference) | Copilot CLI hook registered under an event name that is never dispatched |
485
+ | `copilot.hook.missing-script` | error | internal-consistency | _agentscan inference_ | Copilot CLI hook points at a script that does not exist, so it never runs |
486
+ | `copilot.hook.command-without-command` | error | spec-required | [docs.github.com/en/copilot/reference/hooks-reference](https://docs.github.com/en/copilot/reference/hooks-reference) | Copilot CLI command hook declares none of bash, powershell, command, or exec |
487
+ | `copilot.hook.http-without-url` | error | spec-required | [docs.github.com/en/copilot/reference/hooks-reference](https://docs.github.com/en/copilot/reference/hooks-reference) | Copilot CLI http hook is missing the url field |
488
+ | `copilot.hook.prompt-without-prompt` | error | spec-required | [docs.github.com/en/copilot/reference/hooks-reference](https://docs.github.com/en/copilot/reference/hooks-reference) | Copilot CLI prompt hook is missing the prompt field |
489
+ | `copilot.hook.unknown-handler-type` | error | spec-required | [docs.github.com/en/copilot/reference/hooks-reference](https://docs.github.com/en/copilot/reference/hooks-reference) | Copilot CLI hook uses a handler type that is not command, http, or prompt |
490
+ | `copilot.hook.incompatible-handler` | error | spec-required | [docs.github.com/en/copilot/reference/hooks-reference](https://docs.github.com/en/copilot/reference/hooks-reference) | Copilot CLI prompt hook is registered on an event other than sessionStart |
491
+ | `skill.missing-skill-md` | warning | internal-consistency | [agentskills.io/specification](https://agentskills.io/specification) | Skill directory has no SKILL.md |
492
+ | `claude.skill.missing-frontmatter` | warning | spec-required | [code.claude.com/docs/en/skills](https://code.claude.com/docs/en/skills) | SKILL.md has no YAML frontmatter block |
493
+ | `claude.skill.missing-description` | info | vendor-recommendation | [code.claude.com/docs/en/skills](https://code.claude.com/docs/en/skills) | SKILL.md frontmatter has no description and no first markdown paragraph |
494
+ | `agent-skills.skill.missing-frontmatter` | error | spec-required | [agentskills.io/specification](https://agentskills.io/specification) | Portable skill SKILL.md has no YAML frontmatter block |
495
+ | `agent-skills.skill.missing-name` | error | spec-required | [agentskills.io/specification](https://agentskills.io/specification) | Portable skill frontmatter has no required name |
496
+ | `agent-skills.skill.missing-description` | error | spec-required | [agentskills.io/specification](https://agentskills.io/specification) | Portable skill frontmatter has no required description |
497
+ | `agent-skills.skill.invalid-name` | error | spec-required | [agentskills.io/specification](https://agentskills.io/specification) | Portable skill name is not a valid identifier |
498
+ | `agent-skills.skill.name-does-not-match-directory` | error | spec-required | [agentskills.io/specification](https://agentskills.io/specification) | Portable skill name does not match its directory |
499
+ | `agent-skills.skill.name-too-long` | error | spec-required | [agentskills.io/specification](https://agentskills.io/specification) | Portable skill name exceeds 64 characters |
500
+ | `agent-skills.skill.description-too-long` | error | spec-required | [agentskills.io/specification](https://agentskills.io/specification) | Portable skill description exceeds 1024 characters |
501
+ | `agent-skills.skill.invalid-compatibility` | error | spec-required | [agentskills.io/specification](https://agentskills.io/specification) | Portable skill compatibility is not a string of 1–500 characters |
502
+ | `agent-skills.skill.invalid-metadata` | error | spec-required | [agentskills.io/specification](https://agentskills.io/specification) | Portable skill metadata is not a map of string to string |
503
+ | `agent-skills.skill.invalid-allowed-tools` | error | spec-required | [agentskills.io/specification](https://agentskills.io/specification) | Portable skill allowed-tools is not a string |
504
+ | `agent-skills.skill.body-too-large` | info | vendor-recommendation | [agentskills.io/specification](https://agentskills.io/specification) | Portable skill SKILL.md exceeds the 500-line recommendation |
505
+ | `skill.not-in-lock` | info | heuristic | _lockfile hygiene, not a requirement_ | Skill on disk is not tracked by skills-lock.json |
506
+ | `skill.locked-not-installed` | warning | internal-consistency | _agentscan inference_ | skills-lock.json pins a skill that is not installed |
507
+ | `skill.broken-reference` | warning | internal-consistency | [agentskills.io/specification](https://agentskills.io/specification) | SKILL.md points at a bundled file that does not exist |
508
+ | `skill.duplicate-description` | info | heuristic | _selection hint, not a requirement_ | Two or more skills share an identical description |
509
+ | `skill.description-budget` | info | vendor-recommendation | [code.claude.com/docs/en/skills](https://code.claude.com/docs/en/skills) | Claude skill names + descriptions exceed the listing character budget |
510
+ | `skill.no-lockfile` | info | heuristic | _lockfile hygiene, not a requirement_ | Skills present with no skills-lock.json (requireLock only) |
511
+ | `claude.agent.missing-frontmatter` | error | spec-required | [code.claude.com/docs/en/sub-agents](https://code.claude.com/docs/en/sub-agents) | Agent definition has no YAML frontmatter block |
512
+ | `claude.agent.missing-description` | error | spec-required | [code.claude.com/docs/en/sub-agents](https://code.claude.com/docs/en/sub-agents) | Agent frontmatter has no description |
513
+ | `claude.agent.missing-name` | error | spec-required | [code.claude.com/docs/en/sub-agents](https://code.claude.com/docs/en/sub-agents) | Agent frontmatter has no name |
514
+ | `claude.agent.duplicate-name` | error | spec-required | [code.claude.com/docs/en/sub-agents](https://code.claude.com/docs/en/sub-agents) | Agent names are duplicated |
515
+ | `claude.agent.invalid-name` | warning/error | spec-required | [code.claude.com/docs/en/sub-agents](https://code.claude.com/docs/en/sub-agents) | Agent frontmatter name is not a valid identifier |
516
+ | `claude.mcp.no-launch` | error | spec-required | [code.claude.com/docs/en/mcp](https://code.claude.com/docs/en/mcp) | MCP server declares neither command nor url |
517
+ | `vscode.mcp.no-launch` | error | spec-required | [code.visualstudio.com/docs/agents/reference/mcp-configuration](https://code.visualstudio.com/docs/agents/reference/mcp-configuration) | VS Code MCP server declares neither command nor url |
518
+ | `cursor.mcp.no-launch` | error | spec-required | [cursor.com/docs/context/mcp](https://cursor.com/docs/context/mcp) | Cursor MCP server declares neither command nor url |
519
+ | `antigravity.mcp.no-launch` | error | spec-required | [antigravity.google/docs/mcp](https://antigravity.google/docs/mcp) | Antigravity MCP server declares neither command, serverUrl, nor url |
520
+ | `antigravity.hook.unknown-event` | error | spec-required | [antigravity.google/docs/hooks](https://antigravity.google/docs/hooks) | Antigravity hook registered under an event name that is never dispatched |
521
+ | `antigravity.hook.missing-script` | error | internal-consistency | _agentscan inference_ | Antigravity hook points at a script that does not exist, so it never runs |
522
+ | `antigravity.hook.invalid-group` | error | spec-required | [antigravity.google/docs/hooks](https://antigravity.google/docs/hooks) | Antigravity hook group is missing a hooks array, or matcher is not a string |
523
+ | `antigravity.hook.command-without-command` | error | spec-required | [antigravity.google/docs/hooks](https://antigravity.google/docs/hooks) | Antigravity command hook declares no command |
524
+ | `antigravity.hook.unknown-handler-type` | error | spec-required | [antigravity.google/docs/hooks](https://antigravity.google/docs/hooks) | Antigravity hook handler type is not command |
525
+ | `codex.mcp.no-launch` | error | spec-required | [learn.chatgpt.com/docs/extend/mcp](https://learn.chatgpt.com/docs/extend/mcp) | Codex MCP server declares neither command nor url |
526
+ | `gemini.mcp.no-launch` | error | spec-required | [github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md) | Gemini MCP server declares neither command, url, nor httpUrl |
527
+ | `gemini.mcp.underscore-alias` | warning | vendor-recommendation | [github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md) | Gemini MCP server alias contains an underscore, which can make policies fail silently |
528
+ | `opencode.mcp.no-launch` | error | spec-required | [opencode.ai/v2/docs/mcp-servers](https://opencode.ai/v2/docs/mcp-servers) | OpenCode MCP server declares neither command nor url |
529
+ | `opencode.mcp.missing-type` | error | spec-required | [opencode.ai/v2/docs/mcp-servers](https://opencode.ai/v2/docs/mcp-servers) | OpenCode MCP server is missing type local or remote |
530
+ | `opencode.mcp.local-without-command` | error | spec-required | [opencode.ai/v2/docs/mcp-servers](https://opencode.ai/v2/docs/mcp-servers) | OpenCode local MCP server declares no command |
531
+ | `opencode.mcp.remote-without-url` | error | spec-required | [opencode.ai/v2/docs/mcp-servers](https://opencode.ai/v2/docs/mcp-servers) | OpenCode remote MCP server declares no url |
532
+ | `opencode.mcp.invalid-launch-for-type` | error | spec-required | [opencode.ai/v2/docs/mcp-servers](https://opencode.ai/v2/docs/mcp-servers) | OpenCode MCP server launch field does not match its type |
533
+ | `opencode.mcp.command-not-array` | error | spec-required | [opencode.ai/v2/docs/mcp-servers](https://opencode.ai/v2/docs/mcp-servers) | OpenCode V2 local MCP command is not an argv array |
534
+ | `continue.mcp.no-launch` | error | spec-required | [docs.continue.dev/customize/deep-dives/mcp](https://docs.continue.dev/customize/deep-dives/mcp) | Continue MCP server declares neither command, url, nor uses |
535
+ | `continue.mcp.missing-block-metadata` | error | spec-required | [docs.continue.dev/customize/deep-dives/mcp](https://docs.continue.dev/customize/deep-dives/mcp) | Standalone Continue YAML MCP block is missing name, version, or schema |
536
+ | `commandcode.mcp.no-launch` | error | spec-required | [commandcode.ai/docs/mcp](https://commandcode.ai/docs/mcp) | Command Code MCP server declares neither command nor url |
537
+ | `commandcode.mcp.invalid-transport` | error | spec-required | [commandcode.ai/docs/mcp](https://commandcode.ai/docs/mcp) | Command Code MCP transport is not http or stdio |
538
+ | `commandcode.mcp.http-without-url` | error | spec-required | [commandcode.ai/docs/mcp](https://commandcode.ai/docs/mcp) | Command Code HTTP MCP server declares no url |
539
+ | `commandcode.mcp.stdio-without-command` | error | spec-required | [commandcode.ai/docs/mcp](https://commandcode.ai/docs/mcp) | Command Code stdio MCP server declares no command |
540
+ | `commandcode.hook.unknown-event` | error | spec-required | [commandcode.ai/docs/hooks](https://commandcode.ai/docs/hooks) | Command Code hook registered under an event that is never dispatched |
541
+ | `commandcode.hook.missing-script` | error | internal-consistency | _agentscan inference_ | Command Code hook points at a script that does not exist, so it never runs |
542
+ | `commandcode.hook.invalid-group` | error | spec-required | [commandcode.ai/docs/hooks](https://commandcode.ai/docs/hooks) | Command Code hook group is missing the required nested hooks array, or matcher is not a string |
543
+ | `commandcode.hook.command-without-command` | error | spec-required | [commandcode.ai/docs/hooks](https://commandcode.ai/docs/hooks) | Command Code command hook is missing the command field |
544
+ | `commandcode.hook.unknown-handler-type` | error | spec-required | [commandcode.ai/docs/hooks](https://commandcode.ai/docs/hooks) | Command Code hook uses a handler type other than the required string "command" |
545
+ | `commandcode.hook.timeout-out-of-bounds` | error | spec-required | [commandcode.ai/docs/hooks](https://commandcode.ai/docs/hooks) | Command Code hook timeout is outside 0–600 seconds |
546
+ | `commandcode.agent.reserved-name` | error | spec-required | [commandcode.ai/docs/agents](https://commandcode.ai/docs/agents) | Command Code custom agent uses a reserved name and is ignored |
547
+ | `commandcode.agent.invalid-permission-mode` | error | spec-required | [commandcode.ai/docs/agents](https://commandcode.ai/docs/agents) | Command Code agent permissionMode is not a documented value |
548
+ | `commandcode.agent.invalid-field-type` | error | spec-required | [commandcode.ai/docs/agents](https://commandcode.ai/docs/agents) | Command Code agent field has a type the spec does not accept |
549
+ | `grok.mcp.no-launch` | error | spec-required | [docs.x.ai/build/features/mcp-servers](https://docs.x.ai/build/features/mcp-servers) | Grok MCP server declares neither command nor url |
550
+ | `grok.hook.unknown-event` | error | spec-required | [docs.x.ai/build/features/hooks](https://docs.x.ai/build/features/hooks) | Grok hook registered under an event that is never dispatched |
551
+ | `grok.hook.missing-script` | error | internal-consistency | _agentscan inference_ | Grok hook points at a script that does not exist, so it never runs |
552
+ | `grok.hook.invalid-group` | error | spec-required | [docs.x.ai/build/features/hooks](https://docs.x.ai/build/features/hooks) | Grok hook group is missing the required nested hooks array, or matcher is not a string |
553
+ | `grok.hook.command-without-command` | error | spec-required | [docs.x.ai/build/features/hooks](https://docs.x.ai/build/features/hooks) | Grok command hook is missing the command field |
554
+ | `grok.hook.http-without-url` | error | spec-required | [docs.x.ai/build/features/hooks](https://docs.x.ai/build/features/hooks) | Grok http hook is missing the url field |
555
+ | `grok.hook.unknown-handler-type` | error | spec-required | [docs.x.ai/build/features/hooks](https://docs.x.ai/build/features/hooks) | Grok hook uses a handler type other than command or http, or omits the required type |
556
+ | `grok.skill.missing-frontmatter` | error | spec-required | [docs.x.ai/build/features/skills-plugins-marketplaces](https://docs.x.ai/build/features/skills-plugins-marketplaces) | Grok SKILL.md has no YAML frontmatter block |
557
+ | `mcp.command-missing` | error | internal-consistency | _cross-provider path assertion; docs/spec/mcp.md calls it internal-consistency_ | MCP server command is a path that does not exist on disk |
558
+ | `claude.mcp.url-without-type` | error | spec-required | [code.claude.com/docs/en/mcp](https://code.claude.com/docs/en/mcp) | Remote MCP server has a url but no transport type |
559
+ | `claude.mcp.reserved-name` | error | spec-required | [code.claude.com/docs/en/mcp](https://code.claude.com/docs/en/mcp) | Claude MCP server uses a reserved name that Claude skips at load |
560
+ | `security.hardcoded-secret` | error | security | _cross-provider token-shape judgement, not a quoted rule_ | MCP config contains a token-shaped literal |
561
+ | `mcp.literal-env` | warning | security | [code.claude.com/docs/en/mcp](https://code.claude.com/docs/en/mcp) | MCP env value is a literal secret-shaped key instead of interpolation |
562
+ | `mcp.literal-credential` | warning | security | _cross-provider header judgement; no single vendor page states it_ | MCP headers, http_headers, or auth value is a literal secret instead of interpolation |
563
+ | `budget.agents-md` | info | heuristic | _size hint, not a requirement_ | AGENTS.md longer than the heuristic line budget (>150 lines) |
564
+ | `budget.claude-md` | info | vendor-recommendation | [code.claude.com/docs/en/memory](https://code.claude.com/docs/en/memory) | CLAUDE.md longer than the documented size target (~200 lines) |
565
+ | `budget.agents` | info | heuristic | _size hint, not a requirement_ | More agent definitions than a focused set (>8 files in .claude/agents) |
566
+ | `budget.mcp` | info | heuristic | _size hint, not a requirement_ | Project MCP server count above the point where tool selection may degrade |
567
+ | `codex.budget.instructions` | info | vendor-recommendation | [learn.chatgpt.com/docs/agent-configuration/agents-md](https://learn.chatgpt.com/docs/agent-configuration/agents-md) | Codex AGENTS.md chain exceeds project_doc_max_bytes (32 KiB default) |
568
+ | `cursor.rule.too-large` | info | vendor-recommendation | [cursor.com/docs/rules](https://cursor.com/docs/rules) | Cursor project rule exceeds the 500-line recommendation |
569
+ | `windsurf.rule.too-large` | info | vendor-recommendation | [docs.windsurf.com/windsurf/cascade/memories](https://docs.windsurf.com/windsurf/cascade/memories) | Windsurf workspace rule exceeds the 12,000-character limit |
570
+ | `windsurf.rule.global-too-large` | info | vendor-recommendation | [docs.windsurf.com/windsurf/cascade/memories](https://docs.windsurf.com/windsurf/cascade/memories) | Windsurf global_rules.md exceeds the 6,000-character limit |
571
+ | `windsurf.rule.missing-trigger` | warning | spec-required | [docs.windsurf.com/windsurf/cascade/memories](https://docs.windsurf.com/windsurf/cascade/memories) | Windsurf workspace rule is missing frontmatter trigger |
572
+ | `antigravity.rule.missing-trigger` | warning | spec-required | [antigravity.google/docs/rules](https://antigravity.google/docs/rules) | Antigravity modular rule is missing a frontmatter trigger |
573
+ | `antigravity.rule.invalid-trigger` | warning | spec-required | [antigravity.google/docs/rules](https://antigravity.google/docs/rules) | Antigravity modular rule trigger is not always_on, model_decision, glob, or manual |
574
+ | `antigravity.rule.missing-description` | warning | spec-required | [antigravity.google/docs/rules](https://antigravity.google/docs/rules) | Antigravity model_decision rule is missing a description |
575
+ | `antigravity.rule.missing-globs` | warning | spec-required | [antigravity.google/docs/rules](https://antigravity.google/docs/rules) | Antigravity glob rule is missing globs |
576
+ | `antigravity.rule.too-large` | warning | spec-required | [antigravity.google/docs/rules](https://antigravity.google/docs/rules) | Antigravity rule exceeds 24,000 bytes |
577
+ | `windsurf.mcp.no-launch` | error | spec-required | [docs.windsurf.com/windsurf/cascade/mcp](https://docs.windsurf.com/windsurf/cascade/mcp) | Windsurf MCP server declares neither command, serverUrl, nor url |
578
+ | `devin.mcp.no-launch` | error | spec-required | [docs.devin.ai/cli/extensibility/mcp/configuration](https://docs.devin.ai/cli/extensibility/mcp/configuration) | Devin MCP server declares neither command nor url |
579
+ | `windsurf.hook.unknown-event` | error | spec-required | [docs.devin.ai/desktop/cascade/hooks](https://docs.devin.ai/desktop/cascade/hooks) | Windsurf hook registered under an event name that is never dispatched |
580
+ | `windsurf.hook.missing-script` | error | internal-consistency | _agentscan inference_ | Windsurf hook points at a script that does not exist, so it never runs |
581
+ | `windsurf.hook.command-without-command` | error | spec-required | [docs.devin.ai/desktop/cascade/hooks](https://docs.devin.ai/desktop/cascade/hooks) | Windsurf hook declares neither command nor powershell |
582
+ | `devin.hook.unknown-event` | error | spec-required | [docs.devin.ai/cli/extensibility/hooks/overview](https://docs.devin.ai/cli/extensibility/hooks/overview) | Devin hook registered under an event name that is never dispatched |
583
+ | `devin.hook.missing-script` | error | internal-consistency | _agentscan inference_ | Devin hook points at a script that does not exist, so it never runs |
584
+ | `devin.hook.invalid-group` | error | spec-required | [docs.devin.ai/cli/extensibility/hooks/overview](https://docs.devin.ai/cli/extensibility/hooks/overview) | Devin hook group is missing a hooks array |
585
+ | `devin.hook.command-without-command` | error | spec-required | [docs.devin.ai/cli/extensibility/hooks/overview](https://docs.devin.ai/cli/extensibility/hooks/overview) | Devin command hook declares no command |
586
+ | `devin.hook.prompt-without-prompt` | error | spec-required | [docs.devin.ai/cli/extensibility/hooks/overview](https://docs.devin.ai/cli/extensibility/hooks/overview) | Devin prompt hook declares no prompt |
587
+ | `devin.hook.unknown-handler-type` | error | spec-required | [docs.devin.ai/cli/extensibility/hooks/overview](https://docs.devin.ai/cli/extensibility/hooks/overview) | Devin hook handler type is not command or prompt |
588
+ | `codex.hook.unknown-event` | error | spec-required | [learn.chatgpt.com/docs/hooks](https://learn.chatgpt.com/docs/hooks) | Codex hook registered under an event name that is never dispatched |
589
+ | `codex.hook.missing-script` | error | internal-consistency | _agentscan inference_ | Codex hook points at a script that does not exist, so it never runs |
590
+ | `codex.hook.invalid-group` | error | spec-required | [learn.chatgpt.com/docs/hooks](https://learn.chatgpt.com/docs/hooks) | Codex hook group is missing a hooks array, or matcher is not a string |
591
+ | `codex.hook.command-without-command` | error | spec-required | [learn.chatgpt.com/docs/hooks](https://learn.chatgpt.com/docs/hooks) | Codex command hook declares no command |
592
+ | `codex.hook.mcp-tool-without-server-or-tool` | error | spec-required | [learn.chatgpt.com/docs/hooks](https://learn.chatgpt.com/docs/hooks) | Codex MCP tool hook declares no server or tool |
593
+ | `codex.hook.unknown-handler-type` | error | spec-required | [learn.chatgpt.com/docs/hooks](https://learn.chatgpt.com/docs/hooks) | Codex hook handler type is not command, mcp_tool, prompt, or agent |
594
+ | `codex.hook.incompatible-handler` | error | spec-required | [learn.chatgpt.com/docs/hooks](https://learn.chatgpt.com/docs/hooks) | Codex prompt or agent handler is parsed but skipped, or SessionEnd uses mcp_tool |
595
+ | `codex.hook.timeout-out-of-bounds` | error | spec-required | [learn.chatgpt.com/docs/hooks](https://learn.chatgpt.com/docs/hooks) | Codex SessionEnd or Interrupt hook timeout is outside 1–3 seconds |
596
+ | `gemini.hook.unknown-event` | error | spec-required | [geminicli.com/docs/hooks](https://geminicli.com/docs/hooks/) | Gemini hook registered under an event that is never dispatched |
597
+ | `gemini.hook.missing-script` | error | internal-consistency | _agentscan inference_ | Gemini hook points at a script that does not exist, so it never runs |
598
+ | `gemini.hook.invalid-group` | error | spec-required | [geminicli.com/docs/hooks](https://geminicli.com/docs/hooks/) | Gemini hook group is missing the required nested hooks array, or matcher is not a string |
599
+ | `gemini.hook.command-without-command` | error | spec-required | [geminicli.com/docs/hooks](https://geminicli.com/docs/hooks/) | Gemini command hook is missing the command field |
600
+ | `gemini.hook.unknown-handler-type` | error | spec-required | [geminicli.com/docs/hooks](https://geminicli.com/docs/hooks/) | Gemini hook omits the required type or uses a value other than command |
601
+ | `cursor.hook.unknown-event` | error | spec-required | [cursor.com/docs/hooks](https://cursor.com/docs/hooks) | Cursor hook registered under an event that is never dispatched |
602
+ | `cursor.hook.missing-script` | error | internal-consistency | _agentscan inference_ | Cursor hook points at a script that does not exist, so it never runs |
603
+ | `cursor.hook.command-without-command` | error | spec-required | [cursor.com/docs/hooks](https://cursor.com/docs/hooks) | Cursor hook is missing the required command field |
604
+ | `cursor.hook.unknown-handler-type` | error | spec-required | [cursor.com/docs/hooks](https://cursor.com/docs/hooks) | Cursor hook declares a type other than command or prompt |
605
+ | `kiro.hook.unknown-event` | error | spec-required | [kiro.dev/docs/ide/whats-new-v1/hooks](https://kiro.dev/docs/ide/whats-new-v1/hooks/) | Kiro hook registered under a trigger that is never dispatched |
606
+ | `kiro.hook.missing-script` | error | internal-consistency | _agentscan inference_ | Kiro command hook points at a script that does not exist, so it never runs |
607
+ | `kiro.hook.command-without-command` | error | spec-required | [kiro.dev/docs/hooks](https://kiro.dev/docs/hooks/) | Kiro command hook is missing the command field |
608
+ | `kiro.hook.prompt-without-prompt` | error | spec-required | [kiro.dev/docs/hooks](https://kiro.dev/docs/hooks/) | Kiro agent hook is missing the required prompt field |
609
+ | `kiro.hook.unknown-handler-type` | error | spec-required | [kiro.dev/docs/hooks](https://kiro.dev/docs/hooks/) | Kiro hook action type is missing or is not command or agent |
610
+ | `kiro.hook.invalid-group` | error | spec-required | [kiro.dev/docs/hooks](https://kiro.dev/docs/hooks/) | Kiro hook file is missing version v1 or a hooks array |
611
+ <!-- rules:end -->
612
+
613
+ ### Coverage in 2.0.0
537
614
 
538
615
  Each cell is one coverage dimension. There is no `full` / `partial` label: a
539
616
  documented global or static location that is not opened stays **unread**.
@@ -548,29 +625,28 @@ Dimensions:
548
625
 
549
626
  | Ecosystem | project discovery | global discovery | schema | precedence | conformance |
550
627
  |-----------|-------------------|------------------|--------|------------|-------------|
551
- | Agent Skills | `.agents/skills` (and Cursor / Codex / Command Code skill trees) | `--global` `~/.agents/skills`, `~/.codex/skills` | required `name` / `description` plus optional `compatibility` / `metadata` / `allowed-tools`; 500-line info | n/a | `agent-skills` fixture |
628
+ | Agent Skills | `.agents/skills` (and Cursor / Codex / Command Code skill trees). Default `skills/` uses the Claude schema, not this contract | `--global` `~/.agents/skills`, `~/.codex/skills` | required `name` / `description` plus optional `compatibility` / `metadata` / `allowed-tools`; 500-line info | n/a | `agent-skills` fixture |
552
629
  | AGENTS.md | nested walk-up | n/a | no required fields | nearest-wins | tests |
553
- | Claude Code | project settings, skills, agents, `CLAUDE.md`, `.mcp.json` / `.claude/mcp.json` | unread `~/.claude/settings.json`, managed policy, marketplace plugins | 33 events; required handler `type`; MCP reserved names; first-paragraph skill description; listing budget | walk-up `CLAUDE.md` / `.claude/agents` | `claude-json` fixture |
630
+ | Claude Code | project settings, skills, agents, commands, `CLAUDE.md`, `.mcp.json` / `.claude/mcp.json`; in-tree plugin hooks / skills / agents / commands / `.mcp.json` | `--global` `$CLAUDE_CONFIG_DIR` or `~/.claude` (skills, settings, agents, `CLAUDE.md`, rules, commands) and `$CLAUDE_CONFIG_DIR/.claude.json` or `~/.claude.json` (user + matching local `mcpServers`); unread managed policy, marketplace plugins, other projects' local MCP | 33 events; required handler `type`; MCP reserved names; first-paragraph skill description; listing budget | walk-up `CLAUDE.md` / `.claude/agents` / `.claude/commands`; user+project+local layers inventoried | `claude-json` fixture |
554
631
  | Command Code | git-root project files; per-directory `AGENTS.md` else `.commandcode/AGENTS.md` | `--global` `~/.commandcode/*`; unread `projects/{slug}/mcp.json` | 4 events; command handlers; Agent Skills; MCP `transport` / `type` | settings merge; project+user hooks coexist; skill/MCP shadow | `commandcode` fixture |
555
- | Codex | `.codex/config.toml`, `.codex/skills`, AGENTS chain | `--global` `$CODEX_HOME` / `~/.codex/config.toml` MCP; `AGENTS.override.md` then `AGENTS.md`; unread system, managed, requirements, profiles, plugins, trust | TOML MCP; `project_doc_max_bytes`; Agent Skills | override > `AGENTS.md` > fallbacks; one file per dir; root→cwd; `project_root_markers`; MCP user+project both inventoried | `codex-toml` fixture |
556
- | VS Code | `.github/hooks` without `version: 1`, instruction files, `.github/agents`, `.vscode/mcp.json` | `--global` `~/.copilot/hooks` without `version: 1`; unread policy dirs | 8 events; command-only | workspace over user | `vscode-hooks`, `vscode-json` |
557
- | Copilot CLI | `.github/hooks` with `version: 1` | `--global` `~/.copilot/hooks` with `version: 1`; unread `/etc/github-copilot/policy.d` | camelCase + PascalCase map; `bash` / `powershell` / `command`; `cwd`; `timeoutSec`; prompt on `sessionStart` | documented sources coexist; policy unread | `copilot-hooks` fixture |
558
- | Cursor | nested `.cursor/skills`, `.cursor/mcp.json`, `.cursor/rules` | unread documented Cursor user/global paths | Agent Skills; MCP launch; 500-line rules | n/a | `cursor-json` fixture |
632
+ | Codex | `.codex/hooks.json` and inline `[hooks]` in `.codex/config.toml` on the ancestor walk; `.codex/skills`; AGENTS chain | `--global` `$CODEX_HOME` / `~/.codex` `hooks.json` and `config.toml` (MCP and `[hooks]`); `AGENTS.override.md` then `AGENTS.md`; unread system, managed, requirements, profiles, plugins, trust | 14 hook events; `command` / `mcp_tool`; `prompt` and `agent` parsed but skipped; SessionEnd rejects `mcp_tool`; SessionEnd and Interrupt timeout 1–3; TOML MCP; `project_doc_max_bytes`; Agent Skills | both hook files in a layer are inventoried; trust is not modeled; override > `AGENTS.md` > fallbacks; one file per dir; root→cwd; `project_root_markers`; MCP user+project both inventoried | `codex-hooks`, `codex-toml` fixtures |
633
+ | VS Code | `.github/hooks` without `version: 1` (legacy Local), instruction files, `.github/agents` (frontmatter hooks as vscode-native), `.vscode/mcp.json` `servers` | `--global` `~/.copilot/hooks` without `version: 1`; unread policy dirs; unread marketplace plugin `hooks.json` (install path unpublished) | 8 PascalCase events; command-only. New Copilot hooks are the Copilot row | workspace over user | `vscode-hooks`, `vscode-json` |
634
+ | Copilot CLI | `.github/hooks` with `version: 1`; inline `hooks` in `.github/copilot/settings.json` and `settings.local.json` | `--global` `$COPILOT_HOME` / `~/.copilot/hooks` with `version: 1`, `settings.json`, and `mcp-config.json` (`mcpServers`); unread `/etc/github-copilot/policy.d` | camelCase + PascalCase map; `bash` / `powershell` / `command` / `exec` / `http`; `cwd`; `timeoutSec`; prompt on `sessionStart`; user MCP `command` / `url` | documented sources coexist; policy unread; Copilot also reads `.claude/settings.json` but this scanner lints that file as Claude. Workspace `.mcp.json` stays the shared profile | `copilot-hooks` fixture |
635
+ | Cursor | nested `.cursor/skills`; `.cursor/mcp.json`, `.cursor/hooks.json` and `.cursor/rules` on the ancestor walk only (not descendant package trees from repo root) | `--global` `~/.cursor/hooks.json` and `~/.cursor/mcp.json`; unread MDM/team hooks; unread `~/.cursor/skills/` until a page quotes that directory; unread third-party Claude import as Cursor schema | Agent Skills; MCP launch; 500-line rules; 21 hook events, optional `type` (`command` / `prompt`), required `command`. Missing `version` is not a finding | n/a | `cursor-json` fixture |
559
636
  | Grok | `.grok/config.toml` walk-up; `.grok/hooks/*.json`; `.grok/skills`; `.grok/rules/*.md`; `Agents.md` / `AGENT.md` | `--global` `$GROK_HOME` or `~/.grok` config/hooks/skills; unread managed, requirements, plugins, `[skills] paths`, agents, credentials | `command` / `url` MCP (no `type`); 14 events; command/http; frontmatter required, name/description optional; no rules cap | closer project wins; project same-name replaces user | `grok-toml` fixture |
560
- | Antigravity | `.agents/mcp_config.json` | none | `serverUrl` launch | n/a | `antigravity-json` fixture |
561
- | Gemini | `.gemini/settings.json` | unread `~/.gemini/settings.json` unless `--global` is wired for it (it is not) | `command` / `url` / `httpUrl`; underscore-alias warning | n/a | `gemini-json` fixture |
562
- | Windsurf | `.devin/rules/*.md` (preferred), `.windsurf/rules/*.md` (fallback), `.windsurfrules`; portable `AGENTS.md` / `agents.md` | `--global` `~/.codeium/windsurf/mcp_config.json` and `~/.codeium/windsurf/memories/global_rules.md`; unread auto memories, Devin CLI MCP, system `/etc/devin/rules` | 12k / 6k character rules; workspace `trigger`; MCP `command` / `serverUrl` / `url` (no `type`) | n/a (no project MCP; both rule trees inventoried) | `windsurf-rules` fixture |
563
- | Kiro | none | none | none | n/a | none |
564
- | Cline | none | none | none | n/a | none |
565
- | Roo | none | none | none | n/a | none |
566
- | Kilo | none | none | none | n/a | none |
637
+ | Antigravity | `.agents/mcp_config.json`; `.agents/hooks.json`, `GEMINI.md`, `.agents/GEMINI.md`, `.agents/rules/*.md`, and legacy `.agent/rules/*.md` on the ancestor walk. Workspace `AGENTS.md` stays the AGENTS.md row. `.agents/skills` stays the Agent Skills row | `--global` `~/.gemini/config` `mcp_config.json`, `hooks.json`, `rules/*.md`, `skills/`, `AGENTS.md`, and `GEMINI.md`, plus `~/.gemini/AGENTS.md` and `GEMINI.md`. Unread plugin hooks, plugin rules, plugin skills, `~/.gemini/antigravity-cli/settings.json`, `~/.gemini/antigravity-cli/skills/`, `~/.gemini/skills/`, and `~/.gemini/antigravity/skills/` | `command` / `serverUrl` (`url` is an alias; `httpUrl` is not a launch field); 6 hook events; direct handlers or matcher groups; `type` defaults to `command`; modular rule `trigger`; 24,000-byte rule limit | n/a | `antigravity-json`, `antigravity-hooks` fixtures |
638
+ | Gemini | `.gemini/settings.json` — MCP and `hooks` | `--global` `~/.gemini/settings.json` (hooks and MCP together); unread `/etc/gemini-cli/settings.json`, extension hooks, and `mcp-oauth-tokens.json` | `command` / `url` / `httpUrl`; underscore-alias warning; 11 hook events, required `type` (`command` only), nested groups, `$GEMINI_PROJECT_DIR` | n/a | `gemini-json` fixture |
639
+ | Windsurf | `.devin/rules/*.md` (preferred), `.windsurf/rules/*.md` (fallback), `.windsurfrules`; `.windsurf/hooks.json`; `.windsurf/skills`; portable `AGENTS.md` / `agents.md` | `--global` `~/.codeium/windsurf/mcp_config.json`, `memories/global_rules.md`, `hooks.json`, and `skills`; unread auto memories, system rules/hooks/skills, JetBrains `~/.codeium/hooks.json` | 12k / 6k character rules; workspace `trigger`; MCP `command` / `serverUrl` / `url` (no `type`); 12 snake_case hook events; `command` / `powershell`; Agent Skills on `.windsurf/skills` | n/a (no project MCP on this row; both rule trees inventoried). Devin CLI hooks and MCP are the next row | `windsurf-rules` fixture |
640
+ | Devin CLI | `.devin/hooks.v1.json`; `"hooks"` in `.devin/config.json` and `config.local.json`; `.devin/mcp_config.json` and `mcp_config.local.json`; `mcpServers` left in those config files; `.devin/skills` | `--global` `~/.config/devin/config.json`, `mcp_config.json`, and `skills`; unread Windows `%APPDATA%\devin` | 8 events (`PostCompaction`, not `PostCompact`); `command` / `prompt`; MCP `command` / `url` (`transport` `http` or `sse` is not a check); Agent Skills | Claude settings Devin also loads stay Claude findings | unit tests |
641
+ | Kiro | `.kiro/hooks/*.json`, `.kiro/skills`, `.kiro/agents/**/*.json` and `*.md` on the ancestor walk | `--global` `~/.kiro/hooks/`, `~/.kiro/skills`, `~/.kiro/agents/` | 13 PascalCase triggers, including `SessionEnd`, `PreCompact`, `PostCompact`; `command` / `agent`; Agent Skills. Agents are inventory only: the name is the relative path without the extension, and no field is required | nearer workspace agent shadows a farther one and a global one with the same name | unit tests |
642
+ | Cline | `.cline/skills` | `--global` `~/.cline/skills`; unread hooks (no quoted event list), unread agents | Agent Skills | n/a | unit tests |
643
+ | Roo | `.roo/skills` | `--global` `~/.roo/skills` | Agent Skills | n/a | unit tests |
644
+ | Kilo | `.kilo/skills` | `--global` `~/.kilo/skills`; unread extra paths in `kilo.jsonc` | Agent Skills | n/a | unit tests |
567
645
  | OpenCode | `opencode.json(c)` V1 and V2 | none | V2 local `command` must be an argv array | n/a | `opencode-json` fixture |
568
- | Junie | none | none | none | n/a | none |
646
+ | Junie | `.junie/skills` | `--global` `~/.junie/skills`; unread `--skill-location` / extension skills | Agent Skills with optional `description`; `name` need not match the folder | n/a | unit tests |
569
647
  | Continue | `.continue/config.yaml`, `.continue/mcpServers/*` | none | launch `command` / `url` / `uses`; standalone YAML `name` / `version` / `schema` | n/a | `continue-yaml`, `continue-mcpservers` |
570
648
 
571
- Cursor project rules (`.cursor/rules/**/*.mdc`) are a separate surface: discovery plus `cursor.rule.too-large` at info. Claude `.claude/rules/**/*.md` is inventoried and has no published line budget. Windsurf workspace rules use a 12,000-character budget (`windsurf.rule.too-large`); do not apply the Cursor 500-line check. Portable `AGENTS.md` / `agents.md` stay `sourceProvider: unknown` — this scanner does not claim exclusive Windsurf ownership.
572
-
573
- Old 0.7 rule ids still work in `ignoreRules` and `explain` (for example `hook.missing-script` aliases `claude.hook.missing-script`).
649
+ Cursor project rules (`.cursor/rules/**/*.mdc`) are a separate surface: discovery plus `cursor.rule.too-large` at info. Claude `.claude/rules/**/*.md` is inventoried and has no published line budget. Windsurf workspace rules use a 12,000-character budget (`windsurf.rule.too-large`); do not apply the Cursor 500-line check. Antigravity modular rules require `trigger` and a raw file over 24,000 bytes is `antigravity.rule.too-large`. Portable `AGENTS.md` / `agents.md` stay `sourceProvider: unknown` — this scanner does not claim exclusive Windsurf or Antigravity ownership.
574
650
 
575
651
  Agent definitions are checked for structure only. A Claude agent's frontmatter
576
652
  `name` is not compared to the filename; it must be lowercase letters and
@@ -591,21 +667,32 @@ illustration, not a pointer.
591
667
  ### Where it looks for hooks
592
668
 
593
669
  The Claude hooks reference lists seven places a hook can be registered. agentscan
594
- reads the four that live inside the scanned project, plus VS Code workspace hooks
595
- and Command Code settings hooks:
670
+ reads the project settings files, in-tree plugin and command-file hooks,
671
+ `~/.claude/settings.json` (or `$CLAUDE_CONFIG_DIR/settings.json`)
672
+ under `--global`, plus VS Code workspace hooks and Command Code settings hooks:
596
673
 
597
674
  | Registered in | Script paths resolve against |
598
675
  |---|---|
599
676
  | `.claude/settings.json` · `.claude/settings.local.json` | project root, `${CLAUDE_PROJECT_DIR}` |
677
+ | `~/.claude/settings.json` (or `$CLAUDE_CONFIG_DIR/settings.json`) under `--global` | scanned project root, `${CLAUDE_PROJECT_DIR}` |
600
678
  | A plugin's `hooks/hooks.json` (a directory with `.claude-plugin/plugin.json`, or the scan root itself) | the plugin root, `${CLAUDE_PLUGIN_ROOT}` |
601
- | `SKILL.md` frontmatter | the skill's own directory, then the project root |
679
+ | `SKILL.md` frontmatter (including in-tree plugin skills) | the skill's own directory, then the project root |
680
+ | `.claude/commands/*.md` frontmatter | the command file's directory, then the project root |
602
681
  | `.claude/agents/*.md` frontmatter | the agent file's directory, then the project root |
603
682
  | `.github/hooks/*.json` without `version: 1` (native VS Code) | project root and the hooks directory |
604
683
  | `.github/hooks/*.json` with `version: 1` (Copilot CLI) | project root, `cwd`, host `bash` / `powershell` / `command` |
605
- | `~/.copilot/hooks` under `--global` | same split on `version: 1` |
684
+ | `.github/copilot/settings.json` · `.github/copilot/settings.local.json` (Copilot CLI inline) | project root and the settings directory; always `copilot-cli` |
685
+ | `~/.copilot/hooks` and `~/.copilot/settings.json` (or `$COPILOT_HOME/…`) under `--global` | same Copilot CLI / native VS Code split on hook files; settings hooks are always `copilot-cli` |
606
686
  | `.commandcode/settings.json` · `.commandcode/settings.local.json` (and user settings under `--global`) | Command Code project root (git root, or cwd outside a git repo), `$COMMANDCODE_PROJECT_DIR`, `$COMMANDCODE_CWD` |
607
687
  | `.grok/hooks/*.json` | project root and the hooks directory |
608
688
  | `$GROK_HOME/hooks` or `~/.grok/hooks` under `--global` | same Grok 14-event / command-or-http split |
689
+ | `.github/agents/*.agent.md` frontmatter | vscode-native (command-only); project root and the agent file's directory |
690
+ | `.kiro/hooks/*.json` | project root; array-of-hooks, not Claude event-keyed objects |
691
+ | `~/.kiro/hooks/` under `--global` | scanned project root |
692
+ | `.cursor/hooks.json` | project root |
693
+ | `~/.cursor/hooks.json` under `--global` | scanned project root |
694
+ | `.gemini/settings.json` `hooks` | project root, `$GEMINI_PROJECT_DIR` |
695
+ | `~/.gemini/settings.json` `hooks` under `--global` | scanned project root, `$GEMINI_PROJECT_DIR` |
609
696
 
610
697
  `${CLAUDE_PLUGIN_ROOT}` is expanded **only** for a hook that came from a plugin.
611
698
  In a settings file it names nothing, so the path is skipped rather than guessed
@@ -641,8 +728,8 @@ first.
641
728
  should add a `next-*` skill". That needed a hand-maintained registry, and the
642
729
  version that existed fired on none of the 17 projects measured, so it was
643
730
  removed. Skill provenance comes from `skills-lock.json` instead.
644
- - **No rule produces `refresh` or `keep`.** Comparing a skill's content against
645
- the `computedHash` in `skills-lock.json` is the obvious `refresh` source; the
731
+ - **No skill-content hash check.** Comparing a skill's content against
732
+ the `computedHash` in `skills-lock.json` is the obvious source of one; the
646
733
  hash algorithm used by the installing tool is not documented here, and it is
647
734
  not reproducible from `SKILL.md` bytes alone, so it is not implemented.
648
735
  - **Runtime split.** Source development and the composite Action use Bun; the
@@ -661,39 +748,97 @@ first.
661
748
  still inspect every readable MCP file.
662
749
  - **Codex admin and profile layers are unread.** System `/etc/codex/config.toml`,
663
750
  managed preferences, `requirements.toml`, `$CODEX_HOME/<name>.config.toml`
664
- profiles, plugin MCP, and `trust_level` are not opened. User
665
- `$CODEX_HOME/config.toml` (or `~/.codex/config.toml`) MCP is scanned only
666
- under `--global`. `auth.json` is never opened. Same-name user and project
667
- MCP servers are both inventoried — the published page does not quote a
751
+ profiles, plugin hooks and MCP, and `trust_level` are not opened. Project
752
+ `.codex/hooks.json` and inline `[hooks]` in `.codex/config.toml` are
753
+ inventoried on the ancestor walk even when that layer is untrusted. The
754
+ same two files under `$CODEX_HOME` (or `~/.codex`) are opened only with
755
+ `--global`. `auth.json` is never opened. Same-name user and project MCP
756
+ servers are both inventoried — the published page does not quote a
668
757
  replace-entirely rule. User-file `project_doc_*` knobs are not applied.
758
+ Relative hook commands resolve against the scanned directory, not the git
759
+ root. SessionEnd and Interrupt timeouts outside 1–3 seconds are findings;
760
+ other events have no quoted maximum.
669
761
  - **Command Code local MCP is unread.** `~/.commandcode/projects/{project}/mcp.json`
670
762
  is keyed by a working-directory slug that the sessions page does not publish.
671
763
  Until it does, that path is skipped rather than guessed. Bundled Command Code
672
764
  skills and `--skill` launch flags are not project files and are not scanned.
673
- `mods.paths` are inventoried; the TypeScript is never executed or imported.
765
+ Command Code `mods` settings are not read, and mod TypeScript is never
766
+ executed or imported.
674
767
  `auth.json` and `mcp-tokens.json` are never opened. Command Code project
675
768
  root is the git root (or cwd outside a git repo); a child `.cursor` does
676
769
  not hide `<git-root>/.commandcode/`. Spec/runtime Command Code checks skip
677
770
  shadowed lower-precedence definitions; secrets still inspect every readable
678
771
  MCP file.
679
- - **Three Claude hook locations are unread.** `~/.claude/settings.json`, managed policy
680
- settings, and installed marketplace plugins under `~/.claude/plugins` all sit
681
- outside the scanned project, and the docs say a plugin's install directory
682
- changes on every update. A hook registered in one of those, pointing at a
683
- missing script, is not reported.
772
+ - **Two Claude hook locations stay unread.** Managed policy settings and
773
+ installed marketplace plugins under `~/.claude/plugins` sit outside the
774
+ scanned project, and the docs say a plugin's install directory changes
775
+ on every update. A hook registered in one of those, pointing at a
776
+ missing script, is not reported. User settings, skills, agents,
777
+ `CLAUDE.md`, rules, and commands under `$CLAUDE_CONFIG_DIR` or
778
+ `~/.claude` are scanned only under `--global`. User `~/.claude.json`
779
+ MCP is scanned only under `--global`: top-level `mcpServers` and
780
+ `projects.<scanned-path>.mcpServers`. Other projects' local servers
781
+ are skipped. A file without those keys is skipped, not
782
+ `config.unreadable`. In-tree plugin skills, agents, commands, and
783
+ `.mcp.json` are read; plugin agent frontmatter hooks are ignored
784
+ because Claude skips them.
785
+ - **Default `skills/` is Claude schema.** A repo-root `skills/` directory is
786
+ discovered, but `name` / directory-match from the Agent Skills spec are not
787
+ applied there. `.agents/skills`, `.cursor/skills`, `.codex/skills`,
788
+ `.commandcode/skills`, `.kiro/skills`, `.cline/skills`, `.roo/skills`,
789
+ `.kilo/skills`, and `.junie/skills` use the Agent Skills contract. Junie
790
+ does not require `description` and does not require `name` to match the
791
+ folder.
684
792
  - **Copilot CLI policy hooks are unread.** `/etc/github-copilot/policy.d` and the
685
793
  Windows policy directory are machine-wide admin files. User
686
- `~/.copilot/hooks` is scanned only under `--global`. Inline Copilot
687
- `.github/copilot/settings.json` hooks are unread.
688
- - **Gemini user settings are unread.** `~/.gemini/settings.json` is outside a
689
- normal project scan.
794
+ `~/.copilot/hooks` and `~/.copilot/settings.json` (or `$COPILOT_HOME/…`)
795
+ are scanned only under `--global`. Plugin `hooks.json` stays unread.
796
+ Copilot also reads repository `.claude/settings.json`; this scanner still
797
+ lints that file as Claude (required nested groups and required `type`).
798
+ Copilot-only event names there stay `claude.hook.unknown-event`.
799
+ - **Gemini system settings are unread.** `~/.gemini/settings.json` (hooks and
800
+ MCP together) is scanned only under `--global`. `/etc/gemini-cli/settings.json`
801
+ and extension-provided hooks stay unread. `mcp-oauth-tokens.json` is never
802
+ opened.
803
+ - **Cursor managed hooks are unread.** `~/.cursor/hooks.json` is scanned only
804
+ under `--global`. MDM paths (`/etc/cursor/hooks.json` and the macOS /
805
+ Windows equivalents) and dashboard-synced team hooks stay unread. Cursor's
806
+ third-party import of Claude hook files stays on the Claude profile.
807
+ - **Antigravity global files.** `~/.gemini/config/mcp_config.json`,
808
+ `hooks.json`, `rules/*.md`, `skills/`, `AGENTS.md`, and `GEMINI.md`, plus
809
+ `~/.gemini/AGENTS.md` and `~/.gemini/GEMINI.md`, are scanned only under
810
+ `--global`. Project `.agents/mcp_config.json`, `.agents/hooks.json`,
811
+ `GEMINI.md`, `.agents/GEMINI.md`, `.agents/rules/*.md`, and legacy
812
+ `.agent/rules/*.md` are read on the ancestor walk. Workspace `AGENTS.md`
813
+ stays the portable policy file, so the 24,000-byte rule check does not
814
+ apply to it. That check counts raw bytes, before `@[label](path)`
815
+ expansion, and the 20,000-token aggregate is not checked. `rules.json`
816
+ paths are not followed. `url` is an alias of `serverUrl`. `httpUrl` alone
817
+ is still not a launch field. Plugin hooks, plugin rules, plugin skills,
818
+ `~/.gemini/antigravity-cli/settings.json`,
819
+ `~/.gemini/antigravity-cli/skills/`, `~/.gemini/skills/`, and
820
+ `~/.gemini/antigravity/skills/` stay unread. Hook `timeout` has no quoted
821
+ range, so it is not checked. Global skills use the Agent Skills contract,
822
+ which requires `name` even though the Antigravity skills page says `name`
823
+ defaults to the folder.
824
+ - **Cline hooks stay unread.** Cline's hooks page did not quote an event
825
+ identifier table. Cline agents are unread for the same reason. Kiro agents
826
+ are inventoried from `.kiro/agents/**/*.json` and `*.md` on the ancestor
827
+ walk, and from `~/.kiro/agents/` under `--global`. The name is the path
828
+ relative to the agents directory without the extension. A nearer workspace
829
+ agent shadows a farther one and a global one with the same name. No field
830
+ is required, so there is no `kiro.agent.*` check.
831
+ - **VS Code marketplace plugin hooks are unread.** In-tree Claude plugins
832
+ stay Claude. VS Code's `hooks.json` / `hooks/hooks.json` inside an
833
+ installed plugin package is not a project path.
690
834
  - **Windsurf Devin CLI MCP and auto memories are unread.** Cascade MCP is
691
835
  global-only (`~/.codeium/windsurf/mcp_config.json`, `--global`). The Devin
692
836
  Local agent uses unpublished CLI config files — that path is not guessed.
693
837
  Auto-generated files under `~/.codeium/windsurf/memories/` other than
694
- `global_rules.md` are not opened. System / enterprise rule directories
695
- (`/etc/devin/rules`, `/etc/windsurf/rules`) stay unread. There is no
696
- quoted project MCP path.
838
+ `global_rules.md` are not opened. System / enterprise rule, hook, and
839
+ skill directories (`/etc/devin/rules`, `/etc/windsurf/rules`,
840
+ `/etc/windsurf/hooks.json`, `/etc/windsurf/skills/`) stay unread. There
841
+ is no quoted project MCP path. JetBrains `~/.codeium/hooks.json` is unread.
697
842
  - **Bounded reads.** A `SKILL.md` is read to 64 KB and a policy file to 100 KB,
698
843
  so `skill.broken-reference` and `policyLines` see only that much of a larger
699
844
  file. This is reported — `scan.truncated`, at `info` — rather than left
@@ -707,6 +852,25 @@ Notes for every published version are in [CHANGELOG.md](CHANGELOG.md).
707
852
  GitHub already has pages for [v0.1.0](https://github.com/SimaAlexandru99/agentscan/releases/tag/v0.1.0)
708
853
  and [v0.4.0](https://github.com/SimaAlexandru99/agentscan/releases/tag/v0.4.0).
709
854
 
855
+ **2.0.0** (12 November 2026) is breaking for configs that still name the 0.7
856
+ rule ids or the old threshold keys. `ignoreRules`, `ignoreFindings`, and
857
+ `explain` accept only the printed ids. `thresholds.skillDescriptionBytes`
858
+ and `thresholds.skills` are rejected; the listing budget is
859
+ `thresholds.skillListingChars`. It also ships the November 2026 vendor
860
+ contracts: Kiro agents, Antigravity rules, skills, and hooks, Codex hooks,
861
+ Cursor user MCP, Devin CLI hooks / MCP / skills, and the Copilot user MCP
862
+ file. **143 checks**, **647 tests**.
863
+
864
+ **1.4.0** (2 September 2026) closes two coverage gaps and ships the
865
+ 2026-09-02 re-verification: Copilot CLI inline `hooks` in
866
+ `.github/copilot/settings.json` / `settings.local.json` and `--global`
867
+ `$COPILOT_HOME` / `~/.copilot/settings.json`; `mcp.literal-credential` on
868
+ MCP `headers` / `http_headers` / `auth` (field paths only). Also the
869
+ seven false-positive corrections, spec content hashes, verbatim
870
+ conformance fixtures, and Windsurf Cascade hooks / skills from #13.
871
+ **103 checks**, **532 tests**. Policy hooks, plugin `hooks.json`, and
872
+ `.claude/settings.json` as Copilot stay unread.
873
+
710
874
  **1.3.0** (31 August 2026) adds native Windsurf / Cascade coverage: workspace
711
875
  `.devin/rules` and `.windsurf/rules`, portable `agents.md`, and `--global`
712
876
  `mcp_config.json` plus `global_rules.md`. Devin CLI MCP, auto memories,
@@ -760,7 +924,9 @@ Before publishing a release:
760
924
 
761
925
  1. Confirm `package.json` and `src/version.ts` contain the same version.
762
926
  2. Run `bun run build`, then verify `node dist/cli.js --version` prints it.
763
- 3. Run `bun run typecheck`, `bun test`, and `bun run spec:check`.
927
+ 3. Run `bun run typecheck`, `bun test`, and `bun run spec:check`. If a page
928
+ hash changed, re-read that page, update its `docs/spec/` capture and the
929
+ conformance fixture, then `bun run spec:record`.
764
930
  4. Refresh the test-count badge and version examples above from that release run.
765
931
  5. Add a section to [CHANGELOG.md](CHANGELOG.md) for the version, then publish
766
932
  the GitHub release from that text.