claudecode-linter 2.1.143 → 2.1.148
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/.claudecode-lint.defaults.yaml +14 -1
- package/README.md +24 -6
- package/contracts/agent-frontmatter.schema.json +147 -0
- package/contracts/command-frontmatter.schema.json +93 -0
- package/contracts/settings.schema.json +2212 -0
- package/contracts/skill-frontmatter.schema.json +182 -0
- package/dist/contracts.js +64 -1
- package/dist/discovery.js +26 -2
- package/dist/fixers/settings-json.js +7 -6
- package/dist/index.js +23 -7
- package/dist/linters/agent-md.js +43 -8
- package/dist/linters/claude-md.js +15 -3
- package/dist/linters/command-md.js +31 -5
- package/dist/linters/mcp-json.js +9 -2
- package/dist/linters/settings-json.js +316 -29
- package/dist/linters/skill-md.js +101 -11
- package/dist/plugin-schema.js +12 -0
- package/dist/utils/frontmatter-keys.js +38 -0
- package/package.json +7 -3
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extractedFromClaudeCodeVersion": "2.1.146",
|
|
3
|
+
"extractedAt": "2026-05-21T21:04:24.797Z",
|
|
4
|
+
"schema": {
|
|
5
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
6
|
+
"title": "Claude Code SKILL.md frontmatter",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"name": {
|
|
10
|
+
"description": "Display name. Defaults to the filename without extension."
|
|
11
|
+
},
|
|
12
|
+
"description": {
|
|
13
|
+
"description": "One-line summary shown in listings and the Skill tool."
|
|
14
|
+
},
|
|
15
|
+
"model": {
|
|
16
|
+
"description": "Model override (`haiku`, `sonnet`, `opus`, or a full ID). Use `inherit` to match the parent conversation."
|
|
17
|
+
},
|
|
18
|
+
"allowed-tools": {
|
|
19
|
+
"anyOf": [
|
|
20
|
+
{
|
|
21
|
+
"anyOf": [
|
|
22
|
+
{
|
|
23
|
+
"type": "string"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"type": "number"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"type": "boolean"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "null"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": {
|
|
39
|
+
"type": "string"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"description": "Tools available to the model while this file is active. Comma-separated string or YAML list."
|
|
44
|
+
},
|
|
45
|
+
"argument-hint": {
|
|
46
|
+
"description": "Placeholder text shown after the slash command name."
|
|
47
|
+
},
|
|
48
|
+
"arguments": {
|
|
49
|
+
"anyOf": [
|
|
50
|
+
{
|
|
51
|
+
"anyOf": [
|
|
52
|
+
{
|
|
53
|
+
"type": "string"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"type": "number"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"type": "boolean"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"type": "null"
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"type": "array",
|
|
68
|
+
"items": {
|
|
69
|
+
"type": "string"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"description": "@internal — typed variant of argument-hint; argument-hint is the documented form"
|
|
74
|
+
},
|
|
75
|
+
"disable-model-invocation": {
|
|
76
|
+
"description": "If true, the model cannot invoke this via the Skill tool; only users can type the slash command."
|
|
77
|
+
},
|
|
78
|
+
"user-invocable": {
|
|
79
|
+
"description": "If false, hides the slash command from users; only the model can invoke it via the Skill tool."
|
|
80
|
+
},
|
|
81
|
+
"effort": {
|
|
82
|
+
"description": "Thinking effort for the model: `low`, `medium`, `high`, `max`, or an integer."
|
|
83
|
+
},
|
|
84
|
+
"shell": {
|
|
85
|
+
"description": "Shell for `!`-command blocks: `bash` or `powershell`. Defaults to bash regardless of platform."
|
|
86
|
+
},
|
|
87
|
+
"version": {
|
|
88
|
+
"description": "@internal — bookkeeping, not surfaced to users"
|
|
89
|
+
},
|
|
90
|
+
"when_to_use": {
|
|
91
|
+
"description": "Guidance for when the model should reach for this skill. Becomes part of the tool description."
|
|
92
|
+
},
|
|
93
|
+
"paths": {
|
|
94
|
+
"anyOf": [
|
|
95
|
+
{
|
|
96
|
+
"anyOf": [
|
|
97
|
+
{
|
|
98
|
+
"type": "string"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"type": "number"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"type": "boolean"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"type": "null"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"type": "array",
|
|
113
|
+
"items": {
|
|
114
|
+
"type": "string"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
"description": "Glob patterns this skill applies to. The skill only loads when the model touches matching files."
|
|
119
|
+
},
|
|
120
|
+
"hooks": {
|
|
121
|
+
"description": "Hooks registered while this skill is active. Same shape as settings.json `hooks`."
|
|
122
|
+
},
|
|
123
|
+
"context": {
|
|
124
|
+
"enum": [
|
|
125
|
+
"inline",
|
|
126
|
+
"fork",
|
|
127
|
+
null
|
|
128
|
+
],
|
|
129
|
+
"description": "Where the skill runs: `inline` expands into the current conversation; `fork` spawns a subagent."
|
|
130
|
+
},
|
|
131
|
+
"agent": {
|
|
132
|
+
"description": "Agent type to spawn when `context: fork`."
|
|
133
|
+
},
|
|
134
|
+
"fallback": {
|
|
135
|
+
"description": "@internal — interim defense-in-depth for thin-pointer skill stubs. If true, this skill yields to a same-suffix plugin or MCP skill (`<plugin>:<name>` / `<server>:<name>`) when one is loaded. Stubs carrying this should be deleted once their canonical plugin/MCP skill ships, not maintained."
|
|
136
|
+
},
|
|
137
|
+
"created_by": {
|
|
138
|
+
"description": "@internal — provenance marker (e.g. dream-proposal)"
|
|
139
|
+
},
|
|
140
|
+
"improved_by": {
|
|
141
|
+
"description": "@internal — provenance marker (e.g. dream-proposal)"
|
|
142
|
+
},
|
|
143
|
+
"mcpServers": {
|
|
144
|
+
"description": "@internal"
|
|
145
|
+
},
|
|
146
|
+
"lspServers": {
|
|
147
|
+
"description": "@internal"
|
|
148
|
+
},
|
|
149
|
+
"agents": {
|
|
150
|
+
"description": "@internal"
|
|
151
|
+
},
|
|
152
|
+
"outputStyles": {
|
|
153
|
+
"description": "@internal"
|
|
154
|
+
},
|
|
155
|
+
"themes": {
|
|
156
|
+
"description": "@internal"
|
|
157
|
+
},
|
|
158
|
+
"workflows": {
|
|
159
|
+
"description": "@internal"
|
|
160
|
+
},
|
|
161
|
+
"channels": {
|
|
162
|
+
"description": "@internal"
|
|
163
|
+
},
|
|
164
|
+
"monitors": {
|
|
165
|
+
"description": "@internal"
|
|
166
|
+
},
|
|
167
|
+
"settings": {
|
|
168
|
+
"description": "@internal"
|
|
169
|
+
},
|
|
170
|
+
"experimental": {
|
|
171
|
+
"description": "@internal"
|
|
172
|
+
},
|
|
173
|
+
"dependencies": {
|
|
174
|
+
"description": "@internal"
|
|
175
|
+
},
|
|
176
|
+
"metadata": {
|
|
177
|
+
"description": "@internal"
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
"description": "Claude Code SKILL.md YAML frontmatter. Validates the structure of known fields; the object is intentionally permissive — unknown frontmatter keys are not schema errors (Claude Code still loads the skill), they are reported by the advisory skill-md/no-unknown-frontmatter rule."
|
|
181
|
+
}
|
|
182
|
+
}
|
package/dist/contracts.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// Auto-generated from contracts/claude-code-contracts.json
|
|
2
|
-
// Claude Code v2.1.
|
|
2
|
+
// Claude Code v2.1.148 — extracted 2026-05-22T07:13:45.210Z
|
|
3
3
|
// Do not edit manually. Run: npm run generate-contracts
|
|
4
4
|
export const TOOLS = new Set([
|
|
5
5
|
"Agent",
|
|
6
6
|
"AskUserQuestion",
|
|
7
7
|
"Bash",
|
|
8
8
|
"Config",
|
|
9
|
+
"CronCreate",
|
|
10
|
+
"CronDelete",
|
|
11
|
+
"CronList",
|
|
9
12
|
"Edit",
|
|
10
13
|
"EnterPlanMode",
|
|
11
14
|
"EnterWorktree",
|
|
@@ -16,10 +19,13 @@ export const TOOLS = new Set([
|
|
|
16
19
|
"LSP",
|
|
17
20
|
"ListMcpResources",
|
|
18
21
|
"Mcp",
|
|
22
|
+
"Monitor",
|
|
19
23
|
"NotebookEdit",
|
|
20
24
|
"NotebookRead",
|
|
25
|
+
"PushNotification",
|
|
21
26
|
"Read",
|
|
22
27
|
"ReadMcpResource",
|
|
28
|
+
"RemoteTrigger",
|
|
23
29
|
"SendMessage",
|
|
24
30
|
"Skill",
|
|
25
31
|
"SubscribeMcpResource",
|
|
@@ -164,6 +170,7 @@ export const MCP_SERVER_FIELDS = new Set([
|
|
|
164
170
|
"headersHelper",
|
|
165
171
|
"oauth",
|
|
166
172
|
"role",
|
|
173
|
+
"timeout",
|
|
167
174
|
"type",
|
|
168
175
|
"url",
|
|
169
176
|
]);
|
|
@@ -316,7 +323,63 @@ export const SETTINGS_USER_FIELDS = new Set([
|
|
|
316
323
|
"wslInheritsWindowsSettings",
|
|
317
324
|
]);
|
|
318
325
|
export const SETTINGS_PROJECT_FIELDS = new Set([
|
|
326
|
+
"hooks",
|
|
319
327
|
"permissions",
|
|
328
|
+
"sandbox",
|
|
329
|
+
]);
|
|
330
|
+
// Allowed sub-keys of the settings `permissions` object.
|
|
331
|
+
export const PERMISSIONS_FIELDS = new Set([
|
|
332
|
+
"additionalDirectories",
|
|
333
|
+
"allow",
|
|
334
|
+
"ask",
|
|
335
|
+
"defaultMode",
|
|
336
|
+
"deny",
|
|
337
|
+
"disableAutoMode",
|
|
338
|
+
"disableBypassPermissionsMode",
|
|
339
|
+
]);
|
|
340
|
+
// Allowed sub-keys of the settings `sandbox` object.
|
|
341
|
+
export const SANDBOX_FIELDS = new Set([
|
|
342
|
+
"allowUnsandboxedCommands",
|
|
343
|
+
"autoAllowBashIfSandboxed",
|
|
344
|
+
"bwrapPath",
|
|
345
|
+
"enableWeakerNestedSandbox",
|
|
346
|
+
"enableWeakerNetworkIsolation",
|
|
347
|
+
"enabled",
|
|
348
|
+
"excludedCommands",
|
|
349
|
+
"failIfUnavailable",
|
|
350
|
+
"filesystem",
|
|
351
|
+
"ignoreViolations",
|
|
352
|
+
"network",
|
|
353
|
+
"ripgrep",
|
|
354
|
+
]);
|
|
355
|
+
// Allowed sub-keys of `sandbox.network` and `sandbox.filesystem`.
|
|
356
|
+
export const SANDBOX_NETWORK_FIELDS = new Set([
|
|
357
|
+
"allowAllUnixSockets",
|
|
358
|
+
"allowLocalBinding",
|
|
359
|
+
"allowMachLookup",
|
|
360
|
+
"allowManagedDomainsOnly",
|
|
361
|
+
"allowUnixSockets",
|
|
362
|
+
"allowedDomains",
|
|
363
|
+
"deniedDomains",
|
|
364
|
+
"httpProxyPort",
|
|
365
|
+
"socksProxyPort",
|
|
366
|
+
"tlsTerminate",
|
|
367
|
+
]);
|
|
368
|
+
export const SANDBOX_FILESYSTEM_FIELDS = new Set([
|
|
369
|
+
"allowManagedReadPathsOnly",
|
|
370
|
+
"allowRead",
|
|
371
|
+
"allowWrite",
|
|
372
|
+
"denyRead",
|
|
373
|
+
"denyWrite",
|
|
374
|
+
]);
|
|
375
|
+
// Valid values for `permissions.defaultMode`.
|
|
376
|
+
export const PERMISSION_MODES = new Set([
|
|
377
|
+
"acceptEdits",
|
|
378
|
+
"auto",
|
|
379
|
+
"bypassPermissions",
|
|
380
|
+
"default",
|
|
381
|
+
"dontAsk",
|
|
382
|
+
"plan",
|
|
320
383
|
]);
|
|
321
384
|
// Hand-curated denylist of tools declared in agent frontmatter that never
|
|
322
385
|
// reach plugin-defined subagents at runtime. Source: tracked upstream bugs
|
package/dist/discovery.js
CHANGED
|
@@ -79,6 +79,25 @@ export function discoverArtifacts(targetPath, options) {
|
|
|
79
79
|
}
|
|
80
80
|
return artifacts;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Detect which Claude Code artifact types are present under the given paths.
|
|
84
|
+
* Returns the distinct types, sorted; excludes the `misplaced-file` diagnostic
|
|
85
|
+
* category (it marks a misplacement, not a kind of artifact a project owns).
|
|
86
|
+
* An empty result means the path holds no recognizable Claude Code artifacts.
|
|
87
|
+
*
|
|
88
|
+
* Powers the `--detect` CLI mode: a generic git hook can call it to decide
|
|
89
|
+
* whether a repository is a Claude Code plugin / config tree before linting.
|
|
90
|
+
*/
|
|
91
|
+
export function detectArtifactTypes(paths, ignore = []) {
|
|
92
|
+
const found = new Set();
|
|
93
|
+
for (const targetPath of paths) {
|
|
94
|
+
for (const a of discoverArtifacts(targetPath, { ignore })) {
|
|
95
|
+
if (a.artifactType !== "misplaced-file")
|
|
96
|
+
found.add(a.artifactType);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return [...found].sort();
|
|
100
|
+
}
|
|
82
101
|
function detectScope(filePath) {
|
|
83
102
|
const resolved = resolve(filePath);
|
|
84
103
|
// Inside ~/.claude/ itself (not a subdirectory project)
|
|
@@ -253,8 +272,9 @@ function discoverInDirectory(dir) {
|
|
|
253
272
|
* sitting at a non-canonical location is returned as a
|
|
254
273
|
* `misplaced-file` artifact for the misplaced-file linter to flag.
|
|
255
274
|
*
|
|
256
|
-
* Ignores typical noise dirs (`node_modules`, `.git`, `dist`,
|
|
257
|
-
* plugin install cache's
|
|
275
|
+
* Ignores typical noise dirs (`node_modules`, `.git`, `dist`,
|
|
276
|
+
* `.claude/worktrees/` worktree copies, the plugin install cache's
|
|
277
|
+
* `.in_use` / `.orphaned_at` markers).
|
|
258
278
|
*/
|
|
259
279
|
function findMisplacedFiles(pluginRoot) {
|
|
260
280
|
const out = [];
|
|
@@ -270,6 +290,10 @@ function findMisplacedFiles(pluginRoot) {
|
|
|
270
290
|
ignore: [
|
|
271
291
|
"**/node_modules/**",
|
|
272
292
|
"**/.git/**",
|
|
293
|
+
// `.claude/worktrees/` holds transient git-worktree copies
|
|
294
|
+
// (Claude Code's own EnterWorktree). Linting them re-reports
|
|
295
|
+
// every artifact once per worktree — pure noise.
|
|
296
|
+
"**/.claude/worktrees/**",
|
|
273
297
|
"**/dist/**",
|
|
274
298
|
"**/.in_use/**",
|
|
275
299
|
"**/.orphaned_at/**",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { formatJson } from "../utils/prettier.js";
|
|
2
2
|
const TOP_LEVEL_KEY_ORDER = [
|
|
3
3
|
"permissions",
|
|
4
|
+
"sandbox",
|
|
5
|
+
"hooks",
|
|
4
6
|
"env",
|
|
5
7
|
"plugins",
|
|
6
8
|
"skipDangerousModePermissionPrompt",
|
|
@@ -30,15 +32,14 @@ export const settingsJsonFixer = {
|
|
|
30
32
|
ordered[key] = parsed[key];
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
|
-
// Sort permissions.allow
|
|
35
|
+
// Sort the permissions.allow / deny / ask rule arrays alphabetically
|
|
34
36
|
const permissions = ordered["permissions"];
|
|
35
37
|
if (typeof permissions === "object" && permissions !== null && !Array.isArray(permissions)) {
|
|
36
38
|
const perms = permissions;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
perms["deny"] = [...perms["deny"]].sort();
|
|
39
|
+
for (const list of ["allow", "deny", "ask"]) {
|
|
40
|
+
if (Array.isArray(perms[list])) {
|
|
41
|
+
perms[list] = [...perms[list]].sort();
|
|
42
|
+
}
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
return formatJson(JSON.stringify(ordered));
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import sade from "sade";
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
import { loadConfig, mergeCliRules } from "./config.js";
|
|
8
|
-
import { discoverArtifacts } from "./discovery.js";
|
|
8
|
+
import { discoverArtifacts, detectArtifactTypes } from "./discovery.js";
|
|
9
9
|
import { formatHuman } from "./formatters/human.js";
|
|
10
10
|
import { formatJson } from "./formatters/json.js";
|
|
11
11
|
import { pluginJsonLinter } from "./linters/plugin-json.js";
|
|
@@ -110,19 +110,20 @@ const pkgVersion = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.met
|
|
|
110
110
|
sade("claudecode-linter", true)
|
|
111
111
|
.version(pkgVersion)
|
|
112
112
|
.describe("Linter for Claude Code plugin artifacts")
|
|
113
|
-
.option("--lint", "Lint artifacts and report issues (default)")
|
|
114
|
-
.option("--fix", "Auto-fix lint violations, then report remaining issues")
|
|
115
|
-
.option("--format", "Format all artifacts for consistent style (no lint output)")
|
|
113
|
+
.option("--lint", "Lint artifacts and report issues (default)", false)
|
|
114
|
+
.option("--fix", "Auto-fix lint violations, then report remaining issues", false)
|
|
115
|
+
.option("--format", "Format all artifacts for consistent style (no lint output)", false)
|
|
116
116
|
.option("--output", "Output format: human | json", "human")
|
|
117
117
|
.option("--config", "Config file path")
|
|
118
118
|
.option("--scope", "Filter by scope: user | project | subdirectory")
|
|
119
119
|
.option("--ignore", "Comma-separated glob patterns to ignore")
|
|
120
|
-
.option("--quiet", "Only show errors")
|
|
120
|
+
.option("--quiet", "Only show errors", false)
|
|
121
121
|
.option("--enable", "Comma-separated rule IDs to enable")
|
|
122
122
|
.option("--disable", "Comma-separated rule IDs to disable")
|
|
123
123
|
.option("--rule", "Run only this single rule ID")
|
|
124
|
-
.option("--list-rules", "Print all rules with their default severity and exit")
|
|
125
|
-
.option("--
|
|
124
|
+
.option("--list-rules", "Print all rules with their default severity and exit", false)
|
|
125
|
+
.option("--detect", "Print detected Claude Code artifact type(s), one per line; exit 0 if any, 1 if none", false)
|
|
126
|
+
.option("--fix-dry-run", "Run fixers but print diff instead of writing", false)
|
|
126
127
|
.option("--init", "Copy default config to path (default: current directory)")
|
|
127
128
|
.action(async (opts) => {
|
|
128
129
|
// sade accepts variadic positional via opts._; default to ["."] when empty.
|
|
@@ -153,6 +154,21 @@ sade("claudecode-linter", true)
|
|
|
153
154
|
}
|
|
154
155
|
process.exit(0);
|
|
155
156
|
}
|
|
157
|
+
if (opts.detect) {
|
|
158
|
+
const ignoreD = opts.ignore
|
|
159
|
+
?.split(",")
|
|
160
|
+
.map((p) => p.trim())
|
|
161
|
+
.filter(Boolean) ?? [];
|
|
162
|
+
const types = detectArtifactTypes(paths, ignoreD);
|
|
163
|
+
if (opts.output === "json") {
|
|
164
|
+
process.stdout.write(`${JSON.stringify(types)}\n`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
for (const t of types)
|
|
168
|
+
process.stdout.write(`${t}\n`);
|
|
169
|
+
}
|
|
170
|
+
process.exit(types.length > 0 ? 0 : 1);
|
|
171
|
+
}
|
|
156
172
|
const enableList = opts.enable
|
|
157
173
|
? opts.enable
|
|
158
174
|
.split(",")
|
package/dist/linters/agent-md.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { AGENT_MODELS, AGENT_COLORS, TOOLS, PLUGIN_SUBAGENT_BLOCKED_TOOLS, } from "../contracts.js";
|
|
4
|
+
import { formatAjvError, loadAgentFrontmatterSchema, summarizeErrors, } from "../plugin-schema.js";
|
|
4
5
|
import { isRuleEnabled, getRuleSeverity } from "../types.js";
|
|
5
6
|
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
7
|
+
import { artifactLabel, classifyUnknownFrontmatterKey, } from "../utils/frontmatter-keys.js";
|
|
6
8
|
function findPluginRoot(agentFilePath) {
|
|
7
9
|
let dir = dirname(agentFilePath);
|
|
8
10
|
for (let i = 0; i < 10; i++) {
|
|
@@ -56,10 +58,11 @@ function parseToolsField(v) {
|
|
|
56
58
|
}
|
|
57
59
|
const RULES = [
|
|
58
60
|
{ id: "agent-md/valid-frontmatter", defaultSeverity: "error" },
|
|
61
|
+
{ id: "agent-md/schema-valid", defaultSeverity: "error" },
|
|
59
62
|
{ id: "agent-md/name-required", defaultSeverity: "error" },
|
|
60
63
|
{ id: "agent-md/name-format", defaultSeverity: "error" },
|
|
61
64
|
{ id: "agent-md/description-required", defaultSeverity: "error" },
|
|
62
|
-
{ id: "agent-md/description-
|
|
65
|
+
{ id: "agent-md/description-routing-guidance", defaultSeverity: "warning" },
|
|
63
66
|
{ id: "agent-md/model-required", defaultSeverity: "error" },
|
|
64
67
|
{ id: "agent-md/model-valid", defaultSeverity: "warning" },
|
|
65
68
|
{ id: "agent-md/color-required", defaultSeverity: "warning" },
|
|
@@ -97,6 +100,26 @@ export const agentMdLinter = {
|
|
|
97
100
|
push(diag(config, filePath, "agent-md/valid-frontmatter", "error", fm.error ?? "Invalid frontmatter"));
|
|
98
101
|
return diagnostics;
|
|
99
102
|
}
|
|
103
|
+
// schema-valid — structural validation against the JSON Schema
|
|
104
|
+
// auto-extracted from Claude Code's agent frontmatter Zod validator.
|
|
105
|
+
// The hand-written rules below add Claude-Code-specific advice with
|
|
106
|
+
// friendlier messages (model-valid / color-valid re-check fields the
|
|
107
|
+
// schema also covers — that minimal double-reporting is acceptable).
|
|
108
|
+
// The extracted schema is intentionally permissive about unknown
|
|
109
|
+
// frontmatter keys (Claude Code still loads the agent), so those are
|
|
110
|
+
// NOT reported here — agent-md/no-unknown-frontmatter handles them.
|
|
111
|
+
// Skipped silently if the schema bundle isn't shipped with this install.
|
|
112
|
+
if (isRuleEnabled(config, "agent-md/schema-valid")) {
|
|
113
|
+
const compiled = loadAgentFrontmatterSchema();
|
|
114
|
+
if (compiled) {
|
|
115
|
+
const ok = compiled.validate(fm.data);
|
|
116
|
+
if (!ok && compiled.validate.errors) {
|
|
117
|
+
for (const err of summarizeErrors(compiled.validate.errors)) {
|
|
118
|
+
push(diag(config, filePath, "agent-md/schema-valid", "error", formatAjvError(err)));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
100
123
|
// name
|
|
101
124
|
if (!("name" in fm.data) || typeof fm.data.name !== "string") {
|
|
102
125
|
push(diag(config, filePath, "agent-md/name-required", "error", '"name" is required in frontmatter'));
|
|
@@ -115,8 +138,16 @@ export const agentMdLinter = {
|
|
|
115
138
|
push(diag(config, filePath, "agent-md/description-required", "error", '"description" is required in frontmatter'));
|
|
116
139
|
}
|
|
117
140
|
else {
|
|
118
|
-
|
|
119
|
-
|
|
141
|
+
// An agent description must convey *when to route to it*. Three
|
|
142
|
+
// forms satisfy that: <example> blocks (the upstream few-shot
|
|
143
|
+
// convention), an explicit routing-triggers block, or prose that
|
|
144
|
+
// states the triggers. Warn only when the description does none.
|
|
145
|
+
const desc = fm.data.description;
|
|
146
|
+
const hasExamples = /<example>/i.test(desc);
|
|
147
|
+
const hasRoutingTriggers = /ROUTING TRIGGERS/i.test(desc);
|
|
148
|
+
const hasTriggerProse = /\b(use (it |this )?(for|when|to)\b|use this (agent|skill)|trigger(s|ed)? (on|by|when)|when the user|for any (task|request|work)|invoke (this|when))/i.test(desc);
|
|
149
|
+
if (!hasExamples && !hasRoutingTriggers && !hasTriggerProse) {
|
|
150
|
+
push(diag(config, filePath, "agent-md/description-routing-guidance", "warning", "Description should convey when to route to this agent — via <example> blocks, a routing-triggers block (<!-- BEGIN ROUTING TRIGGERS -->…<!-- END ROUTING TRIGGERS -->), or prose stating the triggers"));
|
|
120
151
|
}
|
|
121
152
|
}
|
|
122
153
|
// model
|
|
@@ -134,11 +165,15 @@ export const agentMdLinter = {
|
|
|
134
165
|
else if (!AGENT_COLORS.has(fm.data.color)) {
|
|
135
166
|
push(diag(config, filePath, "agent-md/color-valid", "warning", `"color" must be one of: ${[...AGENT_COLORS].join(", ")} (got "${fm.data.color}")`));
|
|
136
167
|
}
|
|
137
|
-
//
|
|
138
|
-
|
|
168
|
+
// Frontmatter keys: only flag cross-artifact misplacement. A key valid
|
|
169
|
+
// for a *different* markdown artifact (e.g. a skill-only key on an
|
|
170
|
+
// agent) gets an info; a key valid for no artifact stays silent.
|
|
171
|
+
// "name"/"color" are agent-valid but absent from the contract set.
|
|
172
|
+
const extraKnown = new Set(["name", "color"]);
|
|
139
173
|
for (const key of Object.keys(fm.data)) {
|
|
140
|
-
|
|
141
|
-
|
|
174
|
+
const cls = classifyUnknownFrontmatterKey(key, "agent", extraKnown);
|
|
175
|
+
if (cls?.kind === "owned-by-other" && cls.owner) {
|
|
176
|
+
push(diag(config, filePath, "agent-md/no-unknown-frontmatter", "info", `"${key}" is ${artifactLabel(cls.owner)} frontmatter — Claude Code ignores it on an agent`));
|
|
142
177
|
}
|
|
143
178
|
}
|
|
144
179
|
// system prompt (body)
|
|
@@ -50,10 +50,22 @@ export const claudeMdLinter = {
|
|
|
50
50
|
push(diag(config, filePath, "claude-md/user-level-concise", "info", `User-level CLAUDE.md is ${lines.length} lines — keep global rules concise, put project-specific content in project CLAUDE.md files`));
|
|
51
51
|
}
|
|
52
52
|
if (scope === "project") {
|
|
53
|
-
// Project CLAUDE.md should
|
|
54
|
-
|
|
53
|
+
// Project CLAUDE.md should describe the project. This is satisfied by
|
|
54
|
+
// either an explicit overview-style heading OR descriptive prose near
|
|
55
|
+
// the top — a CLAUDE.md that opens with a title followed by a sentence
|
|
56
|
+
// of project description does not need a literal "Overview" heading.
|
|
57
|
+
const hasOverviewHeading = lines.some((l) => /^#+ .*(overview|project|about|description|what this is|introduction|summary)/i.test(l));
|
|
58
|
+
// Descriptive opening prose: a non-heading, non-list, non-blank line
|
|
59
|
+
// of reasonable length appearing before the first H2 section.
|
|
60
|
+
const firstH2 = lines.findIndex((l) => /^## /.test(l));
|
|
61
|
+
const preambleEnd = firstH2 >= 0 ? firstH2 : lines.length;
|
|
62
|
+
const hasOpeningProse = lines.slice(0, preambleEnd).some((l) => {
|
|
63
|
+
const t = l.trim();
|
|
64
|
+
return t.length >= 25 && !t.startsWith("#") && !/^[-*>|]/.test(t);
|
|
65
|
+
});
|
|
66
|
+
const hasProjectOverview = hasOverviewHeading || hasOpeningProse;
|
|
55
67
|
if (!hasProjectOverview && h2Count > 0) {
|
|
56
|
-
push(diag(config, filePath, "claude-md/project-has-overview", "info", "Project CLAUDE.md should include a project overview section"));
|
|
68
|
+
push(diag(config, filePath, "claude-md/project-has-overview", "info", "Project CLAUDE.md should include a project overview — an \"Overview\" section or descriptive opening prose"));
|
|
57
69
|
}
|
|
58
70
|
}
|
|
59
71
|
// detect potential secrets
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TOOLS } from "../contracts.js";
|
|
2
|
+
import { formatAjvError, loadCommandFrontmatterSchema, summarizeErrors, } from "../plugin-schema.js";
|
|
2
3
|
import { isRuleEnabled, getRuleSeverity } from "../types.js";
|
|
3
4
|
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
5
|
+
import { artifactLabel, classifyUnknownFrontmatterKey, } from "../utils/frontmatter-keys.js";
|
|
4
6
|
const RULES = [
|
|
5
7
|
{ id: "command-md/valid-frontmatter", defaultSeverity: "error" },
|
|
8
|
+
{ id: "command-md/schema-valid", defaultSeverity: "error" },
|
|
6
9
|
{ id: "command-md/description-required", defaultSeverity: "error" },
|
|
7
10
|
{ id: "command-md/allowed-tools-valid", defaultSeverity: "warning" },
|
|
8
11
|
{ id: "command-md/body-present", defaultSeverity: "warning" },
|
|
@@ -31,6 +34,25 @@ export const commandMdLinter = {
|
|
|
31
34
|
push(diag(config, filePath, "command-md/valid-frontmatter", "error", fm.error ?? "Invalid frontmatter"));
|
|
32
35
|
return diagnostics;
|
|
33
36
|
}
|
|
37
|
+
// schema-valid — structural validation against the JSON Schema
|
|
38
|
+
// auto-extracted from Claude Code's command frontmatter Zod validator.
|
|
39
|
+
// The hand-written rules below add Claude-Code-specific advice (known
|
|
40
|
+
// tool names, body presence). The extracted schema is intentionally
|
|
41
|
+
// permissive about unknown frontmatter keys (Claude Code still loads the
|
|
42
|
+
// command), so those are NOT reported here — command-md/no-unknown-
|
|
43
|
+
// frontmatter handles them. Skipped silently if the schema bundle isn't
|
|
44
|
+
// shipped with this install.
|
|
45
|
+
if (isRuleEnabled(config, "command-md/schema-valid")) {
|
|
46
|
+
const compiled = loadCommandFrontmatterSchema();
|
|
47
|
+
if (compiled) {
|
|
48
|
+
const ok = compiled.validate(fm.data);
|
|
49
|
+
if (!ok && compiled.validate.errors) {
|
|
50
|
+
for (const err of summarizeErrors(compiled.validate.errors)) {
|
|
51
|
+
push(diag(config, filePath, "command-md/schema-valid", "error", formatAjvError(err)));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
34
56
|
// description
|
|
35
57
|
if (!("description" in fm.data) || typeof fm.data.description !== "string") {
|
|
36
58
|
push(diag(config, filePath, "command-md/description-required", "error", "\"description\" is required in frontmatter"));
|
|
@@ -46,11 +68,15 @@ export const commandMdLinter = {
|
|
|
46
68
|
}
|
|
47
69
|
}
|
|
48
70
|
}
|
|
49
|
-
//
|
|
50
|
-
|
|
71
|
+
// Frontmatter keys: only flag cross-artifact misplacement. A key valid
|
|
72
|
+
// for a *different* markdown artifact gets an info; a key valid for no
|
|
73
|
+
// artifact stays silent. The hyphenated "allowed-tools"/"argument-hint"
|
|
74
|
+
// are command-valid aliases of the camelCase contract keys.
|
|
75
|
+
const extraKnown = new Set(["allowed-tools", "argument-hint"]);
|
|
51
76
|
for (const key of Object.keys(fm.data)) {
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
const cls = classifyUnknownFrontmatterKey(key, "command", extraKnown);
|
|
78
|
+
if (cls?.kind === "owned-by-other" && cls.owner) {
|
|
79
|
+
push(diag(config, filePath, "command-md/no-unknown-frontmatter", "info", `"${key}" is ${artifactLabel(cls.owner)} frontmatter — Claude Code ignores it on a command`));
|
|
54
80
|
}
|
|
55
81
|
}
|
|
56
82
|
// body
|
package/dist/linters/mcp-json.js
CHANGED
|
@@ -2,6 +2,10 @@ import { basename } from "node:path";
|
|
|
2
2
|
import { MCP_SERVER_FIELDS } from "../contracts.js";
|
|
3
3
|
import { isRuleEnabled, getRuleSeverity } from "../types.js";
|
|
4
4
|
import { isKebabCase } from "../utils/kebab-case.js";
|
|
5
|
+
// Valid `type` values for a URL-based HTTP MCP server. Claude Code v2.1.146
|
|
6
|
+
// accepts both — its HTTP transport schema is
|
|
7
|
+
// `enum(["http","streamable-http"]).transform(()=>"http")`.
|
|
8
|
+
const HTTP_TRANSPORT_TYPES = new Set(["http", "streamable-http"]);
|
|
5
9
|
const RULES = [
|
|
6
10
|
{ id: "mcp-json/scope-file-name", defaultSeverity: "warning" },
|
|
7
11
|
{ id: "mcp-json/valid-json", defaultSeverity: "error" },
|
|
@@ -109,8 +113,11 @@ export const mcpJsonLinter = {
|
|
|
109
113
|
catch {
|
|
110
114
|
push(diag(config, filePath, "mcp-json/url-valid", "error", `Server "${name}" has invalid URL: "${url}"`, sp?.line, sp?.column));
|
|
111
115
|
}
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
// A URL-based HTTP server may declare "http" or "streamable-http".
|
|
117
|
+
// Claude Code v2.1.146 treats them identically — its HTTP transport
|
|
118
|
+
// schema is `enum(["http","streamable-http"]).transform(()=>"http")`.
|
|
119
|
+
if ("type" in server && !HTTP_TRANSPORT_TYPES.has(server.type)) {
|
|
120
|
+
push(diag(config, filePath, "mcp-json/type-matches-transport", "warning", `Server "${name}" has URL but type is "${server.type}" (expected one of ${[...HTTP_TRANSPORT_TYPES].join(", ")})`, sp?.line, sp?.column));
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
// stdio server checks
|