claudecode-linter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.claudecode-lint.defaults.yaml +112 -0
  2. package/LICENSE +21 -0
  3. package/README.md +181 -0
  4. package/dist/config.d.ts +3 -0
  5. package/dist/config.js +66 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/discovery.d.ts +8 -0
  8. package/dist/discovery.js +219 -0
  9. package/dist/discovery.js.map +1 -0
  10. package/dist/fixers/claude-md.d.ts +2 -0
  11. package/dist/fixers/claude-md.js +24 -0
  12. package/dist/fixers/claude-md.js.map +1 -0
  13. package/dist/fixers/frontmatter.d.ts +2 -0
  14. package/dist/fixers/frontmatter.js +85 -0
  15. package/dist/fixers/frontmatter.js.map +1 -0
  16. package/dist/fixers/hooks-json.d.ts +2 -0
  17. package/dist/fixers/hooks-json.js +23 -0
  18. package/dist/fixers/hooks-json.js.map +1 -0
  19. package/dist/fixers/mcp-json.d.ts +2 -0
  20. package/dist/fixers/mcp-json.js +47 -0
  21. package/dist/fixers/mcp-json.js.map +1 -0
  22. package/dist/fixers/plugin-json.d.ts +2 -0
  23. package/dist/fixers/plugin-json.js +34 -0
  24. package/dist/fixers/plugin-json.js.map +1 -0
  25. package/dist/fixers/settings-json.d.ts +2 -0
  26. package/dist/fixers/settings-json.js +47 -0
  27. package/dist/fixers/settings-json.js.map +1 -0
  28. package/dist/formatters/human.d.ts +2 -0
  29. package/dist/formatters/human.js +47 -0
  30. package/dist/formatters/human.js.map +1 -0
  31. package/dist/formatters/json.d.ts +2 -0
  32. package/dist/formatters/json.js +10 -0
  33. package/dist/formatters/json.js.map +1 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.js +246 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/linters/agent-md.d.ts +8 -0
  38. package/dist/linters/agent-md.js +92 -0
  39. package/dist/linters/agent-md.js.map +1 -0
  40. package/dist/linters/claude-md.d.ts +8 -0
  41. package/dist/linters/claude-md.js +109 -0
  42. package/dist/linters/claude-md.js.map +1 -0
  43. package/dist/linters/command-md.d.ts +8 -0
  44. package/dist/linters/command-md.js +61 -0
  45. package/dist/linters/command-md.js.map +1 -0
  46. package/dist/linters/hooks-json.d.ts +8 -0
  47. package/dist/linters/hooks-json.js +123 -0
  48. package/dist/linters/hooks-json.js.map +1 -0
  49. package/dist/linters/mcp-json.d.ts +8 -0
  50. package/dist/linters/mcp-json.js +160 -0
  51. package/dist/linters/mcp-json.js.map +1 -0
  52. package/dist/linters/plugin-json.d.ts +8 -0
  53. package/dist/linters/plugin-json.js +166 -0
  54. package/dist/linters/plugin-json.js.map +1 -0
  55. package/dist/linters/settings-json.d.ts +8 -0
  56. package/dist/linters/settings-json.js +187 -0
  57. package/dist/linters/settings-json.js.map +1 -0
  58. package/dist/linters/skill-md.d.ts +8 -0
  59. package/dist/linters/skill-md.js +97 -0
  60. package/dist/linters/skill-md.js.map +1 -0
  61. package/dist/types.d.ts +39 -0
  62. package/dist/types.js +15 -0
  63. package/dist/types.js.map +1 -0
  64. package/dist/utils/frontmatter.d.ts +9 -0
  65. package/dist/utils/frontmatter.js +65 -0
  66. package/dist/utils/frontmatter.js.map +1 -0
  67. package/dist/utils/kebab-case.d.ts +2 -0
  68. package/dist/utils/kebab-case.js +14 -0
  69. package/dist/utils/kebab-case.js.map +1 -0
  70. package/dist/utils/prettier.d.ts +2 -0
  71. package/dist/utils/prettier.js +17 -0
  72. package/dist/utils/prettier.js.map +1 -0
  73. package/package.json +53 -0
