@praneeth_54/agentdoctor 0.1.0-beta

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 (112) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/LICENSE +21 -0
  3. package/README.md +348 -0
  4. package/dist/agents/claude/adapter.d.ts +1 -0
  5. package/dist/agents/claude/adapter.js +1 -0
  6. package/dist/agents/claude/detector.d.ts +16 -0
  7. package/dist/agents/claude/detector.js +225 -0
  8. package/dist/agents/codex/adapter.d.ts +1 -0
  9. package/dist/agents/codex/adapter.js +1 -0
  10. package/dist/agents/codex/detector.d.ts +17 -0
  11. package/dist/agents/codex/detector.js +151 -0
  12. package/dist/agents/cursor/adapter.d.ts +1 -0
  13. package/dist/agents/cursor/adapter.js +1 -0
  14. package/dist/agents/cursor/detector.d.ts +17 -0
  15. package/dist/agents/cursor/detector.js +198 -0
  16. package/dist/agents/detect-agents.d.ts +17 -0
  17. package/dist/agents/detect-agents.js +23 -0
  18. package/dist/agents/inspect.d.ts +36 -0
  19. package/dist/agents/inspect.js +165 -0
  20. package/dist/agents/registry.d.ts +7 -0
  21. package/dist/agents/registry.js +11 -0
  22. package/dist/agents/types.d.ts +52 -0
  23. package/dist/agents/types.js +15 -0
  24. package/dist/cli/commands/doctor.d.ts +5 -0
  25. package/dist/cli/commands/doctor.js +22 -0
  26. package/dist/cli/commands/explain.d.ts +5 -0
  27. package/dist/cli/commands/explain.js +51 -0
  28. package/dist/cli/commands/fix.d.ts +8 -0
  29. package/dist/cli/commands/fix.js +19 -0
  30. package/dist/cli/commands/scan.d.ts +10 -0
  31. package/dist/cli/commands/scan.js +59 -0
  32. package/dist/cli/index.d.ts +2 -0
  33. package/dist/cli/index.js +3 -0
  34. package/dist/cli/program.d.ts +3 -0
  35. package/dist/cli/program.js +124 -0
  36. package/dist/constants.d.ts +5 -0
  37. package/dist/constants.js +38 -0
  38. package/dist/core/mcp/parse.d.ts +7 -0
  39. package/dist/core/mcp/parse.js +303 -0
  40. package/dist/core/mcp/types.d.ts +33 -0
  41. package/dist/core/mcp/types.js +1 -0
  42. package/dist/core/rules/build-context.d.ts +10 -0
  43. package/dist/core/rules/build-context.js +31 -0
  44. package/dist/core/rules/context/generated-directory.d.ts +2 -0
  45. package/dist/core/rules/context/generated-directory.js +89 -0
  46. package/dist/core/rules/context/large-instruction-file.d.ts +2 -0
  47. package/dist/core/rules/context/large-instruction-file.js +56 -0
  48. package/dist/core/rules/context/large-log-file.d.ts +2 -0
  49. package/dist/core/rules/context/large-log-file.js +45 -0
  50. package/dist/core/rules/dedupe.d.ts +13 -0
  51. package/dist/core/rules/dedupe.js +53 -0
  52. package/dist/core/rules/ignore.d.ts +23 -0
  53. package/dist/core/rules/ignore.js +97 -0
  54. package/dist/core/rules/instructions/duplicate-content.d.ts +2 -0
  55. package/dist/core/rules/instructions/duplicate-content.js +99 -0
  56. package/dist/core/rules/instructions/empty-instructions.d.ts +2 -0
  57. package/dist/core/rules/instructions/empty-instructions.js +53 -0
  58. package/dist/core/rules/instructions/missing-path-reference.d.ts +2 -0
  59. package/dist/core/rules/instructions/missing-path-reference.js +131 -0
  60. package/dist/core/rules/mcp/mcp-rules.d.ts +3 -0
  61. package/dist/core/rules/mcp/mcp-rules.js +83 -0
  62. package/dist/core/rules/registry.d.ts +6 -0
  63. package/dist/core/rules/registry.js +31 -0
  64. package/dist/core/rules/run-rules.d.ts +16 -0
  65. package/dist/core/rules/run-rules.js +36 -0
  66. package/dist/core/rules/security/claude-bypass-permissions.d.ts +7 -0
  67. package/dist/core/rules/security/claude-bypass-permissions.js +52 -0
  68. package/dist/core/rules/security/env-file-exposure.d.ts +2 -0
  69. package/dist/core/rules/security/env-file-exposure.js +87 -0
  70. package/dist/core/rules/security/mcp-broad-filesystem.d.ts +2 -0
  71. package/dist/core/rules/security/mcp-broad-filesystem.js +42 -0
  72. package/dist/core/rules/security/private-key-file.d.ts +2 -0
  73. package/dist/core/rules/security/private-key-file.js +60 -0
  74. package/dist/core/rules/text-cache.d.ts +19 -0
  75. package/dist/core/rules/text-cache.js +112 -0
  76. package/dist/core/rules/thresholds.d.ts +13 -0
  77. package/dist/core/rules/thresholds.js +13 -0
  78. package/dist/core/rules/types.d.ts +57 -0
  79. package/dist/core/rules/types.js +23 -0
  80. package/dist/core/scanner/scan.d.ts +7 -0
  81. package/dist/core/scanner/scan.js +70 -0
  82. package/dist/core/scoring/placeholder.d.ts +7 -0
  83. package/dist/core/scoring/placeholder.js +29 -0
  84. package/dist/detectors/framework.d.ts +17 -0
  85. package/dist/detectors/framework.js +148 -0
  86. package/dist/detectors/language.d.ts +14 -0
  87. package/dist/detectors/language.js +122 -0
  88. package/dist/detectors/monorepo.d.ts +13 -0
  89. package/dist/detectors/monorepo.js +44 -0
  90. package/dist/detectors/package-manager.d.ts +14 -0
  91. package/dist/detectors/package-manager.js +78 -0
  92. package/dist/detectors/project.d.ts +10 -0
  93. package/dist/detectors/project.js +60 -0
  94. package/dist/discovery/files.d.ts +15 -0
  95. package/dist/discovery/files.js +118 -0
  96. package/dist/index.d.ts +8 -0
  97. package/dist/index.js +7 -0
  98. package/dist/reporters/json/report.d.ts +5 -0
  99. package/dist/reporters/json/report.js +84 -0
  100. package/dist/reporters/terminal/report.d.ts +8 -0
  101. package/dist/reporters/terminal/report.js +147 -0
  102. package/dist/security/redaction.d.ts +9 -0
  103. package/dist/security/redaction.js +26 -0
  104. package/dist/types/index.d.ts +137 -0
  105. package/dist/types/index.js +6 -0
  106. package/dist/utils/colors.d.ts +14 -0
  107. package/dist/utils/colors.js +26 -0
  108. package/dist/utils/fs.d.ts +17 -0
  109. package/dist/utils/fs.js +63 -0
  110. package/dist/utils/path.d.ts +17 -0
  111. package/dist/utils/path.js +37 -0
  112. package/package.json +80 -0
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Minimal gitignore-style pattern matching for .gitignore / .cursorignore.
3
+ * Supports common patterns used in agent ignore files — not a full gitignore clone.
4
+ */
5
+ export interface IgnoreIndex {
6
+ gitignorePatterns: string[];
7
+ cursorignorePatterns: string[];
8
+ matchesGitignore(relativePath: string): boolean;
9
+ matchesCursorignore(relativePath: string): boolean;
10
+ /** Whether a path appears excluded for Cursor agent/indexing purposes. */
11
+ isExcludedForCursor(relativePath: string): boolean;
12
+ }
13
+ export declare function parseIgnoreFile(content: string): string[];
14
+ /**
15
+ * Match a relative POSIX path against a single gitignore-like pattern.
16
+ * Negation (`!`) is not fully supported for re-inclusion of parent-excluded paths.
17
+ */
18
+ export declare function matchIgnorePattern(relativePath: string, pattern: string): boolean;
19
+ export declare function pathMatchesAny(relativePath: string, patterns: string[]): boolean;
20
+ export declare function createIgnoreIndex(options: {
21
+ gitignorePatterns: string[];
22
+ cursorignorePatterns: string[];
23
+ }): IgnoreIndex;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Minimal gitignore-style pattern matching for .gitignore / .cursorignore.
3
+ * Supports common patterns used in agent ignore files — not a full gitignore clone.
4
+ */
5
+ function normalizePattern(pattern) {
6
+ const trimmed = pattern.trim();
7
+ if (!trimmed || trimmed.startsWith("#")) {
8
+ return null;
9
+ }
10
+ return trimmed.replace(/\\ /g, " ");
11
+ }
12
+ export function parseIgnoreFile(content) {
13
+ return content
14
+ .split(/\r?\n/)
15
+ .map(normalizePattern)
16
+ .filter((p) => p !== null);
17
+ }
18
+ /**
19
+ * Match a relative POSIX path against a single gitignore-like pattern.
20
+ * Negation (`!`) is not fully supported for re-inclusion of parent-excluded paths.
21
+ */
22
+ export function matchIgnorePattern(relativePath, pattern) {
23
+ let pat = pattern;
24
+ if (pat.startsWith("!")) {
25
+ pat = pat.slice(1);
26
+ }
27
+ const pathNorm = relativePath.replace(/^\/+/, "");
28
+ let matched = false;
29
+ if (pat.endsWith("/")) {
30
+ const dir = pat.slice(0, -1);
31
+ matched =
32
+ pathNorm === dir ||
33
+ pathNorm.startsWith(`${dir}/`) ||
34
+ globMatch(pathNorm, dir) ||
35
+ pathNorm.split("/").some((_, i, parts) => globMatch(parts.slice(0, i + 1).join("/"), dir));
36
+ }
37
+ else if (pat.startsWith("/")) {
38
+ matched = globMatch(pathNorm, pat.slice(1));
39
+ }
40
+ else if (pat.includes("/")) {
41
+ matched = globMatch(pathNorm, pat) || pathNorm.endsWith(`/${pat}`);
42
+ }
43
+ else {
44
+ const base = pathNorm.split("/").pop() ?? pathNorm;
45
+ matched = globMatch(base, pat) || globMatch(pathNorm, pat) || globMatch(pathNorm, `**/${pat}`);
46
+ }
47
+ return matched;
48
+ }
49
+ function globMatch(value, pattern) {
50
+ const escaped = pattern
51
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
52
+ .replace(/\*\*/g, "\0")
53
+ .replace(/\*/g, "[^/]*")
54
+ .replace(/\?/g, "[^/]")
55
+ .replace(/\0/g, ".*");
56
+ return new RegExp(`^${escaped}$`).test(value);
57
+ }
58
+ export function pathMatchesAny(relativePath, patterns) {
59
+ let ignored = false;
60
+ for (const pattern of patterns) {
61
+ if (pattern.startsWith("!")) {
62
+ if (matchIgnorePattern(relativePath, pattern.slice(1))) {
63
+ ignored = false;
64
+ }
65
+ }
66
+ else if (matchIgnorePattern(relativePath, pattern)) {
67
+ ignored = true;
68
+ }
69
+ }
70
+ return ignored;
71
+ }
72
+ /** Cursor documents default ignore of .env* (see cursor.com/docs/reference/ignore-file). */
73
+ const CURSOR_DEFAULT_ENV_PATTERNS = [".env", ".env.*", "**/.env", "**/.env.*"];
74
+ export function createIgnoreIndex(options) {
75
+ const gitignorePatterns = options.gitignorePatterns;
76
+ const cursorignorePatterns = options.cursorignorePatterns;
77
+ return {
78
+ gitignorePatterns,
79
+ cursorignorePatterns,
80
+ matchesGitignore(relativePath) {
81
+ return pathMatchesAny(relativePath, gitignorePatterns);
82
+ },
83
+ matchesCursorignore(relativePath) {
84
+ return pathMatchesAny(relativePath, cursorignorePatterns);
85
+ },
86
+ isExcludedForCursor(relativePath) {
87
+ if (pathMatchesAny(relativePath, cursorignorePatterns)) {
88
+ return true;
89
+ }
90
+ if (pathMatchesAny(relativePath, gitignorePatterns)) {
91
+ return true;
92
+ }
93
+ // Documented default ignore list includes .env*
94
+ return pathMatchesAny(relativePath, CURSOR_DEFAULT_ENV_PATTERNS);
95
+ },
96
+ };
97
+ }
@@ -0,0 +1,2 @@
1
+ import type { RuleDefinition } from "../types.js";
2
+ export declare const duplicateContentRule: RuleDefinition;
@@ -0,0 +1,99 @@
1
+ import { THRESHOLDS } from "../thresholds.js";
2
+ const COMPARE_KINDS = new Set([
3
+ "cursor-rule-mdc",
4
+ "cursor-legacy-cursorrules",
5
+ "agents-md",
6
+ "agents-override-md",
7
+ "claude-md",
8
+ "claude-local-md",
9
+ "claude-rule-md",
10
+ ]);
11
+ function normalizeContent(text) {
12
+ return text
13
+ .replace(/\r\n/g, "\n")
14
+ .replace(/[ \t]+\n/g, "\n")
15
+ .replace(/\n{3,}/g, "\n\n")
16
+ .trim()
17
+ .toLowerCase();
18
+ }
19
+ /** Strip simple MDC/YAML frontmatter for comparison. */
20
+ function stripFrontmatter(text) {
21
+ if (!text.startsWith("---")) {
22
+ return text;
23
+ }
24
+ const end = text.indexOf("\n---", 3);
25
+ if (end === -1) {
26
+ return text;
27
+ }
28
+ return text.slice(end + 4);
29
+ }
30
+ export const duplicateContentRule = {
31
+ id: "instructions/duplicate-content",
32
+ title: "Duplicate agent instructions",
33
+ description: "Detects exact normalized duplicate instruction content across agent config files.",
34
+ category: "instructions",
35
+ severity: "info",
36
+ fixability: "review",
37
+ rationale: "Duplicated instructions across agents can drift over time; sharing a canonical source reduces maintenance.",
38
+ recommendation: "Prefer one canonical instruction file where tooling allows (for example CLAUDE.md importing AGENTS.md), accepting intentional duplication for cross-agent compatibility when needed.",
39
+ async check(context) {
40
+ const files = [];
41
+ const seenPaths = new Set();
42
+ for (const agent of context.agents) {
43
+ for (const file of agent.configFiles) {
44
+ if (!COMPARE_KINDS.has(file.kind) || file.empty || !file.readable) {
45
+ continue;
46
+ }
47
+ if (seenPaths.has(file.relativePath)) {
48
+ const existing = files.find((f) => f.path === file.relativePath);
49
+ existing?.agents.push(agent.id);
50
+ continue;
51
+ }
52
+ seenPaths.add(file.relativePath);
53
+ const cached = await context.textCache.read(file.relativePath);
54
+ if (!cached.text || cached.binary) {
55
+ continue;
56
+ }
57
+ const normalized = normalizeContent(stripFrontmatter(cached.text));
58
+ if (normalized.length < THRESHOLDS.duplicateMinChars) {
59
+ continue;
60
+ }
61
+ files.push({ path: file.relativePath, normalized, agents: [agent.id] });
62
+ }
63
+ }
64
+ const findings = [];
65
+ const reported = new Set();
66
+ for (let i = 0; i < files.length; i += 1) {
67
+ for (let j = i + 1; j < files.length; j += 1) {
68
+ const a = files[i];
69
+ const b = files[j];
70
+ if (!a || !b)
71
+ continue;
72
+ if (a.normalized !== b.normalized) {
73
+ continue;
74
+ }
75
+ const pairKey = [a.path, b.path].sort().join("|");
76
+ if (reported.has(pairKey)) {
77
+ continue;
78
+ }
79
+ reported.add(pairKey);
80
+ const affected = context.agents
81
+ .filter((agent) => agent.configPaths.includes(a.path) || agent.configPaths.includes(b.path))
82
+ .map((agent) => agent.id);
83
+ findings.push({
84
+ ruleId: "instructions/duplicate-content",
85
+ category: "instructions",
86
+ severity: "info",
87
+ title: "Duplicate agent instructions",
88
+ message: `${a.path} and ${b.path} contain identical normalized project instructions`,
89
+ whyItMatters: "Duplication can be intentional for cross-agent compatibility, but identical copies often drift. Prefer a single canonical source when supported.",
90
+ recommendation: "Keep shared guidance in one file and reference/import it from agent-specific files where the tool supports that.",
91
+ affectedAgents: affected,
92
+ evidence: { path: a.path, detail: `duplicate_of=${b.path}` },
93
+ fixability: "review",
94
+ });
95
+ }
96
+ }
97
+ return findings;
98
+ },
99
+ };
@@ -0,0 +1,2 @@
1
+ import type { RuleDefinition } from "../types.js";
2
+ export declare const emptyInstructionsRule: RuleDefinition;
@@ -0,0 +1,53 @@
1
+ const EMPTYABLE_KINDS = new Set([
2
+ "cursor-rule-mdc",
3
+ "cursor-legacy-cursorrules",
4
+ "agents-md",
5
+ "agents-override-md",
6
+ "claude-md",
7
+ "claude-local-md",
8
+ "claude-rule-md",
9
+ ]);
10
+ export const emptyInstructionsRule = {
11
+ id: "instructions/empty-instructions",
12
+ title: "Empty agent instruction file",
13
+ description: "Flags empty project instruction files that agents will skip or ignore.",
14
+ category: "instructions",
15
+ severity: "warning",
16
+ fixability: "review",
17
+ rationale: "Empty instruction files suggest incomplete setup and waste discovery attention.",
18
+ recommendation: "Add meaningful project guidance or remove the empty file.",
19
+ async check(context) {
20
+ const findings = [];
21
+ const seen = new Set();
22
+ for (const agent of context.agents) {
23
+ for (const file of agent.configFiles) {
24
+ if (!EMPTYABLE_KINDS.has(file.kind)) {
25
+ continue;
26
+ }
27
+ if (!file.empty || !file.readable) {
28
+ continue;
29
+ }
30
+ if (seen.has(file.relativePath)) {
31
+ continue;
32
+ }
33
+ seen.add(file.relativePath);
34
+ const affected = context.agents
35
+ .filter((a) => a.configPaths.includes(file.relativePath))
36
+ .map((a) => a.id);
37
+ findings.push({
38
+ ruleId: "instructions/empty-instructions",
39
+ category: "instructions",
40
+ severity: "warning",
41
+ title: "Empty agent instruction file",
42
+ message: `${file.relativePath} exists but is empty`,
43
+ whyItMatters: "Empty instruction files do not guide agents (Codex skips empty AGENTS.md) and can indicate unfinished configuration.",
44
+ recommendation: "Populate the file with concise project guidance or delete it.",
45
+ affectedAgents: affected.length > 0 ? affected : [agent.id],
46
+ evidence: { path: file.relativePath },
47
+ fixability: "review",
48
+ });
49
+ }
50
+ }
51
+ return findings;
52
+ },
53
+ };
@@ -0,0 +1,2 @@
1
+ import type { RuleDefinition } from "../types.js";
2
+ export declare const missingPathReferenceRule: RuleDefinition;
@@ -0,0 +1,131 @@
1
+ import path from "node:path";
2
+ import { pathExistsInsideRoot } from "../../../agents/inspect.js";
3
+ import { isPathInsideRoot } from "../../../utils/path.js";
4
+ const INSTRUCTION_KINDS = new Set([
5
+ "cursor-rule-mdc",
6
+ "cursor-legacy-cursorrules",
7
+ "agents-md",
8
+ "agents-override-md",
9
+ "claude-md",
10
+ "claude-local-md",
11
+ "claude-rule-md",
12
+ ]);
13
+ /**
14
+ * Conservative local path extraction:
15
+ * - Markdown links: [text](relative/path.ext)
16
+ * - Backticked paths with separators or extensions
17
+ */
18
+ function extractPathCandidates(text) {
19
+ const found = new Set();
20
+ for (const match of text.matchAll(/\[[^\]]*]\(([^)]+)\)/g)) {
21
+ const target = (match[1] ?? "").trim();
22
+ if (isConservativeLocalPath(target)) {
23
+ found.add(stripAnchor(target));
24
+ }
25
+ }
26
+ for (const match of text.matchAll(/`([^`]+)`/g)) {
27
+ const target = (match[1] ?? "").trim();
28
+ if (isConservativeLocalPath(target)) {
29
+ found.add(stripAnchor(target));
30
+ }
31
+ }
32
+ return [...found];
33
+ }
34
+ function stripAnchor(value) {
35
+ return value.split("#")[0] ?? value;
36
+ }
37
+ function isConservativeLocalPath(value) {
38
+ if (!value || value.length > 200)
39
+ return false;
40
+ if (/^(https?:|mailto:|file:)/i.test(value))
41
+ return false;
42
+ if (value.startsWith("#"))
43
+ return false;
44
+ if (value.includes("://"))
45
+ return false;
46
+ if (value.includes("\n") || value.includes(" "))
47
+ return false;
48
+ // Must look like a path: has / or a file extension
49
+ if (!value.includes("/") && !/\.[a-z0-9]{1,8}$/i.test(value))
50
+ return false;
51
+ // Reject shell-looking fragments
52
+ if (/[;&|$]/.test(value))
53
+ return false;
54
+ // Reject traversal escape attempts — still check but flag separately
55
+ return true;
56
+ }
57
+ export const missingPathReferenceRule = {
58
+ id: "instructions/missing-path-reference",
59
+ title: "Instruction references a missing path",
60
+ description: "Detects markdown/backtick local path references in instruction files that do not exist.",
61
+ category: "instructions",
62
+ severity: "warning",
63
+ fixability: "manual",
64
+ rationale: "Broken path references mislead agents and waste tool-call attempts.",
65
+ recommendation: "Update or remove the stale path reference.",
66
+ async check(context) {
67
+ const findings = [];
68
+ const seen = new Set();
69
+ for (const agent of context.agents) {
70
+ for (const file of agent.configFiles) {
71
+ if (!INSTRUCTION_KINDS.has(file.kind) || file.empty || !file.readable) {
72
+ continue;
73
+ }
74
+ if (seen.has(file.relativePath)) {
75
+ continue;
76
+ }
77
+ seen.add(file.relativePath);
78
+ const cached = await context.textCache.read(file.relativePath);
79
+ if (!cached.text || cached.binary) {
80
+ continue;
81
+ }
82
+ const candidates = extractPathCandidates(cached.text);
83
+ for (const candidate of candidates) {
84
+ // Resolve relative to instruction file directory, stay inside root
85
+ const baseDir = path.posix.dirname(file.relativePath);
86
+ const resolvedRelative = path.posix.normalize(baseDir === "." ? candidate : path.posix.join(baseDir, candidate));
87
+ if (resolvedRelative.startsWith("../") || resolvedRelative === "..") {
88
+ findings.push({
89
+ ruleId: "instructions/missing-path-reference",
90
+ category: "instructions",
91
+ severity: "warning",
92
+ title: "Instruction references a missing path",
93
+ message: `${file.relativePath} references \`${candidate}\`, which escapes the repository root`,
94
+ whyItMatters: "Path references outside the repository cannot be validated and may confuse agents.",
95
+ recommendation: "Use repository-relative paths only.",
96
+ affectedAgents: context.agents
97
+ .filter((a) => a.configPaths.includes(file.relativePath))
98
+ .map((a) => a.id),
99
+ evidence: { path: file.relativePath, detail: `ref=${candidate}` },
100
+ fixability: "manual",
101
+ });
102
+ continue;
103
+ }
104
+ const absolute = path.resolve(context.root, resolvedRelative);
105
+ if (!isPathInsideRoot(context.root, absolute)) {
106
+ continue;
107
+ }
108
+ const exists = await pathExistsInsideRoot(context.root, resolvedRelative);
109
+ if (exists) {
110
+ continue;
111
+ }
112
+ findings.push({
113
+ ruleId: "instructions/missing-path-reference",
114
+ category: "instructions",
115
+ severity: "warning",
116
+ title: "Instruction references a missing path",
117
+ message: `${file.relativePath} references \`${candidate}\`, but that path does not exist`,
118
+ whyItMatters: "Agents may follow documented paths that no longer exist, causing failed reads and wasted context.",
119
+ recommendation: "Fix the path or remove the stale reference.",
120
+ affectedAgents: context.agents
121
+ .filter((a) => a.configPaths.includes(file.relativePath))
122
+ .map((a) => a.id),
123
+ evidence: { path: file.relativePath, detail: `missing=${resolvedRelative}` },
124
+ fixability: "manual",
125
+ });
126
+ }
127
+ }
128
+ }
129
+ return findings;
130
+ },
131
+ };
@@ -0,0 +1,3 @@
1
+ import type { RuleDefinition } from "../types.js";
2
+ export declare const malformedMcpRule: RuleDefinition;
3
+ export declare const duplicateMcpServerRule: RuleDefinition;
@@ -0,0 +1,83 @@
1
+ export const malformedMcpRule = {
2
+ id: "mcp/malformed-config",
3
+ title: "Malformed MCP configuration",
4
+ description: "Flags MCP config files that exist but cannot be safely parsed.",
5
+ category: "mcp",
6
+ severity: "warning",
7
+ fixability: "manual",
8
+ rationale: "Broken MCP configuration prevents tools from loading and may hide security settings.",
9
+ recommendation: "Fix JSON/TOML syntax and validate against the agent’s MCP schema.",
10
+ async check(context) {
11
+ const findings = [];
12
+ const seen = new Set();
13
+ for (const server of context.mcpServers) {
14
+ if (!server.parseError) {
15
+ continue;
16
+ }
17
+ if (seen.has(server.sourcePath)) {
18
+ continue;
19
+ }
20
+ seen.add(server.sourcePath);
21
+ findings.push({
22
+ ruleId: "mcp/malformed-config",
23
+ category: "mcp",
24
+ severity: "warning",
25
+ title: "Malformed MCP configuration",
26
+ message: `${server.sourcePath} could not be parsed: ${server.parseError}`,
27
+ whyItMatters: "Agents will not load malformed MCP configuration, leaving expected tools unavailable.",
28
+ recommendation: "Correct the configuration syntax. Do not commit secrets into MCP env values.",
29
+ affectedAgents: [server.sourceAgent],
30
+ evidence: { path: server.sourcePath, detail: server.parseError },
31
+ fixability: "manual",
32
+ });
33
+ }
34
+ return findings;
35
+ },
36
+ };
37
+ export const duplicateMcpServerRule = {
38
+ id: "mcp/duplicate-server",
39
+ title: "Duplicate MCP server definition",
40
+ description: "Detects the same MCP server name defined more than once across project configs.",
41
+ category: "mcp",
42
+ severity: "info",
43
+ fixability: "review",
44
+ rationale: "Duplicate server names can make it unclear which configuration is active.",
45
+ recommendation: "Keep a single definition per server name per agent scope.",
46
+ async check(context) {
47
+ const findings = [];
48
+ const byKey = new Map();
49
+ for (const server of context.mcpServers) {
50
+ if (server.parseError || server.name.startsWith("(")) {
51
+ continue;
52
+ }
53
+ const key = `${server.sourceAgent}::${server.name}`;
54
+ const list = byKey.get(key) ?? [];
55
+ list.push(server);
56
+ byKey.set(key, list);
57
+ }
58
+ for (const [key, list] of byKey) {
59
+ if (list.length < 2) {
60
+ continue;
61
+ }
62
+ const first = list[0];
63
+ if (!first)
64
+ continue;
65
+ findings.push({
66
+ ruleId: "mcp/duplicate-server",
67
+ category: "mcp",
68
+ severity: "info",
69
+ title: "Duplicate MCP server definition",
70
+ message: `MCP server "${first.name}" is defined ${list.length} times for ${first.sourceAgent}`,
71
+ whyItMatters: "Duplicate names make it harder to reason about which MCP server configuration an agent will use.",
72
+ recommendation: "Deduplicate server entries so each name appears once per agent config scope.",
73
+ affectedAgents: [first.sourceAgent],
74
+ evidence: {
75
+ path: first.sourcePath,
76
+ detail: `key=${key}; paths=${list.map((s) => s.sourcePath).join(",")}`,
77
+ },
78
+ fixability: "review",
79
+ });
80
+ }
81
+ return findings;
82
+ },
83
+ };
@@ -0,0 +1,6 @@
1
+ import type { RuleDefinition } from "./types.js";
2
+ /**
3
+ * Central rule registry. Add new rules by implementing RuleDefinition and appending here.
4
+ */
5
+ export declare const ruleRegistry: readonly RuleDefinition[];
6
+ export declare function getRuleById(id: string): RuleDefinition | undefined;
@@ -0,0 +1,31 @@
1
+ import { claudeBypassPermissionsRule } from "./security/claude-bypass-permissions.js";
2
+ import { envFileExposureRule } from "./security/env-file-exposure.js";
3
+ import { mcpBroadFilesystemRule } from "./security/mcp-broad-filesystem.js";
4
+ import { privateKeyFileRule } from "./security/private-key-file.js";
5
+ import { generatedDirectoryRule } from "./context/generated-directory.js";
6
+ import { largeInstructionFileRule } from "./context/large-instruction-file.js";
7
+ import { largeLogFileRule } from "./context/large-log-file.js";
8
+ import { duplicateContentRule } from "./instructions/duplicate-content.js";
9
+ import { emptyInstructionsRule } from "./instructions/empty-instructions.js";
10
+ import { missingPathReferenceRule } from "./instructions/missing-path-reference.js";
11
+ import { duplicateMcpServerRule, malformedMcpRule } from "./mcp/mcp-rules.js";
12
+ /**
13
+ * Central rule registry. Add new rules by implementing RuleDefinition and appending here.
14
+ */
15
+ export const ruleRegistry = [
16
+ envFileExposureRule,
17
+ privateKeyFileRule,
18
+ claudeBypassPermissionsRule,
19
+ mcpBroadFilesystemRule,
20
+ largeInstructionFileRule,
21
+ largeLogFileRule,
22
+ generatedDirectoryRule,
23
+ emptyInstructionsRule,
24
+ duplicateContentRule,
25
+ missingPathReferenceRule,
26
+ malformedMcpRule,
27
+ duplicateMcpServerRule,
28
+ ];
29
+ export function getRuleById(id) {
30
+ return ruleRegistry.find((rule) => rule.id === id);
31
+ }
@@ -0,0 +1,16 @@
1
+ import type { Finding } from "../../types/index.js";
2
+ import { summarizeFindings } from "./dedupe.js";
3
+ import type { RuleContext, RuleDefinition } from "./types.js";
4
+ export interface RunRulesOptions {
5
+ context: RuleContext;
6
+ rules?: readonly RuleDefinition[];
7
+ includeRules?: string[];
8
+ excludeRules?: string[];
9
+ }
10
+ export interface RunRulesResult {
11
+ findings: Finding[];
12
+ summary: ReturnType<typeof summarizeFindings>;
13
+ elapsedMs: number;
14
+ ruleErrors: string[];
15
+ }
16
+ export declare function runRules(options: RunRulesOptions): Promise<RunRulesResult>;
@@ -0,0 +1,36 @@
1
+ import { dedupeFindings, summarizeFindings } from "./dedupe.js";
2
+ import { ruleRegistry } from "./registry.js";
3
+ export async function runRules(options) {
4
+ const started = performance.now();
5
+ const rules = (options.rules ?? ruleRegistry).filter((rule) => {
6
+ if (options.includeRules && options.includeRules.length > 0) {
7
+ return options.includeRules.includes(rule.id);
8
+ }
9
+ if (options.excludeRules && options.excludeRules.length > 0) {
10
+ return !options.excludeRules.includes(rule.id);
11
+ }
12
+ return true;
13
+ });
14
+ const drafts = [];
15
+ const ruleErrors = [];
16
+ for (const rule of rules) {
17
+ try {
18
+ const results = await rule.check(options.context);
19
+ for (const draft of results) {
20
+ // Ensure ruleId matches registry entry
21
+ drafts.push({ ...draft, ruleId: rule.id });
22
+ }
23
+ }
24
+ catch (error) {
25
+ const message = error instanceof Error ? error.message : String(error);
26
+ ruleErrors.push(`Rule ${rule.id} failed: ${message}`);
27
+ }
28
+ }
29
+ const findings = dedupeFindings(drafts);
30
+ return {
31
+ findings,
32
+ summary: summarizeFindings(findings),
33
+ elapsedMs: Math.round(performance.now() - started),
34
+ ruleErrors,
35
+ };
36
+ }
@@ -0,0 +1,7 @@
1
+ import type { RuleDefinition } from "../types.js";
2
+ /**
3
+ * Detects Claude Code `defaultMode: bypassPermissions` in project settings.
4
+ * Official docs warn this skips permission prompts and should only be used in
5
+ * isolated environments: https://code.claude.com/docs/en/permissions
6
+ */
7
+ export declare const claudeBypassPermissionsRule: RuleDefinition;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Detects Claude Code `defaultMode: bypassPermissions` in project settings.
3
+ * Official docs warn this skips permission prompts and should only be used in
4
+ * isolated environments: https://code.claude.com/docs/en/permissions
5
+ */
6
+ export const claudeBypassPermissionsRule = {
7
+ id: "security/claude-bypass-permissions",
8
+ title: "Claude Code bypassPermissions mode enabled",
9
+ description: "Flags project Claude Code settings that set defaultMode to bypassPermissions.",
10
+ category: "security",
11
+ severity: "warning",
12
+ affectedAgents: ["claude-code"],
13
+ fixability: "review",
14
+ rationale: "bypassPermissions skips interactive approval for many tool calls, increasing blast radius if the agent is prompted maliciously.",
15
+ recommendation: "Use default or acceptEdits mode for normal development. Reserve bypassPermissions for isolated containers/VMs only.",
16
+ async check(context) {
17
+ const claude = context.agents.find((a) => a.id === "claude-code");
18
+ if (!claude) {
19
+ return [];
20
+ }
21
+ const findings = [];
22
+ for (const file of claude.configFiles) {
23
+ if (file.kind !== "claude-settings" && file.kind !== "claude-settings-local") {
24
+ continue;
25
+ }
26
+ if (file.parseError || !file.readable || file.empty) {
27
+ continue;
28
+ }
29
+ const cached = await context.textCache.read(file.relativePath);
30
+ if (!cached.text) {
31
+ continue;
32
+ }
33
+ // Match JSON string values only — do not execute settings.
34
+ if (/"defaultMode"\s*:\s*"bypassPermissions"/.test(cached.text) ||
35
+ /"defaultMode"\s*:\s*"bypasspermissions"/i.test(cached.text)) {
36
+ findings.push({
37
+ ruleId: "security/claude-bypass-permissions",
38
+ category: "security",
39
+ severity: "warning",
40
+ title: "Claude Code bypassPermissions mode enabled",
41
+ message: `${file.relativePath} sets defaultMode to bypassPermissions`,
42
+ whyItMatters: "Official Claude Code docs state bypassPermissions skips permission prompts (with limited circuit breakers). This widens what an agent session can do without confirmation.",
43
+ recommendation: "Change defaultMode to default (or acceptEdits if appropriate) unless this project runs only in a locked-down isolated environment.",
44
+ affectedAgents: ["claude-code"],
45
+ evidence: { path: file.relativePath },
46
+ fixability: "review",
47
+ });
48
+ }
49
+ }
50
+ return findings;
51
+ },
52
+ };
@@ -0,0 +1,2 @@
1
+ import type { RuleDefinition } from "../types.js";
2
+ export declare const envFileExposureRule: RuleDefinition;