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.
- package/.claudecode-lint.defaults.yaml +21 -2
- package/README.md +90 -3
- package/contracts/agent-frontmatter.schema.json +142 -2
- package/contracts/command-frontmatter.schema.json +128 -2
- package/contracts/hooks.schema.json +275 -0
- package/contracts/lsp.schema.json +2 -2
- package/contracts/mcp.schema.json +326 -0
- package/contracts/monitors.schema.json +3 -3
- package/contracts/plugin.schema.json +672 -22
- package/contracts/schemastore/keybindings.schema.json +179 -0
- package/contracts/schemastore/manifest.json +25 -0
- package/contracts/schemastore/marketplace.schema.json +2067 -0
- package/contracts/schemastore/plugin-manifest.schema.json +1834 -0
- package/contracts/schemastore/settings.schema.json +3062 -0
- package/contracts/settings.schema.json +333 -4
- package/contracts/skill-frontmatter.schema.json +198 -2
- package/dist/config.js +13 -8
- package/dist/contracts.js +20 -1
- package/dist/discovery.js +23 -0
- package/dist/formatters/human.js +3 -2
- package/dist/index.js +80 -12
- package/dist/linters/agent-md.js +44 -7
- package/dist/linters/command-md.js +37 -1
- package/dist/linters/hooks-json.js +24 -0
- package/dist/linters/keybindings-json.js +53 -0
- package/dist/linters/marketplace-json.js +55 -0
- package/dist/linters/mcp-json.js +24 -0
- package/dist/linters/settings-json.js +42 -15
- package/dist/linters/skill-md.js +48 -1
- package/dist/plugin-schema.js +96 -39
- package/dist/utils/asset-path.js +56 -0
- package/dist/utils/effort.js +29 -0
- package/dist/utils/frontmatter-keys.js +32 -3
- package/dist/utils/frontmatter.js +1 -1
- package/dist/utils/safe-write.js +36 -0
- package/dist/utils/terminal.js +18 -0
- 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.
|
|
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")
|
package/dist/formatters/human.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
}
|
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,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/
|
|
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
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
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"));
|