claudecode-linter 2.1.148 → 2.1.150-patch.1

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 (37) hide show
  1. package/.claudecode-lint.defaults.yaml +21 -2
  2. package/README.md +90 -3
  3. package/contracts/agent-frontmatter.schema.json +142 -2
  4. package/contracts/command-frontmatter.schema.json +128 -2
  5. package/contracts/hooks.schema.json +275 -0
  6. package/contracts/lsp.schema.json +2 -2
  7. package/contracts/mcp.schema.json +326 -0
  8. package/contracts/monitors.schema.json +3 -3
  9. package/contracts/plugin.schema.json +672 -22
  10. package/contracts/schemastore/keybindings.schema.json +179 -0
  11. package/contracts/schemastore/manifest.json +25 -0
  12. package/contracts/schemastore/marketplace.schema.json +2067 -0
  13. package/contracts/schemastore/plugin-manifest.schema.json +1834 -0
  14. package/contracts/schemastore/settings.schema.json +3062 -0
  15. package/contracts/settings.schema.json +333 -4
  16. package/contracts/skill-frontmatter.schema.json +198 -2
  17. package/dist/config.js +13 -8
  18. package/dist/contracts.js +20 -1
  19. package/dist/discovery.js +23 -0
  20. package/dist/formatters/human.js +3 -2
  21. package/dist/index.js +80 -12
  22. package/dist/linters/agent-md.js +44 -7
  23. package/dist/linters/command-md.js +37 -1
  24. package/dist/linters/hooks-json.js +24 -0
  25. package/dist/linters/keybindings-json.js +53 -0
  26. package/dist/linters/marketplace-json.js +55 -0
  27. package/dist/linters/mcp-json.js +24 -0
  28. package/dist/linters/settings-json.js +42 -15
  29. package/dist/linters/skill-md.js +48 -1
  30. package/dist/plugin-schema.js +96 -39
  31. package/dist/utils/asset-path.js +56 -0
  32. package/dist/utils/effort.js +29 -0
  33. package/dist/utils/frontmatter-keys.js +32 -3
  34. package/dist/utils/frontmatter.js +1 -1
  35. package/dist/utils/safe-write.js +36 -0
  36. package/dist/utils/terminal.js +18 -0
  37. package/package.json +6 -2
package/dist/contracts.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Auto-generated from contracts/claude-code-contracts.json
2
- // Claude Code v2.1.148 — extracted 2026-05-22T07:13:45.210Z
2
+ // Claude Code v2.1.150 — extracted 2026-05-23T07:05:23.158Z
3
3
  // Do not edit manually. Run: npm run generate-contracts
