claudecode-linter 2.1.136 → 2.1.138-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 +7 -0
- package/dist/canonical-paths.js +43 -0
- package/dist/contracts.js +7 -4
- package/dist/discovery.js +52 -1
- package/dist/index.js +27 -21
- package/dist/linters/agent-md.js +106 -6
- package/dist/linters/misplaced-file.js +45 -0
- package/package.json +2 -2
|
@@ -110,3 +110,10 @@ rules:
|
|
|
110
110
|
claude-md/no-absolute-paths: info
|
|
111
111
|
claude-md/no-todo-markers: info
|
|
112
112
|
claude-md/no-trailing-whitespace: info
|
|
113
|
+
|
|
114
|
+
# ── Misplaced files ──────────────────────────────────────
|
|
115
|
+
# Reserved artifact basenames (plugin.json, hooks.json,
|
|
116
|
+
# marketplace.json, SKILL.md) found at non-canonical paths inside
|
|
117
|
+
# a plugin tree. Claude Code silently ignores them — easy to make,
|
|
118
|
+
# hard to debug.
|
|
119
|
+
misplaced-file/canonical-location: warning
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const CANONICAL_ARTIFACTS = [
|
|
2
|
+
{
|
|
3
|
+
basename: "plugin.json",
|
|
4
|
+
expectedPath: ".claude-plugin/plugin.json",
|
|
5
|
+
description: "plugin manifest",
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
basename: "marketplace.json",
|
|
9
|
+
expectedPath: ".claude-plugin/marketplace.json",
|
|
10
|
+
description: "marketplace manifest",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
basename: "hooks.json",
|
|
14
|
+
expectedPath: "hooks/hooks.json",
|
|
15
|
+
description: "plugin hooks config",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
basename: "SKILL.md",
|
|
19
|
+
expectedPattern: "skills/*/SKILL.md",
|
|
20
|
+
description: "skill manifest",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
basename: ".mcp.json",
|
|
24
|
+
expectedPath: ".mcp.json",
|
|
25
|
+
description: "plugin MCP server definitions",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
basename: ".lsp.json",
|
|
29
|
+
expectedPath: ".lsp.json",
|
|
30
|
+
description: "plugin LSP server configurations",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
basename: "monitors.json",
|
|
34
|
+
expectedPath: "monitors/monitors.json",
|
|
35
|
+
description: "plugin background-monitor declarations",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
basename: "settings.json",
|
|
39
|
+
expectedPath: "settings.json",
|
|
40
|
+
description: "plugin default settings",
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
//# sourceMappingURL=canonical-paths.js.map
|
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.138 — extracted 2026-05-09T06:52:55.909Z
|
|
3
3
|
// Do not edit manually. Run: npm run generate-contracts
|
|
4
4
|
export const TOOLS = new Set([
|
|
5
5
|
"Agent",
|
|
@@ -311,7 +311,10 @@ export const SETTINGS_USER_FIELDS = new Set([
|
|
|
311
311
|
"worktree",
|
|
312
312
|
"wslInheritsWindowsSettings",
|
|
313
313
|
]);
|
|
314
|
-
export const SETTINGS_PROJECT_FIELDS = new Set([
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
export const SETTINGS_PROJECT_FIELDS = new Set(["permissions"]);
|
|
315
|
+
// Hand-curated denylist of tools declared in agent frontmatter that never
|
|
316
|
+
// reach plugin-defined subagents at runtime. Source: tracked upstream bugs
|
|
317
|
+
// https://github.com/anthropics/claude-code/issues/52055
|
|
318
|
+
// https://github.com/anthropics/claude-code/issues/52004
|
|
319
|
+
export const PLUGIN_SUBAGENT_BLOCKED_TOOLS = new Set(["Glob", "Grep"]);
|
|
317
320
|
//# sourceMappingURL=contracts.js.map
|
package/dist/discovery.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { statSync, existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, resolve, join } from "node:path";
|
|
2
|
+
import { basename, dirname, resolve, join, relative } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { globSync } from "tinyglobby";
|
|
5
5
|
import { minimatch } from "minimatch";
|
|
6
|
+
import { CANONICAL_ARTIFACTS } from "./canonical-paths.js";
|
|
6
7
|
const CLAUDE_USER_DIR = join(homedir(), ".claude");
|
|
7
8
|
function loadIgnoreFile(dir) {
|
|
8
9
|
const ignoreFile = join(dir, ".claudecode-lint-ignore");
|
|
@@ -221,8 +222,58 @@ function discoverInDirectory(dir) {
|
|
|
221
222
|
scope: detectScope(claudeMd),
|
|
222
223
|
});
|
|
223
224
|
}
|
|
225
|
+
// Misplaced-file scan: only meaningful inside a plugin tree
|
|
226
|
+
// (we use `.claude-plugin/` as the marker). Outside plugin
|
|
227
|
+
// trees, a stray `plugin.json` / `hooks.json` could legitimately
|
|
228
|
+
// belong to some other tool and we don't want to false-positive.
|
|
229
|
+
if (existsSync(join(dir, ".claude-plugin"))) {
|
|
230
|
+
for (const m of findMisplacedFiles(dir)) {
|
|
231
|
+
artifacts.push(m);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
224
234
|
return artifacts;
|
|
225
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Walk `pluginRoot` looking for files whose basename is reserved
|
|
238
|
+
* for a Claude Code artifact and which Claude Code reads only from
|
|
239
|
+
* a single canonical path. Anything matching a basename but
|
|
240
|
+
* sitting at a non-canonical location is returned as a
|
|
241
|
+
* `misplaced-file` artifact for the misplaced-file linter to flag.
|
|
242
|
+
*
|
|
243
|
+
* Ignores typical noise dirs (`node_modules`, `.git`, `dist`, the
|
|
244
|
+
* plugin install cache's `.in_use` / `.orphaned_at` markers).
|
|
245
|
+
*/
|
|
246
|
+
function findMisplacedFiles(pluginRoot) {
|
|
247
|
+
const out = [];
|
|
248
|
+
for (const entry of CANONICAL_ARTIFACTS) {
|
|
249
|
+
const found = globSync(`**/${entry.basename}`, {
|
|
250
|
+
cwd: pluginRoot,
|
|
251
|
+
absolute: true,
|
|
252
|
+
// dot: walk into dot-directories like `.claude-plugin/`
|
|
253
|
+
// — that's exactly where the most common misplacement
|
|
254
|
+
// (hooks.json under `.claude-plugin/` instead of plugin
|
|
255
|
+
// root's `hooks/`) lives.
|
|
256
|
+
dot: true,
|
|
257
|
+
ignore: [
|
|
258
|
+
"**/node_modules/**",
|
|
259
|
+
"**/.git/**",
|
|
260
|
+
"**/dist/**",
|
|
261
|
+
"**/.in_use/**",
|
|
262
|
+
"**/.orphaned_at/**",
|
|
263
|
+
],
|
|
264
|
+
});
|
|
265
|
+
for (const filePath of found) {
|
|
266
|
+
const rel = relative(pluginRoot, filePath);
|
|
267
|
+
const isCanonical = entry.expectedPattern
|
|
268
|
+
? minimatch(rel, entry.expectedPattern)
|
|
269
|
+
: rel === entry.expectedPath;
|
|
270
|
+
if (!isCanonical) {
|
|
271
|
+
out.push({ filePath, artifactType: "misplaced-file" });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
226
277
|
function classifyFile(filePath) {
|
|
227
278
|
const name = basename(filePath);
|
|
228
279
|
const parent = basename(dirname(filePath));
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { resolve, relative, dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import
|
|
5
|
+
import sade from "sade";
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
import { loadConfig, mergeCliRules } from "./config.js";
|
|
8
8
|
import { discoverArtifacts } from "./discovery.js";
|
|
@@ -16,6 +16,7 @@ import { hooksJsonLinter } from "./linters/hooks-json.js";
|
|
|
16
16
|
import { settingsJsonLinter } from "./linters/settings-json.js";
|
|
17
17
|
import { mcpJsonLinter } from "./linters/mcp-json.js";
|
|
18
18
|
import { claudeMdLinter } from "./linters/claude-md.js";
|
|
19
|
+
import { misplacedFileLinter, MISPLACED_FILE_RULES, } from "./linters/misplaced-file.js";
|
|
19
20
|
import { pluginJsonFixer } from "./fixers/plugin-json.js";
|
|
20
21
|
import { frontmatterFixer } from "./fixers/frontmatter.js";
|
|
21
22
|
import { hooksJsonFixer } from "./fixers/hooks-json.js";
|
|
@@ -39,6 +40,7 @@ const LINTERS = {
|
|
|
39
40
|
"settings-json": settingsJsonLinter,
|
|
40
41
|
"mcp-json": mcpJsonLinter,
|
|
41
42
|
"claude-md": claudeMdLinter,
|
|
43
|
+
"misplaced-file": misplacedFileLinter,
|
|
42
44
|
};
|
|
43
45
|
const FIXERS = {
|
|
44
46
|
"plugin-json": pluginJsonFixer,
|
|
@@ -59,6 +61,7 @@ const ALL_RULES = [
|
|
|
59
61
|
...SETTINGS_JSON_RULES,
|
|
60
62
|
...MCP_JSON_RULES,
|
|
61
63
|
...CLAUDE_MD_RULES,
|
|
64
|
+
...MISPLACED_FILE_RULES,
|
|
62
65
|
];
|
|
63
66
|
function simpleDiff(oldContent, newContent, filePath) {
|
|
64
67
|
if (oldContent === newContent)
|
|
@@ -95,27 +98,30 @@ function simpleDiff(oldContent, newContent, filePath) {
|
|
|
95
98
|
}
|
|
96
99
|
return lines.join("\n");
|
|
97
100
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
.
|
|
101
|
-
.
|
|
102
|
-
.version(JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version)
|
|
103
|
-
.argument("[paths...]", "Plugin directories or individual files", ["."])
|
|
101
|
+
const pkgVersion = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
|
|
102
|
+
sade("claudecode-linter", true)
|
|
103
|
+
.version(pkgVersion)
|
|
104
|
+
.describe("Linter for Claude Code plugin artifacts")
|
|
104
105
|
.option("--lint", "Lint artifacts and report issues (default)")
|
|
105
106
|
.option("--fix", "Auto-fix lint violations, then report remaining issues")
|
|
106
107
|
.option("--format", "Format all artifacts for consistent style (no lint output)")
|
|
107
|
-
.option("--output
|
|
108
|
-
.option("--config
|
|
109
|
-
.option("--scope
|
|
110
|
-
.option("--ignore
|
|
108
|
+
.option("--output", "Output format: human | json", "human")
|
|
109
|
+
.option("--config", "Config file path")
|
|
110
|
+
.option("--scope", "Filter by scope: user | project | subdirectory")
|
|
111
|
+
.option("--ignore", "Comma-separated glob patterns to ignore")
|
|
111
112
|
.option("--quiet", "Only show errors")
|
|
112
|
-
.option("--enable
|
|
113
|
-
.option("--disable
|
|
114
|
-
.option("--rule
|
|
113
|
+
.option("--enable", "Comma-separated rule IDs to enable")
|
|
114
|
+
.option("--disable", "Comma-separated rule IDs to disable")
|
|
115
|
+
.option("--rule", "Run only this single rule ID")
|
|
115
116
|
.option("--list-rules", "Print all rules with their default severity and exit")
|
|
116
117
|
.option("--fix-dry-run", "Run fixers but print diff instead of writing")
|
|
117
|
-
.option("--init
|
|
118
|
-
.action(async (
|
|
118
|
+
.option("--init", "Copy default config to path (default: current directory)")
|
|
119
|
+
.action(async (opts) => {
|
|
120
|
+
// sade accepts variadic positional via opts._; default to ["."] when empty.
|
|
121
|
+
// Multi-word flags like --list-rules arrive in kebab-case form on opts.
|
|
122
|
+
const paths = Array.isArray(opts._) && opts._.length > 0 ? opts._ : ["."];
|
|
123
|
+
const listRules = !!opts["list-rules"];
|
|
124
|
+
const fixDryRun = !!opts["fix-dry-run"];
|
|
119
125
|
try {
|
|
120
126
|
if (opts.init !== undefined) {
|
|
121
127
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -131,7 +137,7 @@ program
|
|
|
131
137
|
process.stdout.write(pc.green(`Created ${targetFile}\n`));
|
|
132
138
|
process.exit(0);
|
|
133
139
|
}
|
|
134
|
-
if (
|
|
140
|
+
if (listRules) {
|
|
135
141
|
const sevColor = { error: pc.red, warning: pc.yellow, info: pc.blue };
|
|
136
142
|
for (const rule of ALL_RULES) {
|
|
137
143
|
const color = sevColor[rule.defaultSeverity];
|
|
@@ -197,7 +203,7 @@ program
|
|
|
197
203
|
}
|
|
198
204
|
}
|
|
199
205
|
}
|
|
200
|
-
else if (
|
|
206
|
+
else if (fixDryRun) {
|
|
201
207
|
const fixer = FIXERS[artifact.artifactType];
|
|
202
208
|
if (fixer) {
|
|
203
209
|
const fixedContent = await fixer.fix(artifact.filePath, content, config);
|
|
@@ -231,7 +237,7 @@ program
|
|
|
231
237
|
}
|
|
232
238
|
process.exit(0);
|
|
233
239
|
}
|
|
234
|
-
if (!
|
|
240
|
+
if (!fixDryRun) {
|
|
235
241
|
const output = opts.output === "json"
|
|
236
242
|
? formatJson(results, !!opts.quiet)
|
|
237
243
|
: formatHuman(results, !!opts.quiet);
|
|
@@ -244,6 +250,6 @@ program
|
|
|
244
250
|
process.stderr.write(pc.red(`Fatal error: ${err.message}\n`));
|
|
245
251
|
process.exit(2);
|
|
246
252
|
}
|
|
247
|
-
})
|
|
248
|
-
|
|
253
|
+
})
|
|
254
|
+
.parse(process.argv);
|
|
249
255
|
//# sourceMappingURL=index.js.map
|
package/dist/linters/agent-md.js
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { AGENT_FRONTMATTER, AGENT_MODELS, AGENT_COLORS, TOOLS, PLUGIN_SUBAGENT_BLOCKED_TOOLS, } from "../contracts.js";
|
|
2
4
|
import { isRuleEnabled, getRuleSeverity } from "../types.js";
|
|
3
5
|
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
6
|
+
function findPluginRoot(agentFilePath) {
|
|
7
|
+
let dir = dirname(agentFilePath);
|
|
8
|
+
for (let i = 0; i < 10; i++) {
|
|
9
|
+
if (existsSync(join(dir, ".claude-plugin", "plugin.json")))
|
|
10
|
+
return dir;
|
|
11
|
+
const parent = dirname(dir);
|
|
12
|
+
if (parent === dir)
|
|
13
|
+
break;
|
|
14
|
+
dir = parent;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function loadJson(p) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function loadPluginName(pluginRoot) {
|
|
27
|
+
const j = loadJson(join(pluginRoot, ".claude-plugin", "plugin.json"));
|
|
28
|
+
if (j && typeof j === "object" && j !== null && "name" in j) {
|
|
29
|
+
const n = j.name;
|
|
30
|
+
return typeof n === "string" ? n : null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function loadMcpServerNames(pluginRoot) {
|
|
35
|
+
const j = loadJson(join(pluginRoot, ".mcp.json"));
|
|
36
|
+
const out = new Set();
|
|
37
|
+
if (j &&
|
|
38
|
+
typeof j === "object" &&
|
|
39
|
+
j !== null &&
|
|
40
|
+
"mcpServers" in j &&
|
|
41
|
+
typeof j.mcpServers === "object" &&
|
|
42
|
+
j.mcpServers !== null) {
|
|
43
|
+
for (const k of Object.keys(j.mcpServers)) {
|
|
44
|
+
out.add(k);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function parseToolsField(v) {
|
|
50
|
+
if (typeof v !== "string")
|
|
51
|
+
return [];
|
|
52
|
+
return v
|
|
53
|
+
.split(",")
|
|
54
|
+
.map((s) => s.trim())
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
}
|
|
4
57
|
const RULES = [
|
|
5
58
|
{ id: "agent-md/valid-frontmatter", defaultSeverity: "error" },
|
|
6
59
|
{ id: "agent-md/name-required", defaultSeverity: "error" },
|
|
@@ -9,12 +62,15 @@ const RULES = [
|
|
|
9
62
|
{ id: "agent-md/description-examples", defaultSeverity: "warning" },
|
|
10
63
|
{ id: "agent-md/model-required", defaultSeverity: "error" },
|
|
11
64
|
{ id: "agent-md/model-valid", defaultSeverity: "warning" },
|
|
12
|
-
{ id: "agent-md/color-required", defaultSeverity: "
|
|
65
|
+
{ id: "agent-md/color-required", defaultSeverity: "warning" },
|
|
13
66
|
{ id: "agent-md/color-valid", defaultSeverity: "warning" },
|
|
14
67
|
{ id: "agent-md/system-prompt-present", defaultSeverity: "error" },
|
|
15
68
|
{ id: "agent-md/system-prompt-length", defaultSeverity: "warning" },
|
|
16
69
|
{ id: "agent-md/system-prompt-second-person", defaultSeverity: "info" },
|
|
17
70
|
{ id: "agent-md/no-unknown-frontmatter", defaultSeverity: "info" },
|
|
71
|
+
{ id: "agent-md/known-tools", defaultSeverity: "warning" },
|
|
72
|
+
{ id: "agent-md/mcp-tools-resolve", defaultSeverity: "error" },
|
|
73
|
+
{ id: "agent-md/plugin-subagent-blocked-tools", defaultSeverity: "warning" },
|
|
18
74
|
];
|
|
19
75
|
function diag(config, filePath, ruleId, defaultSeverity, message, line, column) {
|
|
20
76
|
if (!isRuleEnabled(config, ruleId))
|
|
@@ -47,10 +103,10 @@ export const agentMdLinter = {
|
|
|
47
103
|
}
|
|
48
104
|
else {
|
|
49
105
|
const name = fm.data.name;
|
|
50
|
-
if (!/^[a-z][a-z0-9-]*[a-z0-9]
|
|
51
|
-
name.length <
|
|
106
|
+
if (!/^[a-z][a-z0-9-]*[a-z0-9]?$/.test(name) ||
|
|
107
|
+
name.length < 2 ||
|
|
52
108
|
name.length > 50) {
|
|
53
|
-
push(diag(config, filePath, "agent-md/name-format", "error", `"name" must be
|
|
109
|
+
push(diag(config, filePath, "agent-md/name-format", "error", `"name" must be 2-50 chars, lowercase alphanumeric + hyphens (got "${name}")`));
|
|
54
110
|
}
|
|
55
111
|
}
|
|
56
112
|
// description
|
|
@@ -73,7 +129,7 @@ export const agentMdLinter = {
|
|
|
73
129
|
}
|
|
74
130
|
// color
|
|
75
131
|
if (!("color" in fm.data) || typeof fm.data.color !== "string") {
|
|
76
|
-
push(diag(config, filePath, "agent-md/color-required", "
|
|
132
|
+
push(diag(config, filePath, "agent-md/color-required", "warning", '"color" is required in frontmatter'));
|
|
77
133
|
}
|
|
78
134
|
else if (!AGENT_COLORS.has(fm.data.color)) {
|
|
79
135
|
push(diag(config, filePath, "agent-md/color-valid", "warning", `"color" must be one of: ${[...AGENT_COLORS].join(", ")} (got "${fm.data.color}")`));
|
|
@@ -98,6 +154,50 @@ export const agentMdLinter = {
|
|
|
98
154
|
push(diag(config, filePath, "agent-md/system-prompt-second-person", "info", 'System prompt should use second person ("You are...", "You will...")', fm.bodyStartLine));
|
|
99
155
|
}
|
|
100
156
|
}
|
|
157
|
+
// tools
|
|
158
|
+
const declaredTools = parseToolsField(fm.data.tools);
|
|
159
|
+
if (declaredTools.length > 0) {
|
|
160
|
+
const pluginRoot = findPluginRoot(filePath);
|
|
161
|
+
const pluginName = pluginRoot ? loadPluginName(pluginRoot) : null;
|
|
162
|
+
const mcpServers = pluginRoot
|
|
163
|
+
? loadMcpServerNames(pluginRoot)
|
|
164
|
+
: new Set();
|
|
165
|
+
for (const t of declaredTools) {
|
|
166
|
+
if (t.startsWith("mcp__")) {
|
|
167
|
+
// Two valid shapes:
|
|
168
|
+
// mcp__plugin_<plugin>_<server>__<tool> plugin-namespaced
|
|
169
|
+
// mcp__<server>__<tool> user-config (non-plugin)
|
|
170
|
+
const ns = t.match(/^mcp__plugin_([a-z0-9-]+)_([a-z0-9-]+)__(.+)$/);
|
|
171
|
+
const bare = t.match(/^mcp__([a-z0-9-]+)__(.+)$/);
|
|
172
|
+
if (ns) {
|
|
173
|
+
const declaredPlugin = ns[1];
|
|
174
|
+
const declaredServer = ns[2];
|
|
175
|
+
if (pluginName && declaredPlugin !== pluginName) {
|
|
176
|
+
push(diag(config, filePath, "agent-md/mcp-tools-resolve", "error", `Tool "${t}" references plugin "${declaredPlugin}", but this plugin is named "${pluginName}".`));
|
|
177
|
+
}
|
|
178
|
+
if (pluginRoot &&
|
|
179
|
+
mcpServers.size > 0 &&
|
|
180
|
+
!mcpServers.has(declaredServer)) {
|
|
181
|
+
push(diag(config, filePath, "agent-md/mcp-tools-resolve", "error", `Tool "${t}" references MCP server "${declaredServer}", but .mcp.json declares: [${[...mcpServers].join(", ")}].`));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (bare && pluginRoot) {
|
|
185
|
+
const server = bare[1];
|
|
186
|
+
const rest = bare[2];
|
|
187
|
+
if (mcpServers.has(server)) {
|
|
188
|
+
push(diag(config, filePath, "agent-md/mcp-tools-resolve", "error", `Tool "${t}" uses bare "mcp__<server>__<tool>" form, but this is a plugin (${pluginName ?? "unknown name"}). Plugin agents must use the namespaced form: "mcp__plugin_${pluginName ?? "<plugin>"}_${server}__${rest}".`));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!TOOLS.has(t)) {
|
|
194
|
+
push(diag(config, filePath, "agent-md/known-tools", "warning", `Unknown built-in tool "${t}" in tools: field. Valid: ${[...TOOLS].sort().join(", ")}.`));
|
|
195
|
+
}
|
|
196
|
+
if (pluginRoot && PLUGIN_SUBAGENT_BLOCKED_TOOLS.has(t)) {
|
|
197
|
+
push(diag(config, filePath, "agent-md/plugin-subagent-blocked-tools", "warning", `Tool "${t}" is declared but does NOT reach plugin subagents at runtime — known Claude Code bug (https://github.com/anthropics/claude-code/issues/52055). The agent will silently lack this tool. For local file ops, use Bash with rg/find, or dispatch the built-in Explore subagent.`));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
101
201
|
return diagnostics;
|
|
102
202
|
},
|
|
103
203
|
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { CANONICAL_ARTIFACTS } from "../canonical-paths.js";
|
|
3
|
+
import { isRuleEnabled, getRuleSeverity } from "../types.js";
|
|
4
|
+
export const MISPLACED_FILE_RULES = [
|
|
5
|
+
{ id: "misplaced-file/canonical-location", defaultSeverity: "warning" },
|
|
6
|
+
];
|
|
7
|
+
/**
|
|
8
|
+
* Flag files whose basename is reserved for a Claude Code artifact
|
|
9
|
+
* but which sit at a non-canonical path. Claude Code reads each
|
|
10
|
+
* artifact only from its canonical location and silently ignores
|
|
11
|
+
* copies elsewhere, so this class of mistake is easy to make and
|
|
12
|
+
* hard to debug — `/reload-plugins` reports `0 hooks` (or skills,
|
|
13
|
+
* etc.) with no other signal.
|
|
14
|
+
*
|
|
15
|
+
* Discovery (see discovery.ts) populates this linter's inputs by
|
|
16
|
+
* walking plugin trees and matching basenames; this linter just
|
|
17
|
+
* emits one diagnostic per misplaced file with the expected
|
|
18
|
+
* location.
|
|
19
|
+
*/
|
|
20
|
+
export const misplacedFileLinter = {
|
|
21
|
+
artifactType: "misplaced-file",
|
|
22
|
+
lint(filePath, _content, config) {
|
|
23
|
+
const ruleId = "misplaced-file/canonical-location";
|
|
24
|
+
if (!isRuleEnabled(config, ruleId))
|
|
25
|
+
return [];
|
|
26
|
+
const base = basename(filePath);
|
|
27
|
+
const entry = CANONICAL_ARTIFACTS.find((a) => a.basename === base);
|
|
28
|
+
if (!entry)
|
|
29
|
+
return [];
|
|
30
|
+
const expected = entry.expectedPath ?? entry.expectedPattern ?? "";
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
rule: ruleId,
|
|
34
|
+
severity: getRuleSeverity(config, ruleId, "warning"),
|
|
35
|
+
message: `${base} is at a non-canonical path; ` +
|
|
36
|
+
`Claude Code reads ${entry.description} only from ` +
|
|
37
|
+
`\`${expected}\` (relative to plugin root) and ` +
|
|
38
|
+
`silently ignores copies elsewhere. ` +
|
|
39
|
+
`Move the file there or rename it.`,
|
|
40
|
+
file: filePath,
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=misplaced-file.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudecode-linter",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.138-patch.1",
|
|
4
4
|
"description": "Standalone linter for Claude Code plugins and configuration files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"node": ">=18"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"commander": "^14.0.3",
|
|
49
48
|
"minimatch": "^10.2.4",
|
|
50
49
|
"picocolors": "^1.1.1",
|
|
51
50
|
"prettier": "^3.8.1",
|
|
51
|
+
"sade": "^1.8.1",
|
|
52
52
|
"semver": "^7.7.4",
|
|
53
53
|
"tinyglobby": "^0.2.15",
|
|
54
54
|
"yaml": "^2.8.3"
|