@@ -0,0 +1,92 @@
1
+ import { isRuleEnabled, getRuleSeverity } from "../types.js";
2
+ import { parseFrontmatter } from "../utils/frontmatter.js";
3
+ const RULES = [
4
+ { id: "agent-md/valid-frontmatter", defaultSeverity: "error" },
5
+ { id: "agent-md/name-required", defaultSeverity: "error" },
6
+ { id: "agent-md/name-format", defaultSeverity: "error" },
7
+ { id: "agent-md/description-required", defaultSeverity: "error" },
8
+ { id: "agent-md/description-examples", defaultSeverity: "warning" },
9
+ { id: "agent-md/model-required", defaultSeverity: "error" },
10
+ { id: "agent-md/model-valid", defaultSeverity: "warning" },
11
+ { id: "agent-md/color-required", defaultSeverity: "error" },
12
+ { id: "agent-md/color-valid", defaultSeverity: "warning" },
13
+ { id: "agent-md/system-prompt-present", defaultSeverity: "error" },
14
+ { id: "agent-md/system-prompt-length", defaultSeverity: "warning" },
15
+ { id: "agent-md/system-prompt-second-person", defaultSeverity: "info" },
16
+ ];
17
+ const VALID_MODELS = new Set(["inherit", "sonnet", "opus", "haiku"]);
18
+ const VALID_COLORS = new Set(["blue", "cyan", "green", "yellow", "magenta", "red"]);
19
+ function diag(config, filePath, ruleId, defaultSeverity, message, line, column) {
20
+ if (!isRuleEnabled(config, ruleId))
21
+ return null;
22
+ return {
23
+ rule: ruleId,
24
+ severity: getRuleSeverity(config, ruleId, defaultSeverity),
25
+ message,
26
+ file: filePath,
27
+ line,
28
+ column,
29
+ };
30
+ }
31
+ export const agentMdLinter = {
32
+ artifactType: "agent-md",
33
+ lint(filePath, content, config) {
34
+ const diagnostics = [];
35
+ const push = (d) => { if (d)
36
+ diagnostics.push(d); };
37
+ const fm = parseFrontmatter(content);
38
+ if (!fm.valid) {
39
+ push(diag(config, filePath, "agent-md/valid-frontmatter", "error", fm.error ?? "Invalid frontmatter"));
40
+ return diagnostics;
41
+ }
42
+ // name
43
+ if (!("name" in fm.data) || typeof fm.data.name !== "string") {
44
+ push(diag(config, filePath, "agent-md/name-required", "error", "\"name\" is required in frontmatter"));
45
+ }
46
+ else {
47
+ const name = fm.data.name;
48
+ if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) || name.length < 3 || name.length > 50) {
49
+ push(diag(config, filePath, "agent-md/name-format", "error", `"name" must be 3-50 chars, lowercase alphanumeric + hyphens (got "${name}")`));
50
+ }
51
+ }
52
+ // description
53
+ if (!("description" in fm.data) || typeof fm.data.description !== "string") {
54
+ push(diag(config, filePath, "agent-md/description-required", "error", "\"description\" is required in frontmatter"));
55
+ }
56
+ else {
57
+ if (!/<example>/i.test(fm.data.description)) {
58
+ push(diag(config, filePath, "agent-md/description-examples", "warning", "Description should include <example> blocks for triggering"));
59
+ }
60
+ }
61
+ // model
62
+ if (!("model" in fm.data) || typeof fm.data.model !== "string") {
63
+ push(diag(config, filePath, "agent-md/model-required", "error", "\"model\" is required in frontmatter"));
64
+ }
65
+ else if (!VALID_MODELS.has(fm.data.model)) {
66
+ push(diag(config, filePath, "agent-md/model-valid", "warning", `"model" must be one of: ${[...VALID_MODELS].join(", ")} (got "${fm.data.model}")`));
67
+ }
68
+ // color
69
+ if (!("color" in fm.data) || typeof fm.data.color !== "string") {
70
+ push(diag(config, filePath, "agent-md/color-required", "error", "\"color\" is required in frontmatter"));
71
+ }
72
+ else if (!VALID_COLORS.has(fm.data.color)) {
73
+ push(diag(config, filePath, "agent-md/color-valid", "warning", `"color" must be one of: ${[...VALID_COLORS].join(", ")} (got "${fm.data.color}")`));
74
+ }
75
+ // system prompt (body)
76
+ const body = fm.body.trim();
77
+ if (!body) {
78
+ push(diag(config, filePath, "agent-md/system-prompt-present", "error", "Agent must have a system prompt (body after frontmatter)"));
79
+ }
80
+ else {
81
+ if (body.length < 20) {
82
+ push(diag(config, filePath, "agent-md/system-prompt-length", "warning", `System prompt is very short (${body.length} chars, recommended >= 20)`, fm.bodyStartLine));
83
+ }
84
+ if (!/\byou\b/i.test(body)) {
85
+ push(diag(config, filePath, "agent-md/system-prompt-second-person", "info", "System prompt should use second person (\"You are...\", \"You will...\")", fm.bodyStartLine));
86
+ }
87
+ }
88
+ return diagnostics;
89
+ },
90
+ };
91
+ export { RULES as AGENT_MD_RULES };
92
+ //# sourceMappingURL=agent-md.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-md.js","sourceRoot":"","sources":["../../src/linters/agent-md.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { Linter, Severity } from "../types.js";
2
+ interface RuleDef {
3
+ id: string;
4
+ defaultSeverity: Severity;
5
+ }
6
+ declare const RULES: RuleDef[];
7
+ export declare const claudeMdLinter: Linter;
8
+ export { RULES as CLAUDE_MD_RULES };
@@ -0,0 +1,109 @@
1
+ import { isRuleEnabled, getRuleSeverity } from "../types.js";
2
+ const RULES = [
3
+ { id: "claude-md/not-empty", defaultSeverity: "warning" },
4
+ { id: "claude-md/starts-with-heading", defaultSeverity: "info" },
5
+ { id: "claude-md/has-sections", defaultSeverity: "warning" },
6
+ { id: "claude-md/user-level-concise", defaultSeverity: "info" },
7
+ { id: "claude-md/project-has-overview", defaultSeverity: "info" },
8
+ { id: "claude-md/no-secrets", defaultSeverity: "error" },
9
+ { id: "claude-md/file-length", defaultSeverity: "warning" },
10
+ { id: "claude-md/no-absolute-paths", defaultSeverity: "info" },
11
+ { id: "claude-md/no-todo-markers", defaultSeverity: "info" },
12
+ { id: "claude-md/no-trailing-whitespace", defaultSeverity: "info" },
13
+ ];
14
+ function diag(config, filePath, ruleId, defaultSeverity, message, line, column) {
15
+ if (!isRuleEnabled(config, ruleId))
16
+ return null;
17
+ return {
18
+ rule: ruleId,
19
+ severity: getRuleSeverity(config, ruleId, defaultSeverity),
20
+ message,
21
+ file: filePath,
22
+ line,
23
+ column,
24
+ };
25
+ }
26
+ export const claudeMdLinter = {
27
+ artifactType: "claude-md",
28
+ lint(filePath, content, config, scope) {
29
+ const diagnostics = [];
30
+ const push = (d) => { if (d)
31
+ diagnostics.push(d); };
32
+ const lines = content.split("\n");
33
+ // empty file
34
+ if (!content.trim()) {
35
+ push(diag(config, filePath, "claude-md/not-empty", "warning", "CLAUDE.md is empty"));
36
+ return diagnostics;
37
+ }
38
+ // should start with a heading
39
+ const firstNonEmpty = lines.findIndex((l) => l.trim() !== "");
40
+ if (firstNonEmpty >= 0 && !lines[firstNonEmpty].startsWith("#")) {
41
+ push(diag(config, filePath, "claude-md/starts-with-heading", "info", "CLAUDE.md should start with a heading", firstNonEmpty + 1));
42
+ }
43
+ // check for H2 sections (structure)
44
+ const h2Count = lines.filter((l) => /^## /.test(l)).length;
45
+ if (h2Count === 0) {
46
+ push(diag(config, filePath, "claude-md/has-sections", "warning", "CLAUDE.md should have H2 (##) sections for organization"));
47
+ }
48
+ // Scope-aware: user-level should be concise global rules, project-level should describe the project
49
+ if (scope === "user" && lines.length > 100) {
50
+ push(diag(config, filePath, "claude-md/user-level-concise", "info", `User-level CLAUDE.md is ${lines.length} lines — keep global rules concise, put project-specific content in project CLAUDE.md files`));
51
+ }
52
+ if (scope === "project") {
53
+ // Project CLAUDE.md should have a project description
54
+ const hasProjectOverview = lines.some((l) => /^#+ .*(overview|project|about|description|what this is|introduction|summary)/i.test(l));
55
+ if (!hasProjectOverview && h2Count > 0) {
56
+ push(diag(config, filePath, "claude-md/project-has-overview", "info", "Project CLAUDE.md should include a project overview section"));
57
+ }
58
+ }
59
+ // detect potential secrets
60
+ for (let i = 0; i < lines.length; i++) {
61
+ const line = lines[i];
62
+ // API keys, tokens, passwords in plain text
63
+ const secretMatch = /(?:api[_-]?key|token|password|secret)\s*[:=]\s*["']?[A-Za-z0-9_\-/.]{20,}/i.exec(line);
64
+ if (secretMatch) {
65
+ push(diag(config, filePath, "claude-md/no-secrets", "error", "Possible secret or token detected — do not store credentials in CLAUDE.md", i + 1, secretMatch.index + 1));
66
+ }
67
+ }
68
+ // large file warning
69
+ if (lines.length > 500) {
70
+ push(diag(config, filePath, "claude-md/file-length", "warning", `CLAUDE.md is ${lines.length} lines — consider splitting into focused sections or separate files`));
71
+ }
72
+ // check for broken markdown links to local files
73
+ const linkRe = /\[([^\]]*)\]\(([^)]+)\)/g;
74
+ for (let i = 0; i < lines.length; i++) {
75
+ let match;
76
+ while ((match = linkRe.exec(lines[i])) !== null) {
77
+ const target = match[2];
78
+ // skip URLs and anchors
79
+ if (target.startsWith("http://") || target.startsWith("https://") || target.startsWith("#")) {
80
+ continue;
81
+ }
82
+ // warn about absolute paths
83
+ if (target.startsWith("/")) {
84
+ push(diag(config, filePath, "claude-md/no-absolute-paths", "info", `Link uses absolute path "${target}" — prefer relative paths`, i + 1, match.index + 1));
85
+ }
86
+ }
87
+ }
88
+ // check for TODO/FIXME markers left in instructions
89
+ for (let i = 0; i < lines.length; i++) {
90
+ const todoMatch = /\b(TODO|FIXME|HACK|XXX)\b/.exec(lines[i]);
91
+ if (todoMatch) {
92
+ push(diag(config, filePath, "claude-md/no-todo-markers", "info", `Found ${todoMatch[0]} marker — resolve before finalizing`, i + 1, todoMatch.index + 1));
93
+ }
94
+ }
95
+ // trailing whitespace (formatting)
96
+ let trailingCount = 0;
97
+ for (let i = 0; i < lines.length; i++) {
98
+ if (/[ \t]+$/.test(lines[i])) {
99
+ trailingCount++;
100
+ }
101
+ }
102
+ if (trailingCount > 0) {
103
+ push(diag(config, filePath, "claude-md/no-trailing-whitespace", "info", `${trailingCount} line${trailingCount !== 1 ? "s" : ""} with trailing whitespace`));
104
+ }
105
+ return diagnostics;
106
+ },
107
+ };
108
+ export { RULES as CLAUDE_MD_RULES };
109
+ //# sourceMappingURL=claude-md.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-md.js","sourceRoot":"","sources":["../../src/linters/claude-md.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { Linter, Severity } from "../types.js";
2
+ interface RuleDef {
3
+ id: string;
4
+ defaultSeverity: Severity;
5
+ }
6
+ declare const RULES: RuleDef[];
7
+ export declare const commandMdLinter: Linter;
8
+ export { RULES as COMMAND_MD_RULES };
@@ -0,0 +1,61 @@
1
+ import { isRuleEnabled, getRuleSeverity } from "../types.js";
2
+ import { parseFrontmatter } from "../utils/frontmatter.js";
3
+ const RULES = [
4
+ { id: "command-md/valid-frontmatter", defaultSeverity: "error" },
5
+ { id: "command-md/description-required", defaultSeverity: "error" },
6
+ { id: "command-md/allowed-tools-valid", defaultSeverity: "warning" },
7
+ { id: "command-md/body-present", defaultSeverity: "warning" },
8
+ ];
9
+ const KNOWN_TOOLS = new Set([
10
+ "Read", "Write", "Edit", "Bash", "Glob", "Grep",
11
+ "WebFetch", "WebSearch", "Agent", "AskUserQuestion",
12
+ "NotebookEdit", "TodoWrite", "EnterPlanMode", "ExitPlanMode",
13
+ ]);
14
+ function diag(config, filePath, ruleId, defaultSeverity, message, line, column) {
15
+ if (!isRuleEnabled(config, ruleId))
16
+ return null;
17
+ return {
18
+ rule: ruleId,
19
+ severity: getRuleSeverity(config, ruleId, defaultSeverity),
20
+ message,
21
+ file: filePath,
22
+ line,
23
+ column,
24
+ };
25
+ }
26
+ export const commandMdLinter = {
27
+ artifactType: "command-md",
28
+ lint(filePath, content, config) {
29
+ const diagnostics = [];
30
+ const push = (d) => { if (d)
31
+ diagnostics.push(d); };
32
+ const fm = parseFrontmatter(content);
33
+ if (!fm.valid) {
34
+ push(diag(config, filePath, "command-md/valid-frontmatter", "error", fm.error ?? "Invalid frontmatter"));
35
+ return diagnostics;
36
+ }
37
+ // description
38
+ if (!("description" in fm.data) || typeof fm.data.description !== "string") {
39
+ push(diag(config, filePath, "command-md/description-required", "error", "\"description\" is required in frontmatter"));
40
+ }
41
+ // allowed-tools
42
+ if ("allowed-tools" in fm.data) {
43
+ const tools = fm.data["allowed-tools"];
44
+ if (Array.isArray(tools)) {
45
+ for (const t of tools) {
46
+ if (typeof t === "string" && !KNOWN_TOOLS.has(t)) {
47
+ push(diag(config, filePath, "command-md/allowed-tools-valid", "warning", `Unknown tool "${t}" in allowed-tools`));
48
+ }
49
+ }
50
+ }
51
+ }
52
+ // body
53
+ const body = fm.body.trim();
54
+ if (!body) {
55
+ push(diag(config, filePath, "command-md/body-present", "warning", "Command should have a body with instructions"));
56
+ }
57
+ return diagnostics;
58
+ },
59
+ };
60
+ export { RULES as COMMAND_MD_RULES };
61
+ //# sourceMappingURL=command-md.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-md.js","sourceRoot":"","sources":["../../src/linters/command-md.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { Linter, Severity } from "../types.js";
2
+ interface RuleDef {
3
+ id: string;
4
+ defaultSeverity: Severity;
5
+ }
6
+ declare const RULES: RuleDef[];
7
+ export declare const hooksJsonLinter: Linter;
8
+ export { RULES as HOOKS_JSON_RULES };
@@ -0,0 +1,123 @@
1
+ import { isRuleEnabled, getRuleSeverity } from "../types.js";
2
+ const RULES = [
3
+ { id: "hooks-json/valid-json", defaultSeverity: "error" },
4
+ { id: "hooks-json/root-hooks-key", defaultSeverity: "error" },
5
+ { id: "hooks-json/valid-event-names", defaultSeverity: "error" },
6
+ { id: "hooks-json/hook-type-required", defaultSeverity: "error" },
7
+ { id: "hooks-json/command-has-command", defaultSeverity: "error" },
8
+ { id: "hooks-json/no-hardcoded-paths", defaultSeverity: "warning" },
9
+ { id: "hooks-json/prompt-has-prompt", defaultSeverity: "error" },
10
+ { id: "hooks-json/prompt-event-support", defaultSeverity: "warning" },
11
+ { id: "hooks-json/timeout-range", defaultSeverity: "warning" },
12
+ ];
13
+ const VALID_EVENTS = new Set([
14
+ "PreToolUse", "PostToolUse", "UserPromptSubmit",
15
+ "Stop", "SubagentStop", "SessionStart", "SessionEnd",
16
+ "PreCompact", "Notification",
17
+ ]);
18
+ const PROMPT_EVENTS = new Set([
19
+ "Stop", "SubagentStop", "UserPromptSubmit", "PreToolUse",
20
+ ]);
21
+ function findKeyPosition(content, key) {
22
+ const re = new RegExp(`"${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"\\s*:`);
23
+ const match = re.exec(content);
24
+ if (!match)
25
+ return undefined;
26
+ const before = content.slice(0, match.index);
27
+ const line = before.split("\n").length;
28
+ const lastNl = before.lastIndexOf("\n");
29
+ const column = match.index - lastNl;
30
+ return { line, column };
31
+ }
32
+ function diag(config, filePath, ruleId, defaultSeverity, message, line, column) {
33
+ if (!isRuleEnabled(config, ruleId))
34
+ return null;
35
+ return {
36
+ rule: ruleId,
37
+ severity: getRuleSeverity(config, ruleId, defaultSeverity),
38
+ message,
39
+ file: filePath,
40
+ line,
41
+ column,
42
+ };
43
+ }
44
+ export const hooksJsonLinter = {
45
+ artifactType: "hooks-json",
46
+ lint(filePath, content, config) {
47
+ const diagnostics = [];
48
+ const push = (d) => { if (d)
49
+ diagnostics.push(d); };
50
+ let parsed;
51
+ try {
52
+ parsed = JSON.parse(content);
53
+ }
54
+ catch (e) {
55
+ push(diag(config, filePath, "hooks-json/valid-json", "error", `Invalid JSON: ${e.message}`));
56
+ return diagnostics;
57
+ }
58
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
59
+ push(diag(config, filePath, "hooks-json/valid-json", "error", "hooks.json must be a JSON object"));
60
+ return diagnostics;
61
+ }
62
+ const root = parsed;
63
+ // root "hooks" key
64
+ if (!("hooks" in root) || typeof root.hooks !== "object" || root.hooks === null) {
65
+ push(diag(config, filePath, "hooks-json/root-hooks-key", "error", "Root must have a \"hooks\" key containing event definitions"));
66
+ return diagnostics;
67
+ }
68
+ const hooks = root.hooks;
69
+ for (const [eventName, matchers] of Object.entries(hooks)) {
70
+ const ep = findKeyPosition(content, eventName);
71
+ // valid event name
72
+ if (!VALID_EVENTS.has(eventName)) {
73
+ push(diag(config, filePath, "hooks-json/valid-event-names", "error", `Invalid event name "${eventName}" (valid: ${[...VALID_EVENTS].join(", ")})`, ep?.line, ep?.column));
74
+ continue;
75
+ }
76
+ if (!Array.isArray(matchers))
77
+ continue;
78
+ for (const matcher of matchers) {
79
+ if (typeof matcher !== "object" || matcher === null)
80
+ continue;
81
+ const m = matcher;
82
+ const hookList = m.hooks;
83
+ if (!Array.isArray(hookList))
84
+ continue;
85
+ for (const hook of hookList) {
86
+ if (typeof hook !== "object" || hook === null)
87
+ continue;
88
+ const h = hook;
89
+ // type required
90
+ if (!("type" in h) || (h.type !== "command" && h.type !== "prompt")) {
91
+ push(diag(config, filePath, "hooks-json/hook-type-required", "error", `Hook in ${eventName} must have "type" set to "command" or "prompt"`, ep?.line, ep?.column));
92
+ continue;
93
+ }
94
+ if (h.type === "command") {
95
+ if (!("command" in h) || typeof h.command !== "string") {
96
+ push(diag(config, filePath, "hooks-json/command-has-command", "error", `Command hook in ${eventName} must have a "command" field`, ep?.line, ep?.column));
97
+ }
98
+ else if (/^\//.test(h.command) && !h.command.includes("${CLAUDE_PLUGIN_ROOT}")) {
99
+ push(diag(config, filePath, "hooks-json/no-hardcoded-paths", "warning", `Hook command uses absolute path — use \${CLAUDE_PLUGIN_ROOT} instead`, ep?.line, ep?.column));
100
+ }
101
+ }
102
+ if (h.type === "prompt") {
103
+ if (!("prompt" in h) || typeof h.prompt !== "string") {
104
+ push(diag(config, filePath, "hooks-json/prompt-has-prompt", "error", `Prompt hook in ${eventName} must have a "prompt" field`, ep?.line, ep?.column));
105
+ }
106
+ if (!PROMPT_EVENTS.has(eventName)) {
107
+ push(diag(config, filePath, "hooks-json/prompt-event-support", "warning", `Prompt hooks work best on ${[...PROMPT_EVENTS].join(", ")} (used on ${eventName})`, ep?.line, ep?.column));
108
+ }
109
+ }
110
+ // timeout
111
+ if ("timeout" in h && typeof h.timeout === "number") {
112
+ if (h.timeout < 5 || h.timeout > 600) {
113
+ push(diag(config, filePath, "hooks-json/timeout-range", "warning", `Hook timeout ${h.timeout}s is outside recommended range (5-600s)`, ep?.line, ep?.column));
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ return diagnostics;
120
+ },
121
+ };
122
+ export { RULES as HOOKS_JSON_RULES };
123
+ //# sourceMappingURL=hooks-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks-json.js","sourceRoot":"","sources":["../../src/linters/hooks-json.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAI7D,MAAM,KAAK,GAAc;IACvB,EAAE,EAAE,EAAE,uBAAuB,EAAE,eAAe,EAAE,OAAO,EAAE;IACzD,EAAE,EAAE,EAAE,2BAA2B,EAAE,eAAe,EAAE,OAAO,EAAE;IAC7D,EAAE,EAAE,EAAE,8BAA8B,EAAE,eAAe,EAAE,OAAO,EAAE;IAChE,EAAE,EAAE,EAAE,+BAA+B,EAAE,eAAe,EAAE,OAAO,EAAE;IACjE,EAAE,EAAE,EAAE,gCAAgC,EAAE,eAAe,EAAE,OAAO,EAAE;IAClE,EAAE,EAAE,EAAE,+BAA+B,EAAE,eAAe,EAAE,SAAS,EAAE;IACnE,EAAE,EAAE,EAAE,8BAA8B,EAAE,eAAe,EAAE,OAAO,EAAE;IAChE,EAAE,EAAE,EAAE,iCAAiC,EAAE,eAAe,EAAE,SAAS,EAAE;IACrE,EAAE,EAAE,EAAE,0BAA0B,EAAE,eAAe,EAAE,SAAS,EAAE;CAC/D,CAAC;AAEF,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAC3B,YAAY,EAAE,aAAa,EAAE,kBAAkB;IAC/C,MAAM,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY;IACpD,YAAY,EAAE,cAAc;CAC7B,CAAC,CAAC;AAEH,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IAC5B,MAAM,EAAE,cAAc,EAAE,kBAAkB,EAAE,YAAY;CACzD,CAAC,CAAC;AAEH,SAAS,eAAe,CAAC,OAAe,EAAE,GAAW;IACnD,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC9E,MAAM,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/B,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;IACvC,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;IACpC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC;AAED,SAAS,IAAI,CACX,MAAoB,EACpB,QAAgB,EAChB,MAAc,EACd,eAAyB,EACzB,OAAe,EACf,IAAa,EACb,MAAe;IAEf,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAChD,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,QAAQ,EAAE,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,CAAC;QAC1D,OAAO;QACP,IAAI,EAAE,QAAQ;QACd,IAAI;QACJ,MAAM;KACP,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,eAAe,GAAW;IACrC,YAAY,EAAE,YAAY;IAE1B,IAAI,CAAC,QAAgB,EAAE,OAAe,EAAE,MAAoB;QAC1D,MAAM,WAAW,GAAqB,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,CAAC,CAAwB,EAAE,EAAE,GAAG,IAAI,CAAC;YAAE,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAE3E,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC/B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,uBAAuB,EAAE,OAAO,EAC1D,iBAAkB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC5C,OAAO,WAAW,CAAC;QACrB,CAAC;QAED,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3E,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,uBAAuB,EAAE,OAAO,EAC1D,kCAAkC,CAAC,CAAC,CAAC;YACvC,OAAO,WAAW,CAAC;QACrB,CAAC;QAED,MAAM,IAAI,GAAG,MAAiC,CAAC;QAE/C,mBAAmB;QACnB,IAAI,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YAChF,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,2BAA2B,EAAE,OAAO,EAC9D,6DAA6D,CAAC,CAAC,CAAC;YAClE,OAAO,WAAW,CAAC;QACrB,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAgC,CAAC;QAEpD,KAAK,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1D,MAAM,EAAE,GAAG,eAAe,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;YAC/C,mBAAmB;YACnB,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;gBACjC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,8BAA8B,EAAE,OAAO,EACjE,uBAAuB,SAAS,aAAa,CAAC,GAAG,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;gBACvG,SAAS;YACX,CAAC;YAED,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;gBAAE,SAAS;YAEvC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;gBAC/B,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI;oBAAE,SAAS;gBAC9D,MAAM,CAAC,GAAG,OAAkC,CAAC;gBAC7C,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC;gBACzB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;oBAAE,SAAS;gBAEvC,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;oBAC5B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;wBAAE,SAAS;oBACxD,MAAM,CAAC,GAAG,IAA+B,CAAC;oBAE1C,gBAAgB;oBAChB,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,EAAE,CAAC;wBACpE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,+BAA+B,EAAE,OAAO,EAClE,WAAW,SAAS,gDAAgD,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;wBAC/F,SAAS;oBACX,CAAC;oBAED,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;wBACzB,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;4BACvD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,gCAAgC,EAAE,OAAO,EACnE,mBAAmB,SAAS,8BAA8B,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;wBACvF,CAAC;6BAAM,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,CAAC;4BACjF,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,+BAA+B,EAAE,SAAS,EACpE,sEAAsE,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;wBACnG,CAAC;oBACH,CAAC;oBAED,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;wBACxB,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;4BACrD,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,8BAA8B,EAAE,OAAO,EACjE,kBAAkB,SAAS,6BAA6B,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;wBACrF,CAAC;wBACD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;4BAClC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,iCAAiC,EAAE,SAAS,EACtE,6BAA6B,CAAC,GAAG,aAAa,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,SAAS,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;wBAChH,CAAC;oBACH,CAAC;oBAED,UAAU;oBACV,IAAI,SAAS,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;wBACpD,IAAI,CAAC,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC;4BACrC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,0BAA0B,EAAE,SAAS,EAC/D,gBAAgB,CAAC,CAAC,OAAO,yCAAyC,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;wBAC/F,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;CACF,CAAC;AAEF,OAAO,EAAE,KAAK,IAAI,gBAAgB,EAAE,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Linter, Severity } from "../types.js";
2
+ interface RuleDef {
3
+ id: string;
4
+ defaultSeverity: Severity;
5
+ }
6
+ declare const RULES: RuleDef[];
7
+ export declare const mcpJsonLinter: Linter;
8
+ export { RULES as MCP_JSON_RULES };
@@ -0,0 +1,160 @@
1
+ import { basename } from "node:path";
2
+ import { isRuleEnabled, getRuleSeverity } from "../types.js";
3
+ import { isKebabCase } from "../utils/kebab-case.js";
4
+ const RULES = [
5
+ { id: "mcp-json/scope-file-name", defaultSeverity: "warning" },
6
+ { id: "mcp-json/valid-json", defaultSeverity: "error" },
7
+ { id: "mcp-json/servers-required", defaultSeverity: "error" },
8
+ { id: "mcp-json/servers-object", defaultSeverity: "error" },
9
+ { id: "mcp-json/server-name-kebab", defaultSeverity: "info" },
10
+ { id: "mcp-json/server-object", defaultSeverity: "error" },
11
+ { id: "mcp-json/server-transport", defaultSeverity: "error" },
12
+ { id: "mcp-json/url-protocol", defaultSeverity: "warning" },
13
+ { id: "mcp-json/url-valid", defaultSeverity: "error" },
14
+ { id: "mcp-json/type-matches-transport", defaultSeverity: "warning" },
15
+ { id: "mcp-json/command-args-split", defaultSeverity: "info" },
16
+ { id: "mcp-json/args-array", defaultSeverity: "error" },
17
+ { id: "mcp-json/env-object", defaultSeverity: "error" },
18
+ { id: "mcp-json/env-string-values", defaultSeverity: "warning" },
19
+ { id: "mcp-json/no-unknown-server-fields", defaultSeverity: "info" },
20
+ { id: "mcp-json/no-unknown-root-fields", defaultSeverity: "info" },
21
+ ];
22
+ const KNOWN_SERVER_FIELDS = new Set([
23
+ "type", "url", "command", "args", "env", "cwd",
24
+ ]);
25
+ function findKeyPosition(content, key) {
26
+ const re = new RegExp(`"${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"\\s*:`);
27
+ const match = re.exec(content);
28
+ if (!match)
29
+ return undefined;
30
+ const before = content.slice(0, match.index);
31
+ const line = before.split("\n").length;
32
+ const lastNl = before.lastIndexOf("\n");
33
+ const column = match.index - lastNl;
34
+ return { line, column };
35
+ }
36
+ function diag(config, filePath, ruleId, defaultSeverity, message, line, column) {
37
+ if (!isRuleEnabled(config, ruleId))
38
+ return null;
39
+ return {
40
+ rule: ruleId,
41
+ severity: getRuleSeverity(config, ruleId, defaultSeverity),
42
+ message,
43
+ file: filePath,
44
+ line,
45
+ column,
46
+ };
47
+ }
48
+ export const mcpJsonLinter = {
49
+ artifactType: "mcp-json",
50
+ lint(filePath, content, config, scope) {
51
+ const diagnostics = [];
52
+ const push = (d) => { if (d)
53
+ diagnostics.push(d); };
54
+ const fileName = basename(filePath);
55
+ // Scope-aware file naming: user level = mcp.json, project level = .mcp.json
56
+ if (scope === "user" && fileName === ".mcp.json") {
57
+ push(diag(config, filePath, "mcp-json/scope-file-name", "warning", "At user level (~/.claude/), use \"mcp.json\" instead of \".mcp.json\""));
58
+ }
59
+ if (scope === "project" && fileName === "mcp.json") {
60
+ push(diag(config, filePath, "mcp-json/scope-file-name", "warning", "At project level, use \".mcp.json\" (dot-prefixed) instead of \"mcp.json\""));
61
+ }
62
+ let parsed;
63
+ try {
64
+ parsed = JSON.parse(content);
65
+ }
66
+ catch (e) {
67
+ push(diag(config, filePath, "mcp-json/valid-json", "error", `Invalid JSON: ${e.message}`));
68
+ return diagnostics;
69
+ }
70
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
71
+ push(diag(config, filePath, "mcp-json/valid-json", "error", "mcp.json must be a JSON object"));
72
+ return diagnostics;
73
+ }
74
+ // mcpServers required
75
+ if (!("mcpServers" in parsed)) {
76
+ push(diag(config, filePath, "mcp-json/servers-required", "error", "\"mcpServers\" field is required"));
77
+ return diagnostics;
78
+ }
79
+ const servers = parsed.mcpServers;
80
+ if (typeof servers !== "object" || servers === null || Array.isArray(servers)) {
81
+ push(diag(config, filePath, "mcp-json/servers-object", "error", "\"mcpServers\" must be an object"));
82
+ return diagnostics;
83
+ }
84
+ for (const [name, serverDef] of Object.entries(servers)) {
85
+ const sp = findKeyPosition(content, name);
86
+ // server name convention
87
+ if (!isKebabCase(name)) {
88
+ push(diag(config, filePath, "mcp-json/server-name-kebab", "info", `Server name "${name}" should be kebab-case`, sp?.line, sp?.column));
89
+ }
90
+ if (typeof serverDef !== "object" || serverDef === null || Array.isArray(serverDef)) {
91
+ push(diag(config, filePath, "mcp-json/server-object", "error", `Server "${name}" must be an object`, sp?.line, sp?.column));
92
+ continue;
93
+ }
94
+ const server = serverDef;
95
+ // Must have either type+url (http) or command (stdio)
96
+ const hasUrl = "url" in server && typeof server.url === "string";
97
+ const hasCommand = "command" in server && typeof server.command === "string";
98
+ if (!hasUrl && !hasCommand) {
99
+ push(diag(config, filePath, "mcp-json/server-transport", "error", `Server "${name}" must have either "url" (http) or "command" (stdio)`, sp?.line, sp?.column));
100
+ continue;
101
+ }
102
+ // http server checks
103
+ if (hasUrl) {
104
+ const url = server.url;
105
+ try {
106
+ const parsed = new URL(url);
107
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
108
+ push(diag(config, filePath, "mcp-json/url-protocol", "warning", `Server "${name}" URL uses "${parsed.protocol}" — expected http: or https:`, sp?.line, sp?.column));
109
+ }
110
+ }
111
+ catch {
112
+ push(diag(config, filePath, "mcp-json/url-valid", "error", `Server "${name}" has invalid URL: "${url}"`, sp?.line, sp?.column));
113
+ }
114
+ if ("type" in server && server.type !== "http") {
115
+ push(diag(config, filePath, "mcp-json/type-matches-transport", "warning", `Server "${name}" has URL but type is "${server.type}" (expected "http")`, sp?.line, sp?.column));
116
+ }
117
+ }
118
+ // stdio server checks
119
+ if (hasCommand && !hasUrl) {
120
+ const cmd = server.command;
121
+ if (cmd.includes(" ") && !("args" in server)) {
122
+ push(diag(config, filePath, "mcp-json/command-args-split", "info", `Server "${name}" command contains spaces — consider splitting into "command" and "args"`, sp?.line, sp?.column));
123
+ }
124
+ }
125
+ // args must be array
126
+ if ("args" in server && !Array.isArray(server.args)) {
127
+ push(diag(config, filePath, "mcp-json/args-array", "error", `Server "${name}" "args" must be an array`, sp?.line, sp?.column));
128
+ }
129
+ // env must be object of strings
130
+ if ("env" in server) {
131
+ if (typeof server.env !== "object" || server.env === null || Array.isArray(server.env)) {
132
+ push(diag(config, filePath, "mcp-json/env-object", "error", `Server "${name}" "env" must be an object`, sp?.line, sp?.column));
133
+ }
134
+ else {
135
+ for (const [k, v] of Object.entries(server.env)) {
136
+ if (typeof v !== "string") {
137
+ push(diag(config, filePath, "mcp-json/env-string-values", "warning", `Server "${name}" env.${k} should be a string`, sp?.line, sp?.column));
138
+ }
139
+ }
140
+ }
141
+ }
142
+ // unknown fields
143
+ for (const key of Object.keys(server)) {
144
+ if (!KNOWN_SERVER_FIELDS.has(key)) {
145
+ push(diag(config, filePath, "mcp-json/no-unknown-server-fields", "info", `Server "${name}" has unknown field "${key}"`, sp?.line, sp?.column));
146
+ }
147
+ }
148
+ }
149
+ // unknown root fields
150
+ for (const key of Object.keys(parsed)) {
151
+ if (key !== "mcpServers") {
152
+ const p = findKeyPosition(content, key);
153
+ push(diag(config, filePath, "mcp-json/no-unknown-root-fields", "info", `Unknown root field "${key}" (expected only "mcpServers")`, p?.line, p?.column));
154
+ }
155
+ }
156
+ return diagnostics;
157
+ },
158
+ };
159
+ export { RULES as MCP_JSON_RULES };
160
+ //# sourceMappingURL=mcp-json.js.map