4
4
  export const TOOLS = new Set([
5
5
  "Agent",
@@ -23,9 +23,11 @@ export const TOOLS = new Set([
23
23
  "NotebookEdit",
24
24
  "NotebookRead",
25
25
  "PushNotification",
26
+ "REPL",
26
27
  "Read",
27
28
  "ReadMcpResource",
28
29
  "RemoteTrigger",
30
+ "ScheduleWakeup",
29
31
  "SendMessage",
30
32
  "Skill",
31
33
  "SubscribeMcpResource",
@@ -44,6 +46,7 @@ export const TOOLS = new Set([
44
46
  "UnsubscribePolling",
45
47
  "WebFetch",
46
48
  "WebSearch",
49
+ "Workflow",
47
50
  "Write",
48
51
  ]);
49
52
  export const HOOK_EVENTS = new Set([
@@ -207,6 +210,7 @@ export const SETTINGS_USER_FIELDS = new Set([
207
210
  "advisorModel",
208
211
  "agent",
209
212
  "agentPushNotifEnabled",
213
+ "allowAllClaudeAiMcps",
210
214
  "allowManagedHooksOnly",
211
215
  "allowManagedMcpServersOnly",
212
216
  "allowManagedPermissionRulesOnly",
@@ -230,6 +234,7 @@ export const SETTINGS_USER_FIELDS = new Set([
230
234
  "awsAuthRefresh",
231
235
  "awsCredentialExport",
232
236
  "blockedMarketplaces",
237
+ "breakReminder",
233
238
  "channelsEnabled",
234
239
  "claudeMd",
235
240
  "claudeMdExcludes",
@@ -286,6 +291,7 @@ export const SETTINGS_USER_FIELDS = new Set([
286
291
  "proactive",
287
292
  "promptSuggestionEnabled",
288
293
  "proxyAuthHelper",
294
+ "quietHours",
289
295
  "remote",
290
296
  "remoteControlAtStartup",
291
297
  "respectGitignore",
@@ -389,4 +395,17 @@ export const PLUGIN_SUBAGENT_BLOCKED_TOOLS = new Set([
389
395
  "Glob",
390
396
  "Grep",
391
397
  ]);
398
+ // Hand-curated named values for the frontmatter `effort` field. The Zod
399
+ // schema types `effort` as a permissive scalar; the field's own describe()
400
+ // string in the Claude Code bundle reads: "Thinking effort for the model:
401
+ // `low`, `medium`, `high`, `max`, or an integer." — so a string `effort`
402
+ // must be one of these, and a numeric `effort` must be an integer. (The
403
+ // runtime effortLevel enum also has `xhigh`, but the frontmatter describe
404
+ // string deliberately omits it; we follow the frontmatter contract.)
405
+ export const EFFORT_LEVELS = new Set([
406
+ "low",
407
+ "medium",
408
+ "high",
409
+ "max",
410
+ ]);
392
411
  //# sourceMappingURL=contracts.js.map
package/dist/discovery.js CHANGED
@@ -205,6 +205,25 @@ function discoverInDirectory(dir) {
205
205
  for (const f of monitors) {
206
206
  artifacts.push({ filePath: f, artifactType: "monitors-json" });
207
207
  }
208
+ // .claude-plugin/marketplace.json — schemastore-only artifact.
209
+ const marketplace = join(dir, ".claude-plugin", "marketplace.json");
210
+ if (existsSync(marketplace)) {
211
+ artifacts.push({ filePath: marketplace, artifactType: "marketplace-json" });
212
+ }
213
+ // keybindings.json — usually at ~/.claude/keybindings.json (user scope),
214
+ // but also picked up at project root if present. schemastore-only artifact.
215
+ for (const candidate of [
216
+ join(dir, "keybindings.json"),
217
+ join(dir, ".claude", "keybindings.json"),
218
+ ]) {
219
+ if (existsSync(candidate)) {
220
+ artifacts.push({
221
+ filePath: candidate,
222
+ artifactType: "keybindings-json",
223
+ scope: detectScope(candidate),
224
+ });
225
+ }
226
+ }
208
227
  // Claude config files — settings
209
228
  for (const name of ["settings.json", "settings.local.json"]) {
210
229
  // Direct in dir (handles both ~/.claude/settings.json and project root)
@@ -316,6 +335,10 @@ function classifyFile(filePath) {
316
335
  const parent = basename(dirname(filePath));
317
336
  if (name === "plugin.json" && parent === ".claude-plugin")
318
337
  return "plugin-json";
338
+ if (name === "marketplace.json" && parent === ".claude-plugin")
339
+ return "marketplace-json";
340
+ if (name === "keybindings.json")
341
+ return "keybindings-json";
319
342
  if (name === "SKILL.md")
320
343
  return "skill-md";
321
344
  if (name === "hooks.json" && parent === "hooks")
@@ -1,4 +1,5 @@
1
1
  import pc from "picocolors";
2
+ import { sanitizeForTerminal } from "../utils/terminal.js";
2
3
  const SEVERITY_ICONS = {
3
4
  error: pc.red("error"),
4
5
  warning: pc.yellow("warn "),
@@ -16,12 +17,12 @@ export function formatHuman(results, quiet) {
16
17
  if (filtered.length === 0)
17
18
  continue;
18
19
  lines.push("");
19
- lines.push(pc.underline(result.file));
20
+ lines.push(pc.underline(sanitizeForTerminal(result.file)));
20
21
  for (const d of filtered) {
21
22
  const loc = d.line
22
23
  ? pc.dim(`:${d.line}${d.column ? `:${d.column}` : ""}`)
23
24
  : "";
24
- lines.push(` ${SEVERITY_ICONS[d.severity]} ${d.message} ${pc.dim(d.rule)}${loc}`);
25
+ lines.push(` ${SEVERITY_ICONS[d.severity]} ${sanitizeForTerminal(d.message)} ${pc.dim(d.rule)}${loc}`);
25
26
  if (d.severity === "error")
26
27
  errorCount++;
27
28
  else if (d.severity === "warning")
package/dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs";
2
+ import { readFileSync, writeFileSync, copyFileSync, existsSync, statSync, } from "node:fs";
3
3
  import { resolve, relative, dirname, join } from "node:path";
4
4
  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 { assetCandidates } from "./utils/asset-path.js";
9
+ import { writeBlockedReason } from "./utils/safe-write.js";
10
+ import { sanitizeForTerminal } from "./utils/terminal.js";
8
11
  import { discoverArtifacts, detectArtifactTypes } from "./discovery.js";
9
12
  import { formatHuman } from "./formatters/human.js";
10
13
  import { formatJson } from "./formatters/json.js";
@@ -18,6 +21,8 @@ import { mcpJsonLinter } from "./linters/mcp-json.js";
18
21
  import { claudeMdLinter } from "./linters/claude-md.js";
19
22
  import { lspJsonLinter } from "./linters/lsp-json.js";
20
23
  import { monitorsJsonLinter } from "./linters/monitors-json.js";
24
+ import { marketplaceJsonLinter } from "./linters/marketplace-json.js";
25
+ import { keybindingsJsonLinter } from "./linters/keybindings-json.js";
21
26
  import { misplacedFileLinter, MISPLACED_FILE_RULES, } from "./linters/misplaced-file.js";
22
27
  import { pluginJsonFixer } from "./fixers/plugin-json.js";
23
28
  import { frontmatterFixer } from "./fixers/frontmatter.js";
@@ -35,6 +40,8 @@ import { MCP_JSON_RULES } from "./linters/mcp-json.js";
35
40
  import { CLAUDE_MD_RULES } from "./linters/claude-md.js";
36
41
  import { LSP_JSON_RULES } from "./linters/lsp-json.js";
37
42
  import { MONITORS_JSON_RULES } from "./linters/monitors-json.js";
43
+ import { MARKETPLACE_JSON_RULES } from "./linters/marketplace-json.js";
44
+ import { KEYBINDINGS_JSON_RULES } from "./linters/keybindings-json.js";
38
45
  const LINTERS = {
39
46
  "plugin-json": pluginJsonLinter,
40
47
  "skill-md": skillMdLinter,
@@ -46,6 +53,8 @@ const LINTERS = {
46
53
  "claude-md": claudeMdLinter,
47
54
  "lsp-json": lspJsonLinter,
48
55
  "monitors-json": monitorsJsonLinter,
56
+ "marketplace-json": marketplaceJsonLinter,
57
+ "keybindings-json": keybindingsJsonLinter,
49
58
  "misplaced-file": misplacedFileLinter,
50
59
  };
51
60
  const FIXERS = {
@@ -69,8 +78,16 @@ const ALL_RULES = [
69
78
  ...CLAUDE_MD_RULES,
70
79
  ...LSP_JSON_RULES,
71
80
  ...MONITORS_JSON_RULES,
81
+ ...MARKETPLACE_JSON_RULES,
82
+ ...KEYBINDINGS_JSON_RULES,
72
83
  ...MISPLACED_FILE_RULES,
73
84
  ];
85
+ /**
86
+ * Hard cap on artifact file size. Real Claude Code artifacts are KB-scale;
87
+ * this only exists to reject pathological / malicious inputs (e.g. a
88
+ * multi-gigabyte file crafted to exhaust memory).
89
+ */
90
+ const MAX_ARTIFACT_BYTES = 5 * 1024 * 1024;
74
91
  function simpleDiff(oldContent, newContent, filePath) {
75
92
  if (oldContent === newContent)
76
93
  return "";
@@ -106,7 +123,21 @@ function simpleDiff(oldContent, newContent, filePath) {
106
123
  }
107
124
  return lines.join("\n");
108
125
  }
109
- const pkgVersion = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
126
+ function readPkgVersion() {
127
+ // package.json ships beside dist/ (Node) or beside the executable
128
+ // (bun-compiled single-executable). Try every candidate; fall back to
129
+ // "0.0.0" if none resolve (e.g. an unexpected layout) rather than crashing.
130
+ for (const p of assetCandidates(import.meta.url, ["..", "package.json"])) {
131
+ try {
132
+ return JSON.parse(readFileSync(p, "utf8")).version;
133
+ }
134
+ catch {
135
+ // try next candidate
136
+ }
137
+ }
138
+ return "0.0.0";
139
+ }
140
+ const pkgVersion = readPkgVersion();
110
141
  sade("claudecode-linter", true)
111
142
  .version(pkgVersion)
112
143
  .describe("Linter for Claude Code plugin artifacts")
@@ -133,9 +164,11 @@ sade("claudecode-linter", true)
133
164
  const fixDryRun = !!opts["fix-dry-run"];
134
165
  try {
135
166
  if (opts.init !== undefined) {
136
- const __filename = fileURLToPath(import.meta.url);
137
- const pkgDir = dirname(dirname(__filename));
138
- const defaultsFile = join(pkgDir, ".claudecode-lint.defaults.yaml");
167
+ const defaultsFile = assetCandidates(import.meta.url, [
168
+ "..",
169
+ ".claudecode-lint.defaults.yaml",
170
+ ]).find((p) => existsSync(p)) ??
171
+ join(dirname(dirname(fileURLToPath(import.meta.url))), ".claudecode-lint.defaults.yaml");
139
172
  const targetDir = typeof opts.init === "string" ? resolve(opts.init) : process.cwd();
140
173
  const targetFile = join(targetDir, ".claudecode-lint.yaml");
141
174
  if (existsSync(targetFile)) {
@@ -197,10 +230,33 @@ sade("claudecode-linter", true)
197
230
  ignore: ignorePatterns,
198
231
  });
199
232
  if (artifacts.length === 0) {
200
- process.stderr.write(pc.yellow(`No plugin artifacts found in ${targetPath}\n`));
233
+ process.stderr.write(pc.yellow(`No plugin artifacts found in ${sanitizeForTerminal(targetPath)}\n`));
201
234
  continue;
202
235
  }
236
+ // All --fix / --format writes for this target must stay inside
237
+ // rootDir. If targetPath is a file, rootDir is its parent dir.
238
+ const resolvedTarget = resolve(targetPath);
239
+ let rootDir = resolvedTarget;
240
+ try {
241
+ if (!statSync(resolvedTarget).isDirectory()) {
242
+ rootDir = dirname(resolvedTarget);
243
+ }
244
+ }
245
+ catch {
246
+ rootDir = dirname(resolvedTarget);
247
+ }
203
248
  for (const artifact of artifacts) {
249
+ let sizeBytes;
250
+ try {
251
+ sizeBytes = statSync(artifact.filePath).size;
252
+ }
253
+ catch {
254
+ sizeBytes = 0;
255
+ }
256
+ if (sizeBytes > MAX_ARTIFACT_BYTES) {
257
+ process.stderr.write(pc.yellow(`Skipping ${sanitizeForTerminal(artifact.filePath)}: file exceeds ${MAX_ARTIFACT_BYTES}-byte limit (${sizeBytes} bytes)\n`));
258
+ continue;
259
+ }
204
260
  let content = readFileSync(artifact.filePath, "utf-8");
205
261
  const relPath = relative(process.cwd(), artifact.filePath);
206
262
  if (opts.format) {
@@ -208,8 +264,14 @@ sade("claudecode-linter", true)
208
264
  if (fixer) {
209
265
  const fixedContent = await fixer.fix(artifact.filePath, content, config);
210
266
  if (fixedContent !== content) {
211
- writeFileSync(artifact.filePath, fixedContent);
212
- formatted.push(relPath);
267
+ const blocked = writeBlockedReason(artifact.filePath, rootDir);
268
+ if (blocked) {
269
+ process.stderr.write(pc.red(`Refusing to write ${sanitizeForTerminal(artifact.filePath)}: ${blocked}\n`));
270
+ }
271
+ else {
272
+ writeFileSync(artifact.filePath, fixedContent);
273
+ formatted.push(relPath);
274
+ }
213
275
  }
214
276
  }
215
277
  continue;
@@ -221,9 +283,15 @@ sade("claudecode-linter", true)
221
283
  if (fixer) {
222
284
  const fixedContent = await fixer.fix(artifact.filePath, content, config);
223
285
  if (fixedContent !== content) {
224
- writeFileSync(artifact.filePath, fixedContent);
225
- content = fixedContent;
226
- fixed = 1;
286
+ const blocked = writeBlockedReason(artifact.filePath, rootDir);
287
+ if (blocked) {
288
+ process.stderr.write(pc.red(`Refusing to write ${sanitizeForTerminal(artifact.filePath)}: ${blocked}\n`));
289
+ }
290
+ else {
291
+ writeFileSync(artifact.filePath, fixedContent);
292
+ content = fixedContent;
293
+ fixed = 1;
294
+ }
227
295
  }
228
296
  }
229
297
  }
@@ -233,7 +301,7 @@ sade("claudecode-linter", true)
233
301
  const fixedContent = await fixer.fix(artifact.filePath, content, config);
234
302
  if (fixedContent !== content) {
235
303
  const diff = simpleDiff(content, fixedContent, artifact.filePath);
236
- process.stdout.write(diff + "\n");
304
+ process.stdout.write(sanitizeForTerminal(diff) + "\n");
237
305
  }
238
306
  }
239
307
  }
@@ -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,7 +66,12 @@ 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" },
68
- { id: "agent-md/color-required", 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" },
72
+ // agent-md/color-required removed in gitea#3 — Claude Code marks `color`
73
+ // as `.optional()` and `@internal`. The remaining `color-valid` rule still
74
+ // enforces the value set when an author opts in.
69
75
  { id: "agent-md/color-valid", defaultSeverity: "warning" },
70
76
  { id: "agent-md/system-prompt-present", defaultSeverity: "error" },
71
77
  { id: "agent-md/system-prompt-length", defaultSeverity: "warning" },
@@ -158,12 +164,43 @@ export const agentMdLinter = {
158
164
  !fm.data.model.startsWith("claude-")) {
159
165
  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
166
  }
161
- // color
162
- if (!("color" in fm.data) || typeof fm.data.color !== "string") {
163
- push(diag(config, filePath, "agent-md/color-required", "warning", '"color" is required in frontmatter'));
167
+ // permissionMode — typed as a permissive scalar by the Zod schema;
168
+ // at runtime only the PERMISSION_MODES values take effect.
169
+ if ("permissionMode" in fm.data) {
170
+ const pm = fm.data.permissionMode;
171
+ if (typeof pm !== "string" || !PERMISSION_MODES.has(pm)) {
172
+ push(diag(config, filePath, "agent-md/permission-mode-valid", "warning", `"permissionMode" must be one of: ${[...PERMISSION_MODES].join(", ")} (got ${JSON.stringify(pm)})`));
173
+ }
174
+ }
175
+ // effort — Zod types it as a permissive scalar; the field's describe()
176
+ // string restricts it to a named level or an integer.
177
+ if ("effort" in fm.data) {
178
+ const reason = invalidEffortReason(fm.data.effort);
179
+ if (reason) {
180
+ push(diag(config, filePath, "agent-md/effort-valid", "warning", reason));
181
+ }
164
182
  }
165
- else if (!AGENT_COLORS.has(fm.data.color)) {
166
- push(diag(config, filePath, "agent-md/color-valid", "warning", `"color" must be one of: ${[...AGENT_COLORS].join(", ")} (got "${fm.data.color}")`));
183
+ // maxTurns Zod accepts a string / float; only a positive integer
184
+ // is a meaningful turn budget.
185
+ if ("maxTurns" in fm.data) {
186
+ const mt = fm.data.maxTurns;
187
+ if (typeof mt !== "number" || !Number.isInteger(mt) || mt <= 0) {
188
+ push(diag(config, filePath, "agent-md/max-turns-valid", "warning", `"maxTurns" must be a positive integer (got ${JSON.stringify(mt)})`));
189
+ }
190
+ }
191
+ // color — optional per Claude Code's agent frontmatter Zod schema
192
+ // (`color: hW().optional().describe("@internal — display color in the
193
+ // agents UI")`). Only validate the value if the user set one; never
194
+ // require it. The `color-required` rule (gitea#3) is kept on disk for
195
+ // users who explicitly opted in via .claudecode-lint.yaml, but defaults
196
+ // to off because requiring an @internal display field misled users.
197
+ if ("color" in fm.data) {
198
+ if (typeof fm.data.color !== "string") {
199
+ push(diag(config, filePath, "agent-md/color-valid", "warning", '"color" must be a string when set'));
200
+ }
201
+ else if (!AGENT_COLORS.has(fm.data.color)) {
202
+ push(diag(config, filePath, "agent-md/color-valid", "warning", `"color" must be one of: ${[...AGENT_COLORS].join(", ")} (got "${fm.data.color}")`));
203
+ }
167
204
  }
168
205
  // Frontmatter keys: only flag cross-artifact misplacement. A key valid
169
206
  // for a *different* markdown artifact (e.g. a skill-only key on an
@@ -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) {
@@ -0,0 +1,53 @@
1
+ import { formatAjvError, loadKeybindingsSchema, summarizeErrors, } from "../plugin-schema.js";
2
+ import { isRuleEnabled, getRuleSeverity } from "../types.js";
3
+ export const KEYBINDINGS_JSON_RULES = [
4
+ { id: "keybindings-json/valid-json", defaultSeverity: "error" },
5
+ { id: "keybindings-json/schema-valid", defaultSeverity: "error" },
6
+ ];
7
+ /**
8
+ * Validate `~/.claude/keybindings.json` (or per-project `keybindings.json`)
9
+ * against the schemastore.org curated schema. As with marketplace.json,
10
+ * there is no Zod source for keybindings in the Claude Code bundle —
11
+ * schemastore is the sole authoritative shape we have.
12
+ */
13
+ export const keybindingsJsonLinter = {
14
+ artifactType: "keybindings-json",
15
+ lint(filePath, content, config) {
16
+ const diagnostics = [];
17
+ const push = (d) => {
18
+ if (d)
19
+ diagnostics.push(d);
20
+ };
21
+ let parsed;
22
+ try {
23
+ parsed = JSON.parse(content);
24
+ }
25
+ catch (e) {
26
+ push(diag(config, filePath, "keybindings-json/valid-json", "error", `Invalid JSON: ${e.message}`));
27
+ return diagnostics;
28
+ }
29
+ if (isRuleEnabled(config, "keybindings-json/schema-valid")) {
30
+ const compiled = loadKeybindingsSchema();
31
+ if (compiled) {
32
+ const ok = compiled.validate(parsed);
33
+ if (!ok && compiled.validate.errors) {
34
+ for (const err of summarizeErrors(compiled.validate.errors)) {
35
+ push(diag(config, filePath, "keybindings-json/schema-valid", "error", formatAjvError(err)));
36
+ }
37
+ }
38
+ }
39
+ }
40
+ return diagnostics;
41
+ },
42
+ };
43
+ function diag(config, filePath, ruleId, defaultSeverity, message) {
44
+ if (!isRuleEnabled(config, ruleId))
45
+ return null;
46
+ return {
47
+ rule: ruleId,
48
+ severity: getRuleSeverity(config, ruleId, defaultSeverity),
49
+ message,
50
+ file: filePath,
51
+ };
52
+ }
53
+ //# sourceMappingURL=keybindings-json.js.map
@@ -0,0 +1,55 @@
1
+ import { formatAjvError, loadMarketplaceSchema, summarizeErrors, } from "../plugin-schema.js";
2
+ import { isRuleEnabled, getRuleSeverity } from "../types.js";
3
+ export const MARKETPLACE_JSON_RULES = [
4
+ { id: "marketplace-json/valid-json", defaultSeverity: "error" },
5
+ { id: "marketplace-json/schema-valid", defaultSeverity: "error" },
6
+ ];
7
+ /**
8
+ * Validate `.claude-plugin/marketplace.json` against the schemastore.org
9
+ * curated schema. There's no Zod source for marketplace.json in the Claude
10
+ * Code bundle — schemastore is the sole authoritative shape we have.
11
+ *
12
+ * If the schemastore bundle isn't shipped with this install (unlikely;
13
+ * package.json includes it), the schema check silently skips.
14
+ */
15
+ export const marketplaceJsonLinter = {
16
+ artifactType: "marketplace-json",
17
+ lint(filePath, content, config) {
18
+ const diagnostics = [];
19
+ const push = (d) => {
20
+ if (d)
21
+ diagnostics.push(d);
22
+ };
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(content);
26
+ }
27
+ catch (e) {
28
+ push(diag(config, filePath, "marketplace-json/valid-json", "error", `Invalid JSON: ${e.message}`));
29
+ return diagnostics;
30
+ }
31
+ if (isRuleEnabled(config, "marketplace-json/schema-valid")) {
32
+ const compiled = loadMarketplaceSchema();
33
+ if (compiled) {
34
+ const ok = compiled.validate(parsed);
35
+ if (!ok && compiled.validate.errors) {
36
+ for (const err of summarizeErrors(compiled.validate.errors)) {
37
+ push(diag(config, filePath, "marketplace-json/schema-valid", "error", formatAjvError(err)));
38
+ }
39
+ }
40
+ }
41
+ }
42
+ return diagnostics;
43
+ },
44
+ };
45
+ function diag(config, filePath, ruleId, defaultSeverity, message) {
46
+ if (!isRuleEnabled(config, ruleId))
47
+ return null;
48
+ return {
49
+ rule: ruleId,
50
+ severity: getRuleSeverity(config, ruleId, defaultSeverity),
51
+ message,
52
+ file: filePath,
53
+ };
54
+ }
55
+ //# sourceMappingURL=marketplace-json.js.map
@@ -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"));