claudecode-linter 2.1.150 → 2.1.152
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 +8 -2
- package/contracts/agent-frontmatter.schema.json +2 -2
- package/contracts/command-frontmatter.schema.json +56 -2
- package/contracts/hooks.schema.json +2 -2
- package/contracts/lsp.schema.json +2 -2
- package/contracts/mcp.schema.json +26 -2
- 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 +3240 -0
- package/contracts/settings.schema.json +61 -4
- package/contracts/skill-frontmatter.schema.json +56 -2
- package/dist/contracts.js +4 -1
- package/dist/discovery.js +23 -0
- package/dist/fixers/mcp-json.js +6 -0
- package/dist/index.js +8 -0
- package/dist/linters/agent-md.js +16 -7
- package/dist/linters/keybindings-json.js +53 -0
- package/dist/linters/marketplace-json.js +55 -0
- package/dist/linters/mcp-json.js +8 -0
- package/dist/linters/plugin-json.js +8 -0
- package/dist/linters/settings-json.js +89 -16
- package/dist/linters/skill-md.js +7 -1
- package/dist/plugin-schema.js +89 -41
- package/package.json +4 -2
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extractedFromClaudeCodeVersion": "2.1.
|
|
3
|
-
"extractedAt": "2026-05-
|
|
2
|
+
"extractedFromClaudeCodeVersion": "2.1.152",
|
|
3
|
+
"extractedAt": "2026-05-27T07:39:39.197Z",
|
|
4
4
|
"schema": {
|
|
5
5
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
6
6
|
"title": "Claude Code settings.json",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"properties": {
|
|
9
9
|
"$schema": {
|
|
10
|
-
"const": "
|
|
10
|
+
"const": "https://json.schemastore.org/claude-code-settings.json",
|
|
11
11
|
"description": "JSON Schema reference for Claude Code settings"
|
|
12
12
|
},
|
|
13
13
|
"apiKeyHelper": {
|
|
@@ -78,6 +78,48 @@
|
|
|
78
78
|
"type": "boolean",
|
|
79
79
|
"description": "Whether file picker should respect .gitignore files (default: true). Note: .ignore files are always respected."
|
|
80
80
|
},
|
|
81
|
+
"breakReminder": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"enabled": {
|
|
85
|
+
"type": "boolean",
|
|
86
|
+
"description": "Show a friendly nudge after sustained continuous use (default false). Must be true for the reminder to fire."
|
|
87
|
+
},
|
|
88
|
+
"intervalMinutes": {
|
|
89
|
+
"type": "number",
|
|
90
|
+
"description": "Minutes of continuous use before the reminder fires (default 120). Re-fires every interval until you take a break."
|
|
91
|
+
},
|
|
92
|
+
"breakThresholdMinutes": {
|
|
93
|
+
"type": "number",
|
|
94
|
+
"description": "Minutes of inactivity that count as a break and reset the timer (default 15)"
|
|
95
|
+
},
|
|
96
|
+
"message": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"description": "Custom reminder text. Leave unset for a rotating set of friendly nudges."
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"description": "@internal Opt-in break reminder. When enabled, shows a dismissible nudge after sustained continuous use. Never blocks — just a friendly heads-up."
|
|
102
|
+
},
|
|
103
|
+
"quietHours": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"properties": {
|
|
106
|
+
"enabled": {
|
|
107
|
+
"type": "boolean",
|
|
108
|
+
"description": "Show a one-time nudge when you start or keep using the CLI inside your quiet-hours window (default false)."
|
|
109
|
+
},
|
|
110
|
+
"start": {
|
|
111
|
+
"type": "string",
|
|
112
|
+
"pattern": "^([01]?\\d|2[0-3]):[0-5]\\d$",
|
|
113
|
+
"description": "Start of the quiet-hours window, 24-hour local time \"HH:MM\"."
|
|
114
|
+
},
|
|
115
|
+
"end": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"pattern": "^([01]?\\d|2[0-3]):[0-5]\\d$",
|
|
118
|
+
"description": "End of the quiet-hours window, 24-hour local time \"HH:MM\". May be earlier than start for an overnight range."
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"description": "@internal Opt-in quiet hours. When enabled, shows a single soft nudge per session while inside the configured local-time window. Never blocks."
|
|
122
|
+
},
|
|
81
123
|
"cleanupPeriodDays": {
|
|
82
124
|
"type": "number",
|
|
83
125
|
"description": "Number of days to retain chat transcripts before automatic cleanup (default: 30). Minimum 1. Use a large value for long retention; use --no-session-persistence to disable transcript writes entirely."
|
|
@@ -531,6 +573,10 @@
|
|
|
531
573
|
"type": "boolean",
|
|
532
574
|
"description": "Disable Remote Control (claude.ai/code, `claude remote-control`, `--remote-control`/`--rc`, auto-start, and the in-session toggle). Typically set in managed settings."
|
|
533
575
|
},
|
|
576
|
+
"disableWorkflows": {
|
|
577
|
+
"type": "boolean",
|
|
578
|
+
"description": "@internal Disable the Workflows feature (also via CLAUDE_CODE_DISABLE_WORKFLOWS)."
|
|
579
|
+
},
|
|
534
580
|
"disableSkillShellExecution": {
|
|
535
581
|
"type": "boolean",
|
|
536
582
|
"description": "Disable inline shell execution in skills and custom slash commands from user, project, or plugin sources. Commands are replaced with a placeholder instead of being run."
|
|
@@ -568,6 +614,10 @@
|
|
|
568
614
|
"type": "boolean",
|
|
569
615
|
"description": "When true (and set in managed settings), allowedMcpServers is only read from managed settings. deniedMcpServers still merges from all sources, so users can deny servers for themselves. Users can still add their own MCP servers, but only the admin-defined allowlist applies."
|
|
570
616
|
},
|
|
617
|
+
"allowAllClaudeAiMcps": {
|
|
618
|
+
"type": "boolean",
|
|
619
|
+
"description": "When true (and set in managed settings), claude.ai cloud MCP connectors load alongside managed-mcp.json instead of being suppressed by its exclusive-control lockdown. Default off preserves the lockdown. Read from managed settings only."
|
|
620
|
+
},
|
|
571
621
|
"strictPluginOnlyCustomization": {
|
|
572
622
|
"anyOf": [
|
|
573
623
|
{
|
|
@@ -1836,6 +1886,13 @@
|
|
|
1836
1886
|
},
|
|
1837
1887
|
"description": "Enterprise blocklist of marketplace sources. When set in managed settings, these exact sources are blocked from being added as marketplaces. The check happens BEFORE downloading, so blocked sources never touch the filesystem."
|
|
1838
1888
|
},
|
|
1889
|
+
"pluginSuggestionMarketplaces": {
|
|
1890
|
+
"type": "array",
|
|
1891
|
+
"items": {
|
|
1892
|
+
"type": "string"
|
|
1893
|
+
},
|
|
1894
|
+
"description": "Marketplace names whose plugins may surface as contextual install suggestions (relevance-based tips), in addition to the official marketplace. Only honored when set in managed settings (policy scope); the key is ignored in user, project, and local settings. A name only takes effect when the marketplace is registered on the machine AND its registered source is also declared in managed settings, either as the extraKnownMarketplaces entry for that name or as an entry of strictKnownMarketplaces. A marketplace registered from a different source under an allowlisted name is ignored."
|
|
1895
|
+
},
|
|
1839
1896
|
"forceLoginMethod": {
|
|
1840
1897
|
"enum": [
|
|
1841
1898
|
"claudeai",
|
|
@@ -2328,7 +2385,7 @@
|
|
|
2328
2385
|
},
|
|
2329
2386
|
"showThinkingSummaries": {
|
|
2330
2387
|
"type": "boolean",
|
|
2331
|
-
"description": "
|
|
2388
|
+
"description": "Request API-side thinking summaries and show them in the conversation and in the transcript view (ctrl+o). Set explicitly to override the default for your install."
|
|
2332
2389
|
},
|
|
2333
2390
|
"skipDangerousModePermissionPrompt": {
|
|
2334
2391
|
"type": "boolean",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extractedFromClaudeCodeVersion": "2.1.
|
|
3
|
-
"extractedAt": "2026-05-
|
|
2
|
+
"extractedFromClaudeCodeVersion": "2.1.152",
|
|
3
|
+
"extractedAt": "2026-05-27T07:39:39.199Z",
|
|
4
4
|
"schema": {
|
|
5
5
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
6
6
|
"title": "Claude Code SKILL.md frontmatter",
|
|
@@ -84,6 +84,60 @@
|
|
|
84
84
|
],
|
|
85
85
|
"description": "Tools available to the model while this file is active. Comma-separated string or YAML list."
|
|
86
86
|
},
|
|
87
|
+
"disallowed-tools": {
|
|
88
|
+
"anyOf": [
|
|
89
|
+
{
|
|
90
|
+
"anyOf": [
|
|
91
|
+
{
|
|
92
|
+
"type": "string"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"type": "number"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"type": "boolean"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"type": "null"
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"type": "array",
|
|
107
|
+
"items": {
|
|
108
|
+
"type": "string"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"description": "Tools removed from the model while this file is active. Comma-separated string or YAML list. Cleared when the user sends the next message."
|
|
113
|
+
},
|
|
114
|
+
"disallowedTools": {
|
|
115
|
+
"anyOf": [
|
|
116
|
+
{
|
|
117
|
+
"anyOf": [
|
|
118
|
+
{
|
|
119
|
+
"type": "string"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"type": "number"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"type": "boolean"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"type": "null"
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"type": "array",
|
|
134
|
+
"items": {
|
|
135
|
+
"type": "string"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
"description": "Canonical (normalized) alias of `disallowed-tools`."
|
|
140
|
+
},
|
|
87
141
|
"argument-hint": {
|
|
88
142
|
"anyOf": [
|
|
89
143
|
{
|
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.152 — extracted 2026-05-27T07:39:34.438Z
|
|
3
3
|
// Do not edit manually. Run: npm run generate-contracts
|
|
4
4
|
export const TOOLS = new Set([
|
|
5
5
|
"Agent",
|
|
@@ -56,6 +56,7 @@ export const HOOK_EVENTS = new Set([
|
|
|
56
56
|
"ElicitationResult",
|
|
57
57
|
"FileChanged",
|
|
58
58
|
"InstructionsLoaded",
|
|
59
|
+
"MessageDisplay",
|
|
59
60
|
"Notification",
|
|
60
61
|
"PermissionDenied",
|
|
61
62
|
"PermissionRequest",
|
|
@@ -249,6 +250,7 @@ export const SETTINGS_USER_FIELDS = new Set([
|
|
|
249
250
|
"disableBackgroundAgents",
|
|
250
251
|
"disableRemoteControl",
|
|
251
252
|
"disableSkillShellExecution",
|
|
253
|
+
"disableWorkflows",
|
|
252
254
|
"disabledMcpjsonServers",
|
|
253
255
|
"doneMeansMerged",
|
|
254
256
|
"editorMode",
|
|
@@ -283,6 +285,7 @@ export const SETTINGS_USER_FIELDS = new Set([
|
|
|
283
285
|
"permissions",
|
|
284
286
|
"plansDirectory",
|
|
285
287
|
"pluginConfigs",
|
|
288
|
+
"pluginSuggestionMarketplaces",
|
|
286
289
|
"pluginTrustMessage",
|
|
287
290
|
"policyHelper",
|
|
288
291
|
"prUrlTemplate",
|
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/fixers/mcp-json.js
CHANGED
|
@@ -33,6 +33,12 @@ export const mcpJsonFixer = {
|
|
|
33
33
|
orderedServer[field] = serverObj[field];
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
// gitea#7: URL-based servers with type:"http" rewrite to
|
|
37
|
+
// "streamable-http" (the runtime transport is identical post-v2.1.146;
|
|
38
|
+
// the rename avoids the legacy OAuth-probe code path).
|
|
39
|
+
if (orderedServer.type === "http" && typeof orderedServer.url === "string") {
|
|
40
|
+
orderedServer.type = "streamable-http";
|
|
41
|
+
}
|
|
36
42
|
sortedServers[serverName] = orderedServer;
|
|
37
43
|
}
|
|
38
44
|
else {
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,8 @@ import { mcpJsonLinter } from "./linters/mcp-json.js";
|
|
|
21
21
|
import { claudeMdLinter } from "./linters/claude-md.js";
|
|
22
22
|
import { lspJsonLinter } from "./linters/lsp-json.js";
|
|
23
23
|
import { monitorsJsonLinter } from "./linters/monitors-json.js";
|
|
24
|
+
import { marketplaceJsonLinter } from "./linters/marketplace-json.js";
|
|
25
|
+
import { keybindingsJsonLinter } from "./linters/keybindings-json.js";
|
|
24
26
|
import { misplacedFileLinter, MISPLACED_FILE_RULES, } from "./linters/misplaced-file.js";
|
|
25
27
|
import { pluginJsonFixer } from "./fixers/plugin-json.js";
|
|
26
28
|
import { frontmatterFixer } from "./fixers/frontmatter.js";
|
|
@@ -38,6 +40,8 @@ import { MCP_JSON_RULES } from "./linters/mcp-json.js";
|
|
|
38
40
|
import { CLAUDE_MD_RULES } from "./linters/claude-md.js";
|
|
39
41
|
import { LSP_JSON_RULES } from "./linters/lsp-json.js";
|
|
40
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";
|
|
41
45
|
const LINTERS = {
|
|
42
46
|
"plugin-json": pluginJsonLinter,
|
|
43
47
|
"skill-md": skillMdLinter,
|
|
@@ -49,6 +53,8 @@ const LINTERS = {
|
|
|
49
53
|
"claude-md": claudeMdLinter,
|
|
50
54
|
"lsp-json": lspJsonLinter,
|
|
51
55
|
"monitors-json": monitorsJsonLinter,
|
|
56
|
+
"marketplace-json": marketplaceJsonLinter,
|
|
57
|
+
"keybindings-json": keybindingsJsonLinter,
|
|
52
58
|
"misplaced-file": misplacedFileLinter,
|
|
53
59
|
};
|
|
54
60
|
const FIXERS = {
|
|
@@ -72,6 +78,8 @@ const ALL_RULES = [
|
|
|
72
78
|
...CLAUDE_MD_RULES,
|
|
73
79
|
...LSP_JSON_RULES,
|
|
74
80
|
...MONITORS_JSON_RULES,
|
|
81
|
+
...MARKETPLACE_JSON_RULES,
|
|
82
|
+
...KEYBINDINGS_JSON_RULES,
|
|
75
83
|
...MISPLACED_FILE_RULES,
|
|
76
84
|
];
|
|
77
85
|
/**
|
package/dist/linters/agent-md.js
CHANGED
|
@@ -69,7 +69,9 @@ const RULES = [
|
|
|
69
69
|
{ id: "agent-md/permission-mode-valid", defaultSeverity: "warning" },
|
|
70
70
|
{ id: "agent-md/effort-valid", defaultSeverity: "warning" },
|
|
71
71
|
{ id: "agent-md/max-turns-valid", defaultSeverity: "warning" },
|
|
72
|
-
|
|
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.
|
|
73
75
|
{ id: "agent-md/color-valid", defaultSeverity: "warning" },
|
|
74
76
|
{ id: "agent-md/system-prompt-present", defaultSeverity: "error" },
|
|
75
77
|
{ id: "agent-md/system-prompt-length", defaultSeverity: "warning" },
|
|
@@ -186,12 +188,19 @@ export const agentMdLinter = {
|
|
|
186
188
|
push(diag(config, filePath, "agent-md/max-turns-valid", "warning", `"maxTurns" must be a positive integer (got ${JSON.stringify(mt)})`));
|
|
187
189
|
}
|
|
188
190
|
}
|
|
189
|
-
// color
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
}
|
|
195
204
|
}
|
|
196
205
|
// Frontmatter keys: only flag cross-artifact misplacement. A key valid
|
|
197
206
|
// for a *different* markdown artifact (e.g. a skill-only key on an
|
|
@@ -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
|
@@ -19,6 +19,7 @@ const RULES = [
|
|
|
19
19
|
{ id: "mcp-json/url-protocol", defaultSeverity: "warning" },
|
|
20
20
|
{ id: "mcp-json/url-valid", defaultSeverity: "error" },
|
|
21
21
|
{ id: "mcp-json/type-matches-transport", defaultSeverity: "warning" },
|
|
22
|
+
{ id: "mcp-json/prefer-streamable-http", defaultSeverity: "warning" },
|
|
22
23
|
{ id: "mcp-json/command-args-split", defaultSeverity: "info" },
|
|
23
24
|
{ id: "mcp-json/args-array", defaultSeverity: "error" },
|
|
24
25
|
{ id: "mcp-json/env-object", defaultSeverity: "error" },
|
|
@@ -143,6 +144,13 @@ export const mcpJsonLinter = {
|
|
|
143
144
|
if ("type" in server && !HTTP_TRANSPORT_TYPES.has(server.type)) {
|
|
144
145
|
push(diag(config, filePath, "mcp-json/type-matches-transport", "warning", `Server "${name}" has URL but type is "${server.type}" (expected one of ${[...HTTP_TRANSPORT_TYPES].join(", ")})`, sp?.line, sp?.column));
|
|
145
146
|
}
|
|
147
|
+
// Prefer "streamable-http" over "http" for URL-based MCP servers.
|
|
148
|
+
// Per gitea#7: older Claude Code's "http" transport may trigger an
|
|
149
|
+
// OAuth metadata probe / DCR POST that "streamable-http" skips, and
|
|
150
|
+
// misbehaving upstream proxies can silently fail with "0 tools".
|
|
151
|
+
if (server.type === "http") {
|
|
152
|
+
push(diag(config, filePath, "mcp-json/prefer-streamable-http", "warning", `Server "${name}" uses type "http" — prefer "streamable-http" to avoid OAuth probe edge cases on some upstream proxies`, sp?.line, sp?.column));
|
|
153
|
+
}
|
|
146
154
|
}
|
|
147
155
|
// stdio server checks
|
|
148
156
|
if (hasCommand && !hasUrl) {
|
|
@@ -22,6 +22,7 @@ const RULES = [
|
|
|
22
22
|
{ id: "plugin-json/keywords-no-duplicates", defaultSeverity: "warning" },
|
|
23
23
|
{ id: "plugin-json/no-unknown-fields", defaultSeverity: "info" },
|
|
24
24
|
{ id: "plugin-json/license-spdx", defaultSeverity: "info" },
|
|
25
|
+
{ id: "plugin-json/no-inline-mcp-servers", defaultSeverity: "warning" },
|
|
25
26
|
];
|
|
26
27
|
function findKeyPosition(content, key) {
|
|
27
28
|
const re = new RegExp(`"${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"\\s*:`);
|
|
@@ -178,6 +179,13 @@ export const pluginJsonLinter = {
|
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
181
|
}
|
|
182
|
+
// gitea#9: mcpServers belongs in `.mcp.json` at the plugin root, not as
|
|
183
|
+
// a top-level key in `.claude-plugin/plugin.json`. The legacy shape still
|
|
184
|
+
// loads but uses a different code path and contradicts the official docs.
|
|
185
|
+
if ("mcpServers" in parsed) {
|
|
186
|
+
const p = pos("mcpServers");
|
|
187
|
+
push(diag(config, filePath, "plugin-json/no-inline-mcp-servers", "warning", "\"mcpServers\" should not live inside plugin.json — move to .mcp.json at the plugin root. See https://code.claude.com/docs/en/plugins#plugin-structure-overview", p?.line, p?.column));
|
|
188
|
+
}
|
|
181
189
|
// license
|
|
182
190
|
if ("license" in parsed && typeof parsed.license === "string") {
|
|
183
191
|
if (!SPDX_COMMON.has(parsed.license)) {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
|
-
import {
|
|
1
|
+
import { basename, dirname, join } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { SETTINGS_USER_FIELDS, TOOLS, PERMISSIONS_FIELDS, PERMISSION_MODES, SANDBOX_FIELDS, SANDBOX_NETWORK_FIELDS, SANDBOX_FILESYSTEM_FIELDS, } from "../contracts.js";
|
|
3
4
|
import { formatAjvError, loadSettingsSchema, summarizeErrors, } from "../plugin-schema.js";
|
|
4
5
|
import { isRuleEnabled, getRuleSeverity } from "../types.js";
|
|
5
6
|
const RULES = [
|
|
6
7
|
{ id: "settings-json/valid-json", defaultSeverity: "error" },
|
|
7
8
|
{ id: "settings-json/schema-valid", defaultSeverity: "error" },
|
|
8
|
-
|
|
9
|
+
// settings-json/scope-file-name removed in gitea#4 — .claude/settings.json
|
|
10
|
+
// is a valid project-shared settings source. See misplaced-file/* for
|
|
11
|
+
// wrong-path warnings.
|
|
9
12
|
{ id: "settings-json/scope-field", defaultSeverity: "warning" },
|
|
10
13
|
{ id: "settings-json/no-unknown-fields", defaultSeverity: "warning" },
|
|
11
14
|
{ id: "settings-json/permissions-object", defaultSeverity: "error" },
|
|
@@ -28,6 +31,7 @@ const RULES = [
|
|
|
28
31
|
{ id: "settings-json/plugins-boolean", defaultSeverity: "warning" },
|
|
29
32
|
{ id: "settings-json/plugins-format", defaultSeverity: "warning" },
|
|
30
33
|
{ id: "settings-json/skip-prompt-boolean", defaultSeverity: "error" },
|
|
34
|
+
{ id: "settings-json/disable-project-mcpjson-shadow", defaultSeverity: "warning" },
|
|
31
35
|
];
|
|
32
36
|
// Expected type for each known sandbox sub-key, and for the nested
|
|
33
37
|
// network/filesystem objects. Mirrors Claude Code's Zod schema.
|
|
@@ -242,22 +246,47 @@ export const settingsJsonLinter = {
|
|
|
242
246
|
}
|
|
243
247
|
}
|
|
244
248
|
}
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
//
|
|
249
|
+
// gitea#4: `.claude/settings.json` is a first-class project settings source
|
|
250
|
+
// (`projectSettings`, committed/shared) distinct from `.claude/settings.local.json`
|
|
251
|
+
// (`localSettings`, gitignored). Claude Code's bundle defines both:
|
|
252
|
+
// `case "projectSettings": return Rk.join(".claude","settings.json")`
|
|
253
|
+
// `case "localSettings": return "project, gitignored"`
|
|
254
|
+
// The rule no longer fires for plain project-level settings.json; the
|
|
255
|
+
// `misplaced-file/canonical-location` rule covers files at the wrong path.
|
|
256
|
+
// Determine which fields are valid for this scope. gitea#2: the legacy
|
|
257
|
+
// SETTINGS_PROJECT_FIELDS whitelist is too narrow — Claude Code accepts
|
|
258
|
+
// most user-level fields at project scope too. We now treat all known
|
|
259
|
+
// user fields as valid at project scope, except a small denylist of
|
|
260
|
+
// genuinely user-only fields (auth helpers, plugin enablement, dangerous
|
|
261
|
+
// mode), which still warn via `settings-json/scope-field`.
|
|
262
|
+
const knownFields = SETTINGS_USER_FIELDS;
|
|
263
|
+
// Fields that genuinely only take effect at user scope. Project-scope
|
|
264
|
+
// versions are silently ignored by Claude Code. Source: bundle inspection
|
|
265
|
+
// (auth helpers run from user creds; enabledPlugins / skip-permission
|
|
266
|
+
// flags can't be enabled by a checked-in repo for security reasons).
|
|
267
|
+
const USER_ONLY_FIELDS = new Set([
|
|
268
|
+
"apiKeyHelper",
|
|
269
|
+
"awsAuthRefresh",
|
|
270
|
+
"awsCredentialExport",
|
|
271
|
+
"gcpAuthRefresh",
|
|
272
|
+
"proxyAuthHelper",
|
|
273
|
+
"policyHelper",
|
|
274
|
+
"enabledPlugins",
|
|
275
|
+
"skipDangerousModePermissionPrompt",
|
|
276
|
+
"forceLoginMethod",
|
|
277
|
+
"forceLoginOrgUUID",
|
|
278
|
+
]);
|
|
279
|
+
// unknown / misplaced top-level fields
|
|
252
280
|
for (const key of Object.keys(parsed)) {
|
|
253
281
|
if (!knownFields.has(key)) {
|
|
254
282
|
const p = findKeyPosition(content, key);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
283
|
+
push(diag(config, filePath, "settings-json/no-unknown-fields", "warning", `Unknown top-level field "${key}"`, p?.line, p?.column));
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
// gitea#2: only the genuinely user-only denylist warns at project scope.
|
|
287
|
+
if (USER_ONLY_FIELDS.has(key) && scope && scope !== "user") {
|
|
288
|
+
const p = findKeyPosition(content, key);
|
|
289
|
+
push(diag(config, filePath, "settings-json/scope-field", "warning", `"${key}" only takes effect at user level (~/.claude/settings.json); it is ignored in project settings`, p?.line, p?.column));
|
|
261
290
|
}
|
|
262
291
|
}
|
|
263
292
|
// Validate the sub-keys of a nested object against a known-field set and a
|
|
@@ -444,6 +473,50 @@ export const settingsJsonLinter = {
|
|
|
444
473
|
}
|
|
445
474
|
}
|
|
446
475
|
}
|
|
476
|
+
// gitea#8: when this settings.json sits at <plugin-root>/.claude/settings.json
|
|
477
|
+
// and the plugin also ships a <plugin-root>/.mcp.json, Claude Code loads the
|
|
478
|
+
// plugin's MCP servers twice when launched from the plugin dir (once as the
|
|
479
|
+
// plugin, once as project-scope .mcp.json) and dedupes — triggering /doctor
|
|
480
|
+
// "skipped" warnings. The committed suppression is to list each server name
|
|
481
|
+
// in this file's `disabledMcpjsonServers`.
|
|
482
|
+
if (isRuleEnabled(config, "settings-json/disable-project-mcpjson-shadow") &&
|
|
483
|
+
basename(filePath) === "settings.json" &&
|
|
484
|
+
basename(dirname(filePath)) === ".claude") {
|
|
485
|
+
const pluginRoot = dirname(dirname(filePath));
|
|
486
|
+
const pluginJson = join(pluginRoot, ".claude-plugin", "plugin.json");
|
|
487
|
+
const mcpJson = join(pluginRoot, ".mcp.json");
|
|
488
|
+
if (existsSync(pluginJson) && existsSync(mcpJson)) {
|
|
489
|
+
let mcpServerNames = [];
|
|
490
|
+
try {
|
|
491
|
+
const mcp = JSON.parse(readFileSync(mcpJson, "utf-8"));
|
|
492
|
+
if (isPlainObject(mcp.mcpServers)) {
|
|
493
|
+
mcpServerNames = Object.keys(mcp.mcpServers);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
catch { /* ignore — broken .mcp.json is the mcp-json linter's job */ }
|
|
497
|
+
// settings.local.json with the same disable also satisfies the rule.
|
|
498
|
+
let disabled = Array.isArray(parsed.disabledMcpjsonServers)
|
|
499
|
+
? parsed.disabledMcpjsonServers
|
|
500
|
+
: [];
|
|
501
|
+
const localPath = join(dirname(filePath), "settings.local.json");
|
|
502
|
+
if (existsSync(localPath)) {
|
|
503
|
+
try {
|
|
504
|
+
const local = JSON.parse(readFileSync(localPath, "utf-8"));
|
|
505
|
+
if (Array.isArray(local.disabledMcpjsonServers)) {
|
|
506
|
+
disabled = disabled.concat(local.disabledMcpjsonServers);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
catch { /* ignore */ }
|
|
510
|
+
}
|
|
511
|
+
const disabledSet = new Set(disabled.filter((x) => typeof x === "string"));
|
|
512
|
+
const dp = findKeyPosition(content, "disabledMcpjsonServers");
|
|
513
|
+
for (const name of mcpServerNames) {
|
|
514
|
+
if (!disabledSet.has(name)) {
|
|
515
|
+
push(diag(config, filePath, "settings-json/disable-project-mcpjson-shadow", "warning", `Server "${name}" is declared in .mcp.json but not in .claude/settings.json#disabledMcpjsonServers — when Claude Code is launched from this plugin directory, the MCP server is loaded twice (plugin + project-level), triggering /doctor "skipped" warnings`, dp?.line, dp?.column));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
447
520
|
// skipDangerousModePermissionPrompt — user-level only
|
|
448
521
|
if ("skipDangerousModePermissionPrompt" in parsed) {
|
|
449
522
|
if (typeof parsed.skipDangerousModePermissionPrompt !== "boolean") {
|
package/dist/linters/skill-md.js
CHANGED
|
@@ -52,7 +52,13 @@ function hasTriggerSignal(desc) {
|
|
|
52
52
|
// ("Trigger on …", "Trigger when/whenever/if …", "Triggers: …", "TRIGGER
|
|
53
53
|
// when:") — not when "trigger" merely appears as a noun (e.g. the phrase
|
|
54
54
|
// "no trigger phrases").
|
|
55
|
-
|
|
55
|
+
// gitea#5: widened to accept more applicability phrasings observed in the
|
|
56
|
+
// wild — "Loaded automatically when …", "when a turn touches …",
|
|
57
|
+
// "applies to …", "for memory operations", "Memory protocol for the X
|
|
58
|
+
// MCP" (a noun-opener that names the domain). The runtime makes
|
|
59
|
+
// `description` optional, so this rule is advisory; aim for low false
|
|
60
|
+
// positive rate rather than complete coverage.
|
|
61
|
+
const triggerPhrases = /\btrigger(s)?\b\s*(on|when|whenever|if|upon|for)\b|\btriggers?\s*:|\buse (this skill |it )?(when|whenever|for|to|on|if)\b|\bshould be used (when|whenever|for|to|if)\b|\b(applies|invoke|reach for|call this|run this|use this|loaded|loads|reaches for|fires) (when|whenever|if|to|for|automatically|on)\b|\bwhen (the user|you|asked|working|debugging|diagnosing|investigating|reviewing|writing|building|creating|editing|setting up|deploying|the task|a request|a question|a message|a turn|the turn|the session|the conversation|the model|you need|there|something|anything)\b|\bwhen [a-z]+ing\b|\bfor (debugging|diagnosing|tasks|requests|questions|when|any|memory|recall|search|saves?|the )\b|\bprotocol for\b|\barchivist for\b|\bhandler for\b|\bauto(matic|-?)\s+(save|load|invocation)\b/i;
|
|
56
62
|
if (triggerPhrases.test(d))
|
|
57
63
|
return true;
|
|
58
64
|
// First "sentence-ish" chunk — used to detect imperative / gerund openers.
|