claudecode-linter 2.1.148-patch.1 → 2.1.148-patch.2
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 +13 -0
- package/README.md +3 -3
- package/contracts/agent-frontmatter.schema.json +141 -1
- package/contracts/command-frontmatter.schema.json +127 -1
- package/contracts/hooks.schema.json +275 -0
- package/contracts/mcp.schema.json +302 -0
- package/contracts/settings.schema.json +285 -2
- package/contracts/skill-frontmatter.schema.json +197 -1
- package/dist/contracts.js +13 -0
- package/dist/linters/agent-md.js +29 -1
- package/dist/linters/command-md.js +37 -1
- package/dist/linters/hooks-json.js +24 -0
- package/dist/linters/mcp-json.js +24 -0
- package/dist/linters/skill-md.js +41 -0
- package/dist/plugin-schema.js +7 -0
- package/dist/utils/effort.js +29 -0
- package/dist/utils/frontmatter-keys.js +32 -3
- package/package.json +3 -1
|
@@ -1,18 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extractedFromClaudeCodeVersion": "2.1.146",
|
|
3
|
-
"extractedAt": "2026-05-
|
|
3
|
+
"extractedAt": "2026-05-22T08:49:59.762Z",
|
|
4
4
|
"schema": {
|
|
5
5
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
6
6
|
"title": "Claude Code SKILL.md frontmatter",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"properties": {
|
|
9
9
|
"name": {
|
|
10
|
+
"anyOf": [
|
|
11
|
+
{
|
|
12
|
+
"type": "string"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"type": "number"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"type": "boolean"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"type": "null"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
10
24
|
"description": "Display name. Defaults to the filename without extension."
|
|
11
25
|
},
|
|
12
26
|
"description": {
|
|
27
|
+
"anyOf": [
|
|
28
|
+
{
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "number"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"type": "boolean"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"type": "null"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
13
41
|
"description": "One-line summary shown in listings and the Skill tool."
|
|
14
42
|
},
|
|
15
43
|
"model": {
|
|
44
|
+
"anyOf": [
|
|
45
|
+
{
|
|
46
|
+
"type": "string"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"type": "number"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "boolean"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"type": "null"
|
|
56
|
+
}
|
|
57
|
+
],
|
|
16
58
|
"description": "Model override (`haiku`, `sonnet`, `opus`, or a full ID). Use `inherit` to match the parent conversation."
|
|
17
59
|
},
|
|
18
60
|
"allowed-tools": {
|
|
@@ -43,6 +85,20 @@
|
|
|
43
85
|
"description": "Tools available to the model while this file is active. Comma-separated string or YAML list."
|
|
44
86
|
},
|
|
45
87
|
"argument-hint": {
|
|
88
|
+
"anyOf": [
|
|
89
|
+
{
|
|
90
|
+
"type": "string"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"type": "number"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"type": "boolean"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"type": "null"
|
|
100
|
+
}
|
|
101
|
+
],
|
|
46
102
|
"description": "Placeholder text shown after the slash command name."
|
|
47
103
|
},
|
|
48
104
|
"arguments": {
|
|
@@ -73,21 +129,105 @@
|
|
|
73
129
|
"description": "@internal — typed variant of argument-hint; argument-hint is the documented form"
|
|
74
130
|
},
|
|
75
131
|
"disable-model-invocation": {
|
|
132
|
+
"anyOf": [
|
|
133
|
+
{
|
|
134
|
+
"type": "string"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"type": "number"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"type": "boolean"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"type": "null"
|
|
144
|
+
}
|
|
145
|
+
],
|
|
76
146
|
"description": "If true, the model cannot invoke this via the Skill tool; only users can type the slash command."
|
|
77
147
|
},
|
|
78
148
|
"user-invocable": {
|
|
149
|
+
"anyOf": [
|
|
150
|
+
{
|
|
151
|
+
"type": "string"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"type": "number"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"type": "boolean"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"type": "null"
|
|
161
|
+
}
|
|
162
|
+
],
|
|
79
163
|
"description": "If false, hides the slash command from users; only the model can invoke it via the Skill tool."
|
|
80
164
|
},
|
|
81
165
|
"effort": {
|
|
166
|
+
"anyOf": [
|
|
167
|
+
{
|
|
168
|
+
"type": "string"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"type": "number"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"type": "boolean"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"type": "null"
|
|
178
|
+
}
|
|
179
|
+
],
|
|
82
180
|
"description": "Thinking effort for the model: `low`, `medium`, `high`, `max`, or an integer."
|
|
83
181
|
},
|
|
84
182
|
"shell": {
|
|
183
|
+
"anyOf": [
|
|
184
|
+
{
|
|
185
|
+
"type": "string"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"type": "number"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"type": "boolean"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"type": "null"
|
|
195
|
+
}
|
|
196
|
+
],
|
|
85
197
|
"description": "Shell for `!`-command blocks: `bash` or `powershell`. Defaults to bash regardless of platform."
|
|
86
198
|
},
|
|
87
199
|
"version": {
|
|
200
|
+
"anyOf": [
|
|
201
|
+
{
|
|
202
|
+
"type": "string"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"type": "number"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"type": "boolean"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"type": "null"
|
|
212
|
+
}
|
|
213
|
+
],
|
|
88
214
|
"description": "@internal — bookkeeping, not surfaced to users"
|
|
89
215
|
},
|
|
90
216
|
"when_to_use": {
|
|
217
|
+
"anyOf": [
|
|
218
|
+
{
|
|
219
|
+
"type": "string"
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
"type": "number"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"type": "boolean"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"type": "null"
|
|
229
|
+
}
|
|
230
|
+
],
|
|
91
231
|
"description": "Guidance for when the model should reach for this skill. Becomes part of the tool description."
|
|
92
232
|
},
|
|
93
233
|
"paths": {
|
|
@@ -129,15 +269,71 @@
|
|
|
129
269
|
"description": "Where the skill runs: `inline` expands into the current conversation; `fork` spawns a subagent."
|
|
130
270
|
},
|
|
131
271
|
"agent": {
|
|
272
|
+
"anyOf": [
|
|
273
|
+
{
|
|
274
|
+
"type": "string"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
"type": "number"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"type": "boolean"
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"type": "null"
|
|
284
|
+
}
|
|
285
|
+
],
|
|
132
286
|
"description": "Agent type to spawn when `context: fork`."
|
|
133
287
|
},
|
|
134
288
|
"fallback": {
|
|
289
|
+
"anyOf": [
|
|
290
|
+
{
|
|
291
|
+
"type": "string"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"type": "number"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"type": "boolean"
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"type": "null"
|
|
301
|
+
}
|
|
302
|
+
],
|
|
135
303
|
"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
304
|
},
|
|
137
305
|
"created_by": {
|
|
306
|
+
"anyOf": [
|
|
307
|
+
{
|
|
308
|
+
"type": "string"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"type": "number"
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"type": "boolean"
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"type": "null"
|
|
318
|
+
}
|
|
319
|
+
],
|
|
138
320
|
"description": "@internal — provenance marker (e.g. dream-proposal)"
|
|
139
321
|
},
|
|
140
322
|
"improved_by": {
|
|
323
|
+
"anyOf": [
|
|
324
|
+
{
|
|
325
|
+
"type": "string"
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"type": "number"
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"type": "boolean"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"type": "null"
|
|
335
|
+
}
|
|
336
|
+
],
|
|
141
337
|
"description": "@internal — provenance marker (e.g. dream-proposal)"
|
|
142
338
|
},
|
|
143
339
|
"mcpServers": {
|
package/dist/contracts.js
CHANGED
|
@@ -389,4 +389,17 @@ export const PLUGIN_SUBAGENT_BLOCKED_TOOLS = new Set([
|
|
|
389
389
|
"Glob",
|
|
390
390
|
"Grep",
|
|
391
391
|
]);
|
|
392
|
+
// Hand-curated named values for the frontmatter `effort` field. The Zod
|
|
393
|
+
// schema types `effort` as a permissive scalar; the field's own describe()
|
|
394
|
+
// string in the Claude Code bundle reads: "Thinking effort for the model:
|
|
395
|
+
// `low`, `medium`, `high`, `max`, or an integer." — so a string `effort`
|
|
396
|
+
// must be one of these, and a numeric `effort` must be an integer. (The
|
|
397
|
+
// runtime effortLevel enum also has `xhigh`, but the frontmatter describe
|
|
398
|
+
// string deliberately omits it; we follow the frontmatter contract.)
|
|
399
|
+
export const EFFORT_LEVELS = new Set([
|
|
400
|
+
"low",
|
|
401
|
+
"medium",
|
|
402
|
+
"high",
|
|
403
|
+
"max",
|
|
404
|
+
]);
|
|
392
405
|
//# sourceMappingURL=contracts.js.map
|
package/dist/linters/agent-md.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { AGENT_MODELS, AGENT_COLORS, TOOLS, PLUGIN_SUBAGENT_BLOCKED_TOOLS, } from "../contracts.js";
|
|
3
|
+
import { AGENT_MODELS, AGENT_COLORS, TOOLS, PERMISSION_MODES, PLUGIN_SUBAGENT_BLOCKED_TOOLS, } from "../contracts.js";
|
|
4
|
+
import { invalidEffortReason } from "../utils/effort.js";
|
|
4
5
|
import { formatAjvError, loadAgentFrontmatterSchema, summarizeErrors, } from "../plugin-schema.js";
|
|
5
6
|
import { isRuleEnabled, getRuleSeverity } from "../types.js";
|
|
6
7
|
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
@@ -65,6 +66,9 @@ const RULES = [
|
|
|
65
66
|
{ id: "agent-md/description-routing-guidance", defaultSeverity: "warning" },
|
|
66
67
|
{ id: "agent-md/model-required", defaultSeverity: "error" },
|
|
67
68
|
{ id: "agent-md/model-valid", defaultSeverity: "warning" },
|
|
69
|
+
{ id: "agent-md/permission-mode-valid", defaultSeverity: "warning" },
|
|
70
|
+
{ id: "agent-md/effort-valid", defaultSeverity: "warning" },
|
|
71
|
+
{ id: "agent-md/max-turns-valid", defaultSeverity: "warning" },
|
|
68
72
|
{ id: "agent-md/color-required", defaultSeverity: "warning" },
|
|
69
73
|
{ id: "agent-md/color-valid", defaultSeverity: "warning" },
|
|
70
74
|
{ id: "agent-md/system-prompt-present", defaultSeverity: "error" },
|
|
@@ -158,6 +162,30 @@ export const agentMdLinter = {
|
|
|
158
162
|
!fm.data.model.startsWith("claude-")) {
|
|
159
163
|
push(diag(config, filePath, "agent-md/model-valid", "warning", `"model" must be one of: ${[...AGENT_MODELS].join(", ")} or a versioned claude-* model ID (got "${fm.data.model}")`));
|
|
160
164
|
}
|
|
165
|
+
// permissionMode — typed as a permissive scalar by the Zod schema;
|
|
166
|
+
// at runtime only the PERMISSION_MODES values take effect.
|
|
167
|
+
if ("permissionMode" in fm.data) {
|
|
168
|
+
const pm = fm.data.permissionMode;
|
|
169
|
+
if (typeof pm !== "string" || !PERMISSION_MODES.has(pm)) {
|
|
170
|
+
push(diag(config, filePath, "agent-md/permission-mode-valid", "warning", `"permissionMode" must be one of: ${[...PERMISSION_MODES].join(", ")} (got ${JSON.stringify(pm)})`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// effort — Zod types it as a permissive scalar; the field's describe()
|
|
174
|
+
// string restricts it to a named level or an integer.
|
|
175
|
+
if ("effort" in fm.data) {
|
|
176
|
+
const reason = invalidEffortReason(fm.data.effort);
|
|
177
|
+
if (reason) {
|
|
178
|
+
push(diag(config, filePath, "agent-md/effort-valid", "warning", reason));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// maxTurns — Zod accepts a string / float; only a positive integer
|
|
182
|
+
// is a meaningful turn budget.
|
|
183
|
+
if ("maxTurns" in fm.data) {
|
|
184
|
+
const mt = fm.data.maxTurns;
|
|
185
|
+
if (typeof mt !== "number" || !Number.isInteger(mt) || mt <= 0) {
|
|
186
|
+
push(diag(config, filePath, "agent-md/max-turns-valid", "warning", `"maxTurns" must be a positive integer (got ${JSON.stringify(mt)})`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
161
189
|
// color
|
|
162
190
|
if (!("color" in fm.data) || typeof fm.data.color !== "string") {
|
|
163
191
|
push(diag(config, filePath, "agent-md/color-required", "warning", '"color" is required in frontmatter'));
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { TOOLS } from "../contracts.js";
|
|
1
|
+
import { AGENT_MODELS, TOOLS } from "../contracts.js";
|
|
2
2
|
import { formatAjvError, loadCommandFrontmatterSchema, summarizeErrors, } from "../plugin-schema.js";
|
|
3
3
|
import { isRuleEnabled, getRuleSeverity } from "../types.js";
|
|
4
|
+
import { invalidEffortReason } from "../utils/effort.js";
|
|
4
5
|
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
5
6
|
import { artifactLabel, classifyUnknownFrontmatterKey, } from "../utils/frontmatter-keys.js";
|
|
6
7
|
const RULES = [
|
|
7
8
|
{ id: "command-md/valid-frontmatter", defaultSeverity: "error" },
|
|
8
9
|
{ id: "command-md/schema-valid", defaultSeverity: "error" },
|
|
9
10
|
{ id: "command-md/description-required", defaultSeverity: "error" },
|
|
11
|
+
{ id: "command-md/name-format", defaultSeverity: "warning" },
|
|
12
|
+
{ id: "command-md/model-valid", defaultSeverity: "warning" },
|
|
13
|
+
{ id: "command-md/effort-valid", defaultSeverity: "warning" },
|
|
14
|
+
{ id: "command-md/frontmatter-field-type", defaultSeverity: "warning" },
|
|
10
15
|
{ id: "command-md/allowed-tools-valid", defaultSeverity: "warning" },
|
|
11
16
|
{ id: "command-md/body-present", defaultSeverity: "warning" },
|
|
12
17
|
{ id: "command-md/no-unknown-frontmatter", defaultSeverity: "info" },
|
|
@@ -57,6 +62,37 @@ export const commandMdLinter = {
|
|
|
57
62
|
if (!("description" in fm.data) || typeof fm.data.description !== "string") {
|
|
58
63
|
push(diag(config, filePath, "command-md/description-required", "error", "\"description\" is required in frontmatter"));
|
|
59
64
|
}
|
|
65
|
+
// name — optional display-name override. Zod types it as a permissive
|
|
66
|
+
// scalar; if present it must be a non-empty string.
|
|
67
|
+
if ("name" in fm.data) {
|
|
68
|
+
const name = fm.data.name;
|
|
69
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
70
|
+
push(diag(config, filePath, "command-md/name-format", "warning", `"name" must be a non-empty string (got ${JSON.stringify(name)})`));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// model — Zod types it as a permissive scalar; accept a named alias or a
|
|
74
|
+
// versioned claude-* model id. Mirrors agent-md/model-valid.
|
|
75
|
+
if ("model" in fm.data && typeof fm.data.model === "string") {
|
|
76
|
+
const model = fm.data.model;
|
|
77
|
+
if (!AGENT_MODELS.has(model) && !model.startsWith("claude-")) {
|
|
78
|
+
push(diag(config, filePath, "command-md/model-valid", "warning", `"model" must be one of: ${[...AGENT_MODELS].join(", ")} or a versioned claude-* model ID (got "${model}")`));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// effort — Zod types it as a permissive scalar; the field's describe()
|
|
82
|
+
// string restricts it to a named level or an integer.
|
|
83
|
+
if ("effort" in fm.data) {
|
|
84
|
+
const reason = invalidEffortReason(fm.data.effort);
|
|
85
|
+
if (reason) {
|
|
86
|
+
push(diag(config, filePath, "command-md/effort-valid", "warning", reason));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// boolean fields — the Zod schema accepts the string "true"; only a real
|
|
90
|
+
// boolean behaves as expected.
|
|
91
|
+
for (const field of ["disable-model-invocation", "user-invocable"]) {
|
|
92
|
+
if (field in fm.data && typeof fm.data[field] !== "boolean") {
|
|
93
|
+
push(diag(config, filePath, "command-md/frontmatter-field-type", "warning", `"${field}" must be a boolean (got ${JSON.stringify(fm.data[field])})`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
60
96
|
// allowed-tools
|
|
61
97
|
if ("allowed-tools" in fm.data) {
|
|
62
98
|
const tools = fm.data["allowed-tools"];
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { HOOK_EVENTS, HOOK_TYPES, PROMPT_EVENTS } from "../contracts.js";
|
|
2
|
+
import { formatAjvError, loadHooksJsonSchema, summarizeErrors, } from "../plugin-schema.js";
|
|
2
3
|
import { isRuleEnabled, getRuleSeverity } from "../types.js";
|
|
3
4
|
const RULES = [
|
|
4
5
|
{ id: "hooks-json/valid-json", defaultSeverity: "error" },
|
|
6
|
+
{ id: "hooks-json/schema-valid", defaultSeverity: "error" },
|
|
5
7
|
{ id: "hooks-json/root-hooks-key", defaultSeverity: "error" },
|
|
6
8
|
{ id: "hooks-json/valid-event-names", defaultSeverity: "error" },
|
|
7
9
|
{ id: "hooks-json/hook-type-required", defaultSeverity: "error" },
|
|
@@ -52,6 +54,28 @@ export const hooksJsonLinter = {
|
|
|
52
54
|
push(diag(config, filePath, "hooks-json/valid-json", "error", "hooks.json must be a JSON object"));
|
|
53
55
|
return diagnostics;
|
|
54
56
|
}
|
|
57
|
+
// schema-valid — structural validation against the JSON Schema
|
|
58
|
+
// auto-extracted from Claude Code's hooks-config Zod validator (the
|
|
59
|
+
// hook-event → matcher-array map, each matcher holding a discriminated
|
|
60
|
+
// union of hook definitions). The hand-written rules below add
|
|
61
|
+
// Claude-Code-specific advice the schema can't express (hardcoded-path
|
|
62
|
+
// warnings, prompt-event-support hints, …). The extracted schema is
|
|
63
|
+
// intentionally permissive about unknown hook fields (Claude Code's hook
|
|
64
|
+
// objects are not .strict()). Skipped silently if the schema bundle isn't
|
|
65
|
+
// shipped with this install.
|
|
66
|
+
if (isRuleEnabled(config, "hooks-json/schema-valid")) {
|
|
67
|
+
const compiled = loadHooksJsonSchema();
|
|
68
|
+
if (compiled) {
|
|
69
|
+
const ok = compiled.validate(parsed);
|
|
70
|
+
if (!ok && compiled.validate.errors) {
|
|
71
|
+
for (const err of summarizeErrors(compiled.validate.errors)) {
|
|
72
|
+
const firstSeg = err.instancePath.split("/").filter(Boolean)[0];
|
|
73
|
+
const p = firstSeg ? findKeyPosition(content, firstSeg) : undefined;
|
|
74
|
+
push(diag(config, filePath, "hooks-json/schema-valid", "error", formatAjvError(err), p?.line, p?.column));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
55
79
|
const root = parsed;
|
|
56
80
|
// root "hooks" key
|
|
57
81
|
if (!("hooks" in root) || typeof root.hooks !== "object" || root.hooks === null) {
|
package/dist/linters/mcp-json.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
import { MCP_SERVER_FIELDS } from "../contracts.js";
|
|
3
|
+
import { formatAjvError, loadMcpJsonSchema, summarizeErrors, } from "../plugin-schema.js";
|
|
3
4
|
import { isRuleEnabled, getRuleSeverity } from "../types.js";
|
|
4
5
|
import { isKebabCase } from "../utils/kebab-case.js";
|
|
5
6
|
// Valid `type` values for a URL-based HTTP MCP server. Claude Code v2.1.146
|
|
@@ -9,6 +10,7 @@ const HTTP_TRANSPORT_TYPES = new Set(["http", "streamable-http"]);
|
|
|
9
10
|
const RULES = [
|
|
10
11
|
{ id: "mcp-json/scope-file-name", defaultSeverity: "warning" },
|
|
11
12
|
{ id: "mcp-json/valid-json", defaultSeverity: "error" },
|
|
13
|
+
{ id: "mcp-json/schema-valid", defaultSeverity: "error" },
|
|
12
14
|
{ id: "mcp-json/servers-required", defaultSeverity: "error" },
|
|
13
15
|
{ id: "mcp-json/servers-object", defaultSeverity: "error" },
|
|
14
16
|
{ id: "mcp-json/server-name-kebab", defaultSeverity: "info" },
|
|
@@ -73,6 +75,28 @@ export const mcpJsonLinter = {
|
|
|
73
75
|
push(diag(config, filePath, "mcp-json/valid-json", "error", "mcp.json must be a JSON object"));
|
|
74
76
|
return diagnostics;
|
|
75
77
|
}
|
|
78
|
+
// schema-valid — structural validation against the JSON Schema
|
|
79
|
+
// auto-extracted from Claude Code's .mcp.json Zod validator (the
|
|
80
|
+
// `mcpServers` record of discriminated transport configs). The hand-written
|
|
81
|
+
// rules below add Claude-Code-specific advice the schema can't express
|
|
82
|
+
// (kebab-case names, ${CLAUDE_PLUGIN_ROOT} hints, …). The extracted schema
|
|
83
|
+
// is intentionally permissive about unknown server fields (Claude Code's
|
|
84
|
+
// server union is not .strict()), so those are NOT reported here —
|
|
85
|
+
// mcp-json/no-unknown-server-fields handles them. Skipped silently if the
|
|
86
|
+
// schema bundle isn't shipped with this install.
|
|
87
|
+
if (isRuleEnabled(config, "mcp-json/schema-valid")) {
|
|
88
|
+
const compiled = loadMcpJsonSchema();
|
|
89
|
+
if (compiled) {
|
|
90
|
+
const ok = compiled.validate(parsed);
|
|
91
|
+
if (!ok && compiled.validate.errors) {
|
|
92
|
+
for (const err of summarizeErrors(compiled.validate.errors)) {
|
|
93
|
+
const firstSeg = err.instancePath.split("/").filter(Boolean)[0];
|
|
94
|
+
const p = firstSeg ? findKeyPosition(content, firstSeg) : undefined;
|
|
95
|
+
push(diag(config, filePath, "mcp-json/schema-valid", "error", formatAjvError(err), p?.line, p?.column));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
76
100
|
// mcpServers required
|
|
77
101
|
if (!("mcpServers" in parsed)) {
|
|
78
102
|
push(diag(config, filePath, "mcp-json/servers-required", "error", "\"mcpServers\" field is required"));
|
package/dist/linters/skill-md.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { AGENT_MODELS, TOOLS } from "../contracts.js";
|
|
1
2
|
import { formatAjvError, loadSkillFrontmatterSchema, summarizeErrors, } from "../plugin-schema.js";
|
|
2
3
|
import { isRuleEnabled, getRuleSeverity } from "../types.js";
|
|
4
|
+
import { invalidEffortReason } from "../utils/effort.js";
|
|
3
5
|
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
4
6
|
import { artifactLabel, classifyUnknownFrontmatterKey, } from "../utils/frontmatter-keys.js";
|
|
5
7
|
import { isKebabCase } from "../utils/kebab-case.js";
|
|
@@ -13,6 +15,10 @@ const RULES = [
|
|
|
13
15
|
{ id: "skill-md/description-max-length", defaultSeverity: "error" },
|
|
14
16
|
{ id: "skill-md/description-no-angle-brackets", defaultSeverity: "error" },
|
|
15
17
|
{ id: "skill-md/description-trigger-phrases", defaultSeverity: "warning" },
|
|
18
|
+
{ id: "skill-md/model-valid", defaultSeverity: "warning" },
|
|
19
|
+
{ id: "skill-md/effort-valid", defaultSeverity: "warning" },
|
|
20
|
+
{ id: "skill-md/allowed-tools-valid", defaultSeverity: "warning" },
|
|
21
|
+
{ id: "skill-md/frontmatter-field-type", defaultSeverity: "warning" },
|
|
16
22
|
{ id: "skill-md/no-unknown-frontmatter", defaultSeverity: "info" },
|
|
17
23
|
{ id: "skill-md/body-word-count", defaultSeverity: "info" },
|
|
18
24
|
{ id: "skill-md/body-has-headers", defaultSeverity: "info" },
|
|
@@ -151,6 +157,41 @@ export const skillMdLinter = {
|
|
|
151
157
|
"(e.g., \"Use when…\", \"Trigger on…\", \"when the user asks to…\", or an imperative verb)"));
|
|
152
158
|
}
|
|
153
159
|
}
|
|
160
|
+
// model — Zod types it as a permissive scalar; accept a named alias or a
|
|
161
|
+
// versioned claude-* model id. Mirrors agent-md/model-valid.
|
|
162
|
+
if ("model" in fm.data && typeof fm.data.model === "string") {
|
|
163
|
+
const model = fm.data.model;
|
|
164
|
+
if (!AGENT_MODELS.has(model) && !model.startsWith("claude-")) {
|
|
165
|
+
push(diag(config, filePath, "skill-md/model-valid", "warning", `"model" must be one of: ${[...AGENT_MODELS].join(", ")} or a versioned claude-* model ID (got "${model}")`));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// effort — Zod types it as a permissive scalar; the field's describe()
|
|
169
|
+
// string restricts it to a named level or an integer.
|
|
170
|
+
if ("effort" in fm.data) {
|
|
171
|
+
const reason = invalidEffortReason(fm.data.effort);
|
|
172
|
+
if (reason) {
|
|
173
|
+
push(diag(config, filePath, "skill-md/effort-valid", "warning", reason));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// allowed-tools — validate built-in tool names. mcp__* patterns are
|
|
177
|
+
// dynamic (resolved at runtime); accept them. Mirrors command-md.
|
|
178
|
+
if ("allowed-tools" in fm.data) {
|
|
179
|
+
const tools = fm.data["allowed-tools"];
|
|
180
|
+
if (Array.isArray(tools)) {
|
|
181
|
+
for (const t of tools) {
|
|
182
|
+
if (typeof t === "string" && !t.startsWith("mcp__") && !TOOLS.has(t)) {
|
|
183
|
+
push(diag(config, filePath, "skill-md/allowed-tools-valid", "warning", `Unknown tool "${t}" in allowed-tools`));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// boolean fields — the Zod schema accepts the string "true"; only a real
|
|
189
|
+
// boolean behaves as expected.
|
|
190
|
+
for (const field of ["disable-model-invocation", "user-invocable"]) {
|
|
191
|
+
if (field in fm.data && typeof fm.data[field] !== "boolean") {
|
|
192
|
+
push(diag(config, filePath, "skill-md/frontmatter-field-type", "warning", `"${field}" must be a boolean (got ${JSON.stringify(fm.data[field])})`));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
154
195
|
// Frontmatter keys: only flag cross-artifact misplacement. A key that is
|
|
155
196
|
// valid for a *different* markdown artifact (e.g. agent's "effort" on a
|
|
156
197
|
// skill) gets an info; a key valid for no artifact stays silent.
|
package/dist/plugin-schema.js
CHANGED
|
@@ -52,6 +52,7 @@ function loadCompiledSchema(fileName) {
|
|
|
52
52
|
const compiled = {
|
|
53
53
|
validate: getAjv().compile(wrapped.schema),
|
|
54
54
|
extractedFromVersion: wrapped.extractedFromClaudeCodeVersion,
|
|
55
|
+
knownFields: new Set(Object.keys(wrapped.schema.properties ?? {})),
|
|
55
56
|
};
|
|
56
57
|
compiledCache.set(fileName, compiled);
|
|
57
58
|
return compiled;
|
|
@@ -65,6 +66,12 @@ export function loadMonitorsSchema() {
|
|
|
65
66
|
export function loadSettingsSchema() {
|
|
66
67
|
return loadCompiledSchema("settings.schema.json");
|
|
67
68
|
}
|
|
69
|
+
export function loadMcpJsonSchema() {
|
|
70
|
+
return loadCompiledSchema("mcp.schema.json");
|
|
71
|
+
}
|
|
72
|
+
export function loadHooksJsonSchema() {
|
|
73
|
+
return loadCompiledSchema("hooks.schema.json");
|
|
74
|
+
}
|
|
68
75
|
export function loadSkillFrontmatterSchema() {
|
|
69
76
|
return loadCompiledSchema("skill-frontmatter.schema.json");
|
|
70
77
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { EFFORT_LEVELS } from "../contracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Validate a frontmatter `effort` value against the Claude Code contract.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code's frontmatter Zod schema types `effort` as a permissive scalar
|
|
6
|
+
* (`anyOf[string,number,boolean,null]`), but the field's own `.describe()`
|
|
7
|
+
* string in the bundle reads:
|
|
8
|
+
* "Thinking effort for the model: `low`, `medium`, `high`, `max`, or an integer."
|
|
9
|
+
*
|
|
10
|
+
* So a valid `effort` is either one of the named levels (EFFORT_LEVELS) or an
|
|
11
|
+
* integer. Anything else — a non-integer number, a boolean, an unknown string,
|
|
12
|
+
* null — is reported. Returns a human-readable reason when invalid, or null
|
|
13
|
+
* when the value is acceptable.
|
|
14
|
+
*/
|
|
15
|
+
export function invalidEffortReason(value) {
|
|
16
|
+
const valid = [...EFFORT_LEVELS].join(", ");
|
|
17
|
+
if (typeof value === "string") {
|
|
18
|
+
if (EFFORT_LEVELS.has(value))
|
|
19
|
+
return null;
|
|
20
|
+
return `"effort" string must be one of: ${valid} (got "${value}")`;
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "number") {
|
|
23
|
+
if (Number.isInteger(value))
|
|
24
|
+
return null;
|
|
25
|
+
return `"effort" number must be an integer (got ${value})`;
|
|
26
|
+
}
|
|
27
|
+
return `"effort" must be one of: ${valid}, or an integer (got ${JSON.stringify(value)})`;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=effort.js.map
|
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
import { SKILL_FRONTMATTER, AGENT_FRONTMATTER, COMMAND_FRONTMATTER, } from "../contracts.js";
|
|
2
|
-
|
|
2
|
+
import { loadSkillFrontmatterSchema, loadAgentFrontmatterSchema, loadCommandFrontmatterSchema, } from "../plugin-schema.js";
|
|
3
|
+
// Census-extraction sets (scripts/extract-contracts.ts). Used only as a
|
|
4
|
+
// fallback when the auto-extracted frontmatter schema is unavailable — the
|
|
5
|
+
// census extractor lags the schema walker (e.g. it was missing `effort`).
|
|
6
|
+
const CENSUS_FALLBACK = {
|
|
3
7
|
skill: SKILL_FRONTMATTER,
|
|
4
8
|
agent: AGENT_FRONTMATTER,
|
|
5
9
|
command: COMMAND_FRONTMATTER,
|
|
6
10
|
};
|
|
11
|
+
const SCHEMA_LOADERS = {
|
|
12
|
+
skill: loadSkillFrontmatterSchema,
|
|
13
|
+
agent: loadAgentFrontmatterSchema,
|
|
14
|
+
command: loadCommandFrontmatterSchema,
|
|
15
|
+
};
|
|
16
|
+
const keyCache = new Map();
|
|
17
|
+
/**
|
|
18
|
+
* Known frontmatter keys for an artifact — the union of the auto-extracted
|
|
19
|
+
* schema's property names (authoritative, complete, kept in sync with Claude
|
|
20
|
+
* Code's Zod) and the census-contract set. Union rather than schema-only so a
|
|
21
|
+
* key known to *either* extraction is never mis-flagged: `no-unknown-
|
|
22
|
+
* frontmatter` is advisory and a false positive is the only real harm.
|
|
23
|
+
*/
|
|
24
|
+
function knownKeys(kind) {
|
|
25
|
+
const cached = keyCache.get(kind);
|
|
26
|
+
if (cached)
|
|
27
|
+
return cached;
|
|
28
|
+
const merged = new Set(CENSUS_FALLBACK[kind]);
|
|
29
|
+
const schema = SCHEMA_LOADERS[kind]();
|
|
30
|
+
if (schema)
|
|
31
|
+
for (const k of schema.knownFields)
|
|
32
|
+
merged.add(k);
|
|
33
|
+
keyCache.set(kind, merged);
|
|
34
|
+
return merged;
|
|
35
|
+
}
|
|
7
36
|
const ARTIFACT_LABEL = {
|
|
8
37
|
skill: "skill",
|
|
9
38
|
agent: "agent",
|
|
@@ -18,13 +47,13 @@ const ARTIFACT_LABEL = {
|
|
|
18
47
|
*/
|
|
19
48
|
export function classifyUnknownFrontmatterKey(key, self, extraKnown = new Set()) {
|
|
20
49
|
// Known for the current artifact — not unknown at all.
|
|
21
|
-
if (
|
|
50
|
+
if (knownKeys(self).has(key) || extraKnown.has(key))
|
|
22
51
|
return null;
|
|
23
52
|
// Valid for some *other* markdown artifact → misplacement.
|
|
24
53
|
for (const kind of ["skill", "agent", "command"]) {
|
|
25
54
|
if (kind === self)
|
|
26
55
|
continue;
|
|
27
|
-
if (
|
|
56
|
+
if (knownKeys(kind).has(key)) {
|
|
28
57
|
return { kind: "owned-by-other", owner: kind };
|
|
29
58
|
}
|
|
30
59
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudecode-linter",
|
|
3
|
-
"version": "2.1.148-patch.
|
|
3
|
+
"version": "2.1.148-patch.2",
|
|
4
4
|
"description": "Standalone linter for Claude Code plugins and configuration files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
"contracts/skill-frontmatter.schema.json",
|
|
31
31
|
"contracts/agent-frontmatter.schema.json",
|
|
32
32
|
"contracts/command-frontmatter.schema.json",
|
|
33
|
+
"contracts/mcp.schema.json",
|
|
34
|
+
"contracts/hooks.schema.json",
|
|
33
35
|
".claudecode-lint.defaults.yaml",
|
|
34
36
|
"README.md"
|
|
35
37
|
],
|