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.
@@ -1,18 +1,60 @@
1
1
  {
2
2
  "extractedFromClaudeCodeVersion": "2.1.146",
3
- "extractedAt": "2026-05-21T21:04:24.797Z",
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
@@ -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) {
@@ -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"));
@@ -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.
@@ -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
- const KEY_SETS = {
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 (KEY_SETS[self].has(key) || extraKnown.has(key))
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 (KEY_SETS[kind].has(key)) {
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.1",
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
  ],