@tomsmart-ai/mcp-audit-claude-code 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.
- package/README.md +85 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +368 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
- package/src/index.ts +420 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @tomsmart-ai/mcp-audit-claude-code
|
|
2
|
+
|
|
3
|
+
> MCP server for auditing Claude Code configurations. Originally built for [AI Orchestration Audit-as-a-Service](https://smartflowproai.com/work-with-me) deliverables. Open-source under MIT.
|
|
4
|
+
|
|
5
|
+
Drop this server into your own Claude Code setup and it gains three tools for self-auditing your `~/.claude/settings.json` + skills + hooks directories.
|
|
6
|
+
|
|
7
|
+
## What it audits
|
|
8
|
+
|
|
9
|
+
### audit_config_file
|
|
10
|
+
|
|
11
|
+
Parses your Claude Code `settings.json` and flags:
|
|
12
|
+
- **HIGH**: `bypassPermissions` defaultMode (security risk), hardcoded API keys leaked in config
|
|
13
|
+
- **MED**: missing permissions block, empty allow lists
|
|
14
|
+
- **LOW**: missing hooks block, no env vars set, excessive plugins enabled
|
|
15
|
+
|
|
16
|
+
### audit_skills_dir
|
|
17
|
+
|
|
18
|
+
Scans `~/.claude/skills/` and flags per-skill:
|
|
19
|
+
- **MED**: missing SKILL.md, missing YAML frontmatter, missing description field
|
|
20
|
+
- **LOW**: description shorter than 20 chars, body shorter than 100 chars
|
|
21
|
+
|
|
22
|
+
### audit_hooks_dir
|
|
23
|
+
|
|
24
|
+
Scans `~/.claude/hooks/` shell scripts and flags:
|
|
25
|
+
- **HIGH**: hook not executable (`chmod +x` missing), leaked API keys in hook content
|
|
26
|
+
- **MED**: missing `set -e` error handling in bash hooks
|
|
27
|
+
- **LOW**: hardcoded `/Users/<name>/` paths (portability hit)
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g @tomsmart-ai/mcp-audit-claude-code
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Use with Claude Code
|
|
36
|
+
|
|
37
|
+
Add to `~/.claude.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"audit-claude-code": {
|
|
43
|
+
"command": "mcp-audit-claude-code"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Restart Claude Code. Then ask:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Audit my Claude Code config at ~/.claude/settings.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Claude calls `audit_config_file`, returns findings list with severity + fix hints.
|
|
56
|
+
|
|
57
|
+
## Use case: AAA self-audit
|
|
58
|
+
|
|
59
|
+
This server powers the AI Orchestration Audit-as-a-Service stack audits (Tier 1 $500 — Tier 3 $1,500). When you commission an audit, this MCP server runs against your stack first, then a human review applies cultural context and shipping pragmatism on top.
|
|
60
|
+
|
|
61
|
+
If you want to skip the audit-as-a-service and run the same checks yourself, install this server and ask Claude to audit. The tools are the same; what you don't get is the cultural review layer + the implementation effort (audit-as-a-service includes one fix on Tier 2+).
|
|
62
|
+
|
|
63
|
+
## Why open-source
|
|
64
|
+
|
|
65
|
+
The audit logic itself is straightforward — read JSON, check patterns, return findings. Selling closed access to the tool would protect zero moat. The moat is the cultural-context review + the experience auditing many stacks + knowing which findings actually matter for which workflows.
|
|
66
|
+
|
|
67
|
+
Open-source the tool, sell the judgment.
|
|
68
|
+
|
|
69
|
+
## Roadmap
|
|
70
|
+
|
|
71
|
+
- v0.1.0 (today, Wt 26.05.2026) — initial audit tools for config, skills, hooks
|
|
72
|
+
- v0.2.0 — add `audit_memory_dir` for `~/.claude/projects/*/memory/` quality scan
|
|
73
|
+
- v0.3.0 — add `audit_mcp_server_config` for inspecting MCP server registrations
|
|
74
|
+
- v0.4.0 — add `recommend_hooks` returning suggested hook scaffolds for missing categories
|
|
75
|
+
- v0.5.0 — cross-check `audit_config_file` against `audit_hooks_dir` (consistency: hooks referenced in config exist on disk)
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
80
|
+
|
|
81
|
+
## Author
|
|
82
|
+
|
|
83
|
+
Tom Smart — [smartflowproai.com](https://smartflowproai.com) · [@TomSmart_ai](https://x.com/TomSmart_ai) · [github.com/smartflowproai-lang](https://github.com/smartflowproai-lang)
|
|
84
|
+
|
|
85
|
+
Pair-built with Claude (Anthropic's coding agent). Tom scoped the tools, drafted findings categories, decided severity thresholds. Claude wrote the TypeScript. Tom reviewed each change and ran smoke tests before publish.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mcp-audit-claude-code v0.1.0
|
|
4
|
+
*
|
|
5
|
+
* MCP server exposing audit tools for Claude Code configurations.
|
|
6
|
+
* Primary use: AI Orchestration Audit-as-a-Service (AAA) deliverables.
|
|
7
|
+
* Secondary use: solo self-audit by anyone running Claude Code.
|
|
8
|
+
*
|
|
9
|
+
* Tools:
|
|
10
|
+
* - audit_config_file(file_path) — parses ~/.claude/settings.json, flags anti-patterns
|
|
11
|
+
* - audit_skills_dir(dir_path) — scans skill SKILL.md files for quality issues
|
|
12
|
+
* - audit_hooks_dir(dir_path) — checks hook scripts for safety + error handling
|
|
13
|
+
*
|
|
14
|
+
* Built Wt 26.05.2026 ~18:20 PL ja-side autonomous as Tom Smart's AAA primary new monetization project.
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mcp-audit-claude-code v0.1.0
|
|
4
|
+
*
|
|
5
|
+
* MCP server exposing audit tools for Claude Code configurations.
|
|
6
|
+
* Primary use: AI Orchestration Audit-as-a-Service (AAA) deliverables.
|
|
7
|
+
* Secondary use: solo self-audit by anyone running Claude Code.
|
|
8
|
+
*
|
|
9
|
+
* Tools:
|
|
10
|
+
* - audit_config_file(file_path) — parses ~/.claude/settings.json, flags anti-patterns
|
|
11
|
+
* - audit_skills_dir(dir_path) — scans skill SKILL.md files for quality issues
|
|
12
|
+
* - audit_hooks_dir(dir_path) — checks hook scripts for safety + error handling
|
|
13
|
+
*
|
|
14
|
+
* Built Wt 26.05.2026 ~18:20 PL ja-side autonomous as Tom Smart's AAA primary new monetization project.
|
|
15
|
+
*/
|
|
16
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
17
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
19
|
+
import * as fs from "node:fs";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
const server = new Server({
|
|
22
|
+
name: "@tomsmart-ai/mcp-audit-claude-code",
|
|
23
|
+
version: "0.1.0",
|
|
24
|
+
}, {
|
|
25
|
+
capabilities: {
|
|
26
|
+
tools: {},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
function auditConfigFile(filePath) {
|
|
30
|
+
const findings = [];
|
|
31
|
+
if (!fs.existsSync(filePath)) {
|
|
32
|
+
return {
|
|
33
|
+
findings: [{ severity: "HIGH", category: "missing", message: `Config file not found at ${filePath}`, fix_hint: "Verify path; if Claude Code is installed, default location is ~/.claude/settings.json" }],
|
|
34
|
+
summary: "FAIL — config file not found",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
let config;
|
|
38
|
+
try {
|
|
39
|
+
config = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
return {
|
|
43
|
+
findings: [{ severity: "HIGH", category: "syntax", message: `JSON parse error: ${e.message}`, fix_hint: "Validate JSON syntax via `jq . settings.json`" }],
|
|
44
|
+
summary: "FAIL — syntax error",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Check 1: permissions section structure
|
|
48
|
+
if (!config.permissions) {
|
|
49
|
+
findings.push({
|
|
50
|
+
severity: "MED",
|
|
51
|
+
category: "permissions",
|
|
52
|
+
message: "No permissions block. Claude Code will prompt on every tool call.",
|
|
53
|
+
fix_hint: "Add permissions section z allowlist of safe paths (Write/Edit) and tools (Bash command patterns).",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else if (config.permissions.defaultMode === "bypassPermissions") {
|
|
57
|
+
findings.push({
|
|
58
|
+
severity: "HIGH",
|
|
59
|
+
category: "security",
|
|
60
|
+
message: "defaultMode set to 'bypassPermissions' — Claude can execute arbitrary tools without confirmation.",
|
|
61
|
+
fix_hint: "Use 'acceptEdits' for most use cases. Reserve 'bypassPermissions' only for trusted automation contexts (CI, VPS overnight chains).",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Check 2: hooks
|
|
65
|
+
if (!config.hooks || Object.keys(config.hooks).length === 0) {
|
|
66
|
+
findings.push({
|
|
67
|
+
severity: "LOW",
|
|
68
|
+
category: "hooks",
|
|
69
|
+
message: "No hooks configured. Missing opportunity for pre-prompt context injection / pre-tool-use safety / stop-hook gates.",
|
|
70
|
+
fix_hint: "Common hooks worth setting: UserPromptSubmit (memory grep / context injection), PreToolUse (audit / safety), PostToolUse (logging).",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Check 3: env vars
|
|
74
|
+
if (!config.env) {
|
|
75
|
+
findings.push({
|
|
76
|
+
severity: "LOW",
|
|
77
|
+
category: "env",
|
|
78
|
+
message: "No env vars configured.",
|
|
79
|
+
fix_hint: "Consider setting CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING or other harness-level controls.",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Check 4: hardcoded API keys
|
|
83
|
+
const configString = JSON.stringify(config);
|
|
84
|
+
const apiKeyPatterns = [
|
|
85
|
+
/sk_live_[a-zA-Z0-9_]{20,}/g,
|
|
86
|
+
/sk-ant-[a-zA-Z0-9_-]{20,}/g,
|
|
87
|
+
/sk-[a-zA-Z0-9_-]{20,}/g,
|
|
88
|
+
/AIza[a-zA-Z0-9_-]{30,}/g,
|
|
89
|
+
/ghp_[a-zA-Z0-9]{30,}/g,
|
|
90
|
+
];
|
|
91
|
+
for (const pat of apiKeyPatterns) {
|
|
92
|
+
const m = configString.match(pat);
|
|
93
|
+
if (m) {
|
|
94
|
+
findings.push({
|
|
95
|
+
severity: "HIGH",
|
|
96
|
+
category: "secret_leak",
|
|
97
|
+
message: `Possible API key / token found in config: ${m[0].substring(0, 12)}…`,
|
|
98
|
+
fix_hint: "Move secrets to env vars OR external secret store (1Password CLI, age-encrypted file). Never commit raw secrets to settings.json.",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Check 5: empty allow list
|
|
103
|
+
if (config.permissions?.allow && config.permissions.allow.length === 0) {
|
|
104
|
+
findings.push({
|
|
105
|
+
severity: "MED",
|
|
106
|
+
category: "permissions",
|
|
107
|
+
message: "permissions.allow is empty — every tool call will prompt.",
|
|
108
|
+
fix_hint: "Populate allow list z safe paths (e.g. Write(/path/to/safe-dir/**)) and Bash command patterns (e.g. Bash(ls *)).",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Check 6: plugins enabled
|
|
112
|
+
if (config.enabledPlugins) {
|
|
113
|
+
const pluginCount = Object.keys(config.enabledPlugins).filter(k => config.enabledPlugins[k]).length;
|
|
114
|
+
if (pluginCount > 5) {
|
|
115
|
+
findings.push({
|
|
116
|
+
severity: "LOW",
|
|
117
|
+
category: "plugins",
|
|
118
|
+
message: `${pluginCount} plugins enabled. Each adds token overhead at startup.`,
|
|
119
|
+
fix_hint: "Disable unused plugins. Set value to false in enabledPlugins to keep config but skip load.",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const highCount = findings.filter(f => f.severity === "HIGH").length;
|
|
124
|
+
const medCount = findings.filter(f => f.severity === "MED").length;
|
|
125
|
+
const lowCount = findings.filter(f => f.severity === "LOW").length;
|
|
126
|
+
let summary;
|
|
127
|
+
if (highCount > 0)
|
|
128
|
+
summary = `BLOCK — ${highCount} HIGH, ${medCount} MED, ${lowCount} LOW`;
|
|
129
|
+
else if (medCount > 0)
|
|
130
|
+
summary = `WARN — ${medCount} MED, ${lowCount} LOW`;
|
|
131
|
+
else if (lowCount > 0)
|
|
132
|
+
summary = `POLISH — ${lowCount} LOW`;
|
|
133
|
+
else
|
|
134
|
+
summary = `PASS — no anti-patterns detected`;
|
|
135
|
+
return { findings, summary };
|
|
136
|
+
}
|
|
137
|
+
// =====================================================================
|
|
138
|
+
// Tool: audit_skills_dir
|
|
139
|
+
// =====================================================================
|
|
140
|
+
function auditSkillsDir(dirPath) {
|
|
141
|
+
const findings = [];
|
|
142
|
+
if (!fs.existsSync(dirPath)) {
|
|
143
|
+
return {
|
|
144
|
+
findings: [{ severity: "HIGH", category: "missing", message: `Skills directory not found at ${dirPath}`, fix_hint: "Default Claude Code skills location is ~/.claude/skills/" }],
|
|
145
|
+
summary: "FAIL — skills dir not found",
|
|
146
|
+
skill_count: 0,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const skills = fs.readdirSync(dirPath).filter(name => {
|
|
150
|
+
const fullPath = path.join(dirPath, name);
|
|
151
|
+
return fs.statSync(fullPath).isDirectory();
|
|
152
|
+
});
|
|
153
|
+
if (skills.length === 0) {
|
|
154
|
+
findings.push({
|
|
155
|
+
severity: "LOW",
|
|
156
|
+
category: "empty",
|
|
157
|
+
message: "No skills installed.",
|
|
158
|
+
fix_hint: "Skills provide reusable workflows. Consider building skills for common multi-step tasks.",
|
|
159
|
+
});
|
|
160
|
+
return { findings, summary: "EMPTY — no skills", skill_count: 0 };
|
|
161
|
+
}
|
|
162
|
+
for (const skillName of skills) {
|
|
163
|
+
const skillDir = path.join(dirPath, skillName);
|
|
164
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
165
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
166
|
+
findings.push({
|
|
167
|
+
severity: "MED",
|
|
168
|
+
category: "skill_structure",
|
|
169
|
+
message: `Skill '${skillName}' missing SKILL.md`,
|
|
170
|
+
fix_hint: "Every skill needs SKILL.md with frontmatter (name + description) and body instructions.",
|
|
171
|
+
});
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const skillMd = fs.readFileSync(skillMdPath, "utf-8");
|
|
175
|
+
// Check frontmatter present
|
|
176
|
+
if (!skillMd.startsWith("---")) {
|
|
177
|
+
findings.push({
|
|
178
|
+
severity: "MED",
|
|
179
|
+
category: "skill_frontmatter",
|
|
180
|
+
message: `Skill '${skillName}' SKILL.md missing YAML frontmatter`,
|
|
181
|
+
fix_hint: "Add ---\\nname: skill-name\\ndescription: When to use\\n--- at top of SKILL.md",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// Check description quality
|
|
185
|
+
const descMatch = skillMd.match(/description:\s*(.+)/);
|
|
186
|
+
if (!descMatch) {
|
|
187
|
+
findings.push({
|
|
188
|
+
severity: "MED",
|
|
189
|
+
category: "skill_description",
|
|
190
|
+
message: `Skill '${skillName}' missing description field`,
|
|
191
|
+
fix_hint: "description field is what Claude uses to decide if skill matches user request. Be specific about when to invoke.",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
else if (descMatch[1].trim().length < 20) {
|
|
195
|
+
findings.push({
|
|
196
|
+
severity: "LOW",
|
|
197
|
+
category: "skill_description",
|
|
198
|
+
message: `Skill '${skillName}' description is short (${descMatch[1].trim().length} chars)`,
|
|
199
|
+
fix_hint: "Aim for 50-150 chars in description. Include trigger keywords + use cases.",
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// Check body length
|
|
203
|
+
const bodyAfterFrontmatter = skillMd.replace(/^---[\s\S]*?---\n/, "").trim();
|
|
204
|
+
if (bodyAfterFrontmatter.length < 100) {
|
|
205
|
+
findings.push({
|
|
206
|
+
severity: "MED",
|
|
207
|
+
category: "skill_body",
|
|
208
|
+
message: `Skill '${skillName}' body very short (${bodyAfterFrontmatter.length} chars)`,
|
|
209
|
+
fix_hint: "Skill body should provide step-by-step instructions Claude follows. Short skills often miss edge cases.",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const summary = findings.length === 0
|
|
214
|
+
? `PASS — ${skills.length} skills, no anti-patterns`
|
|
215
|
+
: `${findings.filter(f => f.severity === "HIGH").length} HIGH / ${findings.filter(f => f.severity === "MED").length} MED / ${findings.filter(f => f.severity === "LOW").length} LOW across ${skills.length} skills`;
|
|
216
|
+
return { findings, summary, skill_count: skills.length };
|
|
217
|
+
}
|
|
218
|
+
// =====================================================================
|
|
219
|
+
// Tool: audit_hooks_dir
|
|
220
|
+
// =====================================================================
|
|
221
|
+
function auditHooksDir(dirPath) {
|
|
222
|
+
const findings = [];
|
|
223
|
+
if (!fs.existsSync(dirPath)) {
|
|
224
|
+
return {
|
|
225
|
+
findings: [{ severity: "LOW", category: "missing", message: `Hooks directory not found at ${dirPath}`, fix_hint: "Default Claude Code hooks location is ~/.claude/hooks/" }],
|
|
226
|
+
summary: "EMPTY — no hooks dir",
|
|
227
|
+
hook_count: 0,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const hookFiles = fs.readdirSync(dirPath).filter(name => name.endsWith(".sh") || name.endsWith(".py") || name.endsWith(".js"));
|
|
231
|
+
if (hookFiles.length === 0) {
|
|
232
|
+
return { findings: [{ severity: "LOW", category: "empty", message: "No hook scripts found.", fix_hint: "Consider hooks for UserPromptSubmit (context injection), PreToolUse (audit), PostToolUse (logging)." }], summary: "EMPTY — no hooks", hook_count: 0 };
|
|
233
|
+
}
|
|
234
|
+
for (const hookFile of hookFiles) {
|
|
235
|
+
const fullPath = path.join(dirPath, hookFile);
|
|
236
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
237
|
+
const stats = fs.statSync(fullPath);
|
|
238
|
+
// Check executable bit (owner OR group OR world executable)
|
|
239
|
+
if (!(stats.mode & 0o111)) {
|
|
240
|
+
findings.push({
|
|
241
|
+
severity: "HIGH",
|
|
242
|
+
category: "permissions",
|
|
243
|
+
message: `Hook '${hookFile}' is not executable. Claude Code will NOT invoke it.`,
|
|
244
|
+
fix_hint: `Run: chmod +x ${fullPath}`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
// Check error handling for bash hooks
|
|
248
|
+
if (hookFile.endsWith(".sh")) {
|
|
249
|
+
if (!content.includes("set -e") && !content.includes("set -euo")) {
|
|
250
|
+
findings.push({
|
|
251
|
+
severity: "MED",
|
|
252
|
+
category: "error_handling",
|
|
253
|
+
message: `Hook '${hookFile}' missing 'set -e' or 'set -euo pipefail'`,
|
|
254
|
+
fix_hint: "Add 'set -euo pipefail' at top of bash hook to fail-fast on errors instead of silent corruption.",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Check for hardcoded paths to current user
|
|
259
|
+
const userMatches = content.match(/\/Users\/[a-zA-Z0-9_-]+/g);
|
|
260
|
+
if (userMatches) {
|
|
261
|
+
findings.push({
|
|
262
|
+
severity: "LOW",
|
|
263
|
+
category: "portability",
|
|
264
|
+
message: `Hook '${hookFile}' has hardcoded user path (e.g. ${userMatches[0]})`,
|
|
265
|
+
fix_hint: "Use $HOME or ~/ instead of /Users/<name>/ for portability across machines.",
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
// Check for credentials in hook
|
|
269
|
+
const apiKeyPatterns = [/sk_live_[a-zA-Z0-9_]{20,}/, /sk-[a-zA-Z0-9_-]{20,}/];
|
|
270
|
+
for (const pat of apiKeyPatterns) {
|
|
271
|
+
if (content.match(pat)) {
|
|
272
|
+
findings.push({
|
|
273
|
+
severity: "HIGH",
|
|
274
|
+
category: "secret_leak",
|
|
275
|
+
message: `Hook '${hookFile}' contains hardcoded API key.`,
|
|
276
|
+
fix_hint: "Move secrets to env vars OR external secret store. Read via $VAR_NAME at hook runtime.",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const summary = findings.length === 0
|
|
282
|
+
? `PASS — ${hookFiles.length} hooks, no anti-patterns`
|
|
283
|
+
: `${findings.filter(f => f.severity === "HIGH").length} HIGH / ${findings.filter(f => f.severity === "MED").length} MED / ${findings.filter(f => f.severity === "LOW").length} LOW across ${hookFiles.length} hooks`;
|
|
284
|
+
return { findings, summary, hook_count: hookFiles.length };
|
|
285
|
+
}
|
|
286
|
+
// =====================================================================
|
|
287
|
+
// MCP server wiring
|
|
288
|
+
// =====================================================================
|
|
289
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
290
|
+
tools: [
|
|
291
|
+
{
|
|
292
|
+
name: "audit_config_file",
|
|
293
|
+
description: "Audit a Claude Code settings.json configuration file for anti-patterns: missing permissions, bypassPermissions risk, hardcoded API keys, empty allow lists, excessive plugins. Returns findings with severity HIGH/MED/LOW and fix hints.",
|
|
294
|
+
inputSchema: {
|
|
295
|
+
type: "object",
|
|
296
|
+
properties: {
|
|
297
|
+
file_path: {
|
|
298
|
+
type: "string",
|
|
299
|
+
description: "Absolute path to Claude Code settings.json (typical: ~/.claude/settings.json)",
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
required: ["file_path"],
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: "audit_skills_dir",
|
|
307
|
+
description: "Audit a Claude Code skills directory for quality issues: missing SKILL.md, missing frontmatter, short descriptions, sparse body content. Returns findings + skill count.",
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {
|
|
311
|
+
dir_path: {
|
|
312
|
+
type: "string",
|
|
313
|
+
description: "Absolute path to Claude Code skills directory (typical: ~/.claude/skills/)",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
required: ["dir_path"],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: "audit_hooks_dir",
|
|
321
|
+
description: "Audit a Claude Code hooks directory: missing executable bit, missing 'set -e' error handling, hardcoded user paths, leaked credentials in hook scripts. Returns findings + hook count.",
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: {
|
|
325
|
+
dir_path: {
|
|
326
|
+
type: "string",
|
|
327
|
+
description: "Absolute path to Claude Code hooks directory (typical: ~/.claude/hooks/)",
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
required: ["dir_path"],
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
}));
|
|
335
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
336
|
+
const { name, arguments: args } = request.params;
|
|
337
|
+
let result;
|
|
338
|
+
if (name === "audit_config_file") {
|
|
339
|
+
result = auditConfigFile(args.file_path);
|
|
340
|
+
}
|
|
341
|
+
else if (name === "audit_skills_dir") {
|
|
342
|
+
result = auditSkillsDir(args.dir_path);
|
|
343
|
+
}
|
|
344
|
+
else if (name === "audit_hooks_dir") {
|
|
345
|
+
result = auditHooksDir(args.dir_path);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
content: [
|
|
352
|
+
{
|
|
353
|
+
type: "text",
|
|
354
|
+
text: JSON.stringify(result, null, 2),
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
};
|
|
358
|
+
});
|
|
359
|
+
async function main() {
|
|
360
|
+
const transport = new StdioServerTransport();
|
|
361
|
+
await server.connect(transport);
|
|
362
|
+
console.error("mcp-audit-claude-code v0.1.0 listening on stdio");
|
|
363
|
+
}
|
|
364
|
+
main().catch((err) => {
|
|
365
|
+
console.error("Fatal error:", err);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
});
|
|
368
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB;IACE,IAAI,EAAE,oCAAoC;IAC1C,OAAO,EAAE,OAAO;CACjB,EACD;IACE,YAAY,EAAE;QACZ,KAAK,EAAE,EAAE;KACV;CACF,CACF,CAAC;AAaF,SAAS,eAAe,CAAC,QAAgB;IACvC,MAAM,QAAQ,GAAoB,EAAE,CAAC;IAErC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,OAAO;YACL,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,4BAA4B,QAAQ,EAAE,EAAE,QAAQ,EAAE,uFAAuF,EAAE,CAAC;YACzM,OAAO,EAAE,8BAA8B;SACxC,CAAC;IACJ,CAAC;IAED,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1D,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO;YACL,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAsB,CAAW,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,+CAA+C,EAAE,CAAC;YACrK,OAAO,EAAE,qBAAqB;SAC/B,CAAC;IACJ,CAAC;IAED,yCAAyC;IACzC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,aAAa;YACvB,OAAO,EAAE,mEAAmE;YAC5E,QAAQ,EAAE,mGAAmG;SAC9G,CAAC,CAAC;IACL,CAAC;SAAM,IAAI,MAAM,CAAC,WAAW,CAAC,WAAW,KAAK,mBAAmB,EAAE,CAAC;QAClE,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,MAAM;YAChB,QAAQ,EAAE,UAAU;YACpB,OAAO,EAAE,mGAAmG;YAC5G,QAAQ,EAAE,oIAAoI;SAC/I,CAAC,CAAC;IACL,CAAC;IAED,iBAAiB;IACjB,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5D,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,oHAAoH;YAC7H,QAAQ,EAAE,qIAAqI;SAChJ,CAAC,CAAC;IACL,CAAC;IAED,oBAAoB;IACpB,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QAChB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,yBAAyB;YAClC,QAAQ,EAAE,yFAAyF;SACpG,CAAC,CAAC;IACL,CAAC;IAED,8BAA8B;IAC9B,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC5C,MAAM,cAAc,GAAG;QACrB,4BAA4B;QAC5B,4BAA4B;QAC5B,wBAAwB;QACxB,yBAAyB;QACzB,uBAAuB;KACxB,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,cAAc,EAAE,CAAC;QACjC,MAAM,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,EAAE,CAAC;YACN,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,QAAQ,EAAE,aAAa;gBACvB,OAAO,EAAE,6CAA6C,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG;gBAC9E,QAAQ,EAAE,mIAAmI;aAC9I,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,4BAA4B;IAC5B,IAAI,MAAM,CAAC,WAAW,EAAE,KAAK,IAAI,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvE,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,aAAa;YACvB,OAAO,EAAE,2DAA2D;YACpE,QAAQ,EAAE,kHAAkH;SAC7H,CAAC,CAAC;IACL,CAAC;IAED,2BAA2B;IAC3B,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;QAC1B,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QACpG,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YACpB,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,KAAK;gBACf,QAAQ,EAAE,SAAS;gBACnB,OAAO,EAAE,GAAG,WAAW,wDAAwD;gBAC/E,QAAQ,EAAE,4FAA4F;aACvG,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,SAAS,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,MAAM,CAAC;IACrE,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,MAAM,CAAC;IACnE,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,MAAM,CAAC;IAEnE,IAAI,OAAe,CAAC;IACpB,IAAI,SAAS,GAAG,CAAC;QAAE,OAAO,GAAG,WAAW,SAAS,UAAU,QAAQ,SAAS,QAAQ,MAAM,CAAC;SACtF,IAAI,QAAQ,GAAG,CAAC;QAAE,OAAO,GAAG,UAAU,QAAQ,SAAS,QAAQ,MAAM,CAAC;SACtE,IAAI,QAAQ,GAAG,CAAC;QAAE,OAAO,GAAG,YAAY,QAAQ,MAAM,CAAC;;QACvD,OAAO,GAAG,kCAAkC,CAAC;IAElD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC;AAED,wEAAwE;AACxE,yBAAyB;AACzB,wEAAwE;AAExE,SAAS,cAAc,CAAC,OAAe;IACrC,MAAM,QAAQ,GAAoB,EAAE,CAAC;IAErC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,OAAO;YACL,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,iCAAiC,OAAO,EAAE,EAAE,QAAQ,EAAE,0DAA0D,EAAE,CAAC;YAChL,OAAO,EAAE,6BAA6B;YACtC,WAAW,EAAE,CAAC;SACf,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE;QACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC1C,OAAO,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC;YACZ,QAAQ,EAAE,KAAK;YACf,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,sBAAsB;YAC/B,QAAQ,EAAE,0FAA0F;SACrG,CAAC,CAAC;QACH,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IACpE,CAAC;IAED,KAAK,MAAM,SAAS,IAAI,MAAM,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QAC/C,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAEpD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAChC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,KAAK;gBACf,QAAQ,EAAE,iBAAiB;gBAC3B,OAAO,EAAE,UAAU,SAAS,oBAAoB;gBAChD,QAAQ,EAAE,yFAAyF;aACpG,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QAEtD,4BAA4B;QAC5B,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/B,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,KAAK;gBACf,QAAQ,EAAE,mBAAmB;gBAC7B,OAAO,EAAE,UAAU,SAAS,qCAAqC;gBACjE,QAAQ,EAAE,gFAAgF;aAC3F,CAAC,CAAC;QACL,CAAC;QAED,4BAA4B;QAC5B,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;QACvD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,KAAK;gBACf,QAAQ,EAAE,mBAAmB;gBAC7B,OAAO,EAAE,UAAU,SAAS,6BAA6B;gBACzD,QAAQ,EAAE,kHAAkH;aAC7H,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;YAC3C,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,KAAK;gBACf,QAAQ,EAAE,mBAAmB;gBAC7B,OAAO,EAAE,UAAU,SAAS,2BAA2B,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,SAAS;gBAC1F,QAAQ,EAAE,4EAA4E;aACvF,CAAC,CAAC;QACL,CAAC;QAED,oBAAoB;QACpB,MAAM,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7E,IAAI,oBAAoB,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YACtC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,KAAK;gBACf,QAAQ,EAAE,YAAY;gBACtB,OAAO,EAAE,UAAU,SAAS,sBAAsB,oBAAoB,CAAC,MAAM,SAAS;gBACtF,QAAQ,EAAE,yGAAyG;aACpH,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC;QACnC,CAAC,CAAC,UAAU,MAAM,CAAC,MAAM,2BAA2B;QACpD,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,MAAM,WAAW,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,MAAM,UAAU,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,MAAM,eAAe,MAAM,CAAC,MAAM,SAAS,CAAC;IAEtN,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;AAC3D,CAAC;AAED,wEAAwE;AACxE,wBAAwB;AACxB,wEAAwE;AAExE,SAAS,aAAa,CAAC,OAAe;IACpC,MAAM,QAAQ,GAAoB,EAAE,CAAC;IAErC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,OAAO;YACL,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,gCAAgC,OAAO,EAAE,EAAE,QAAQ,EAAE,wDAAwD,EAAE,CAAC;YAC5K,OAAO,EAAE,sBAAsB;YAC/B,UAAU,EAAE,CAAC;SACd,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,EAAE,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/H,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,wBAAwB,EAAE,QAAQ,EAAE,qGAAqG,EAAE,CAAC,EAAE,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IAChQ,CAAC;IAED,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC9C,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACnD,MAAM,KAAK,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAEpC,4DAA4D;QAC5D,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC;YAC1B,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,QAAQ,EAAE,aAAa;gBACvB,OAAO,EAAE,SAAS,QAAQ,sDAAsD;gBAChF,QAAQ,EAAE,iBAAiB,QAAQ,EAAE;aACtC,CAAC,CAAC;QACL,CAAC;QAED,sCAAsC;QACtC,IAAI,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBACjE,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,KAAK;oBACf,QAAQ,EAAE,gBAAgB;oBAC1B,OAAO,EAAE,SAAS,QAAQ,2CAA2C;oBACrE,QAAQ,EAAE,kGAAkG;iBAC7G,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,4CAA4C;QAC5C,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;QAC9D,IAAI,WAAW,EAAE,CAAC;YAChB,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,KAAK;gBACf,QAAQ,EAAE,aAAa;gBACvB,OAAO,EAAE,SAAS,QAAQ,mCAAmC,WAAW,CAAC,CAAC,CAAC,GAAG;gBAC9E,QAAQ,EAAE,4EAA4E;aACvF,CAAC,CAAC;QACL,CAAC;QAED,gCAAgC;QAChC,MAAM,cAAc,GAAG,CAAC,2BAA2B,EAAE,uBAAuB,CAAC,CAAC;QAC9E,KAAK,MAAM,GAAG,IAAI,cAAc,EAAE,CAAC;YACjC,IAAI,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;gBACvB,QAAQ,CAAC,IAAI,CAAC;oBACZ,QAAQ,EAAE,MAAM;oBAChB,QAAQ,EAAE,aAAa;oBACvB,OAAO,EAAE,SAAS,QAAQ,+BAA+B;oBACzD,QAAQ,EAAE,wFAAwF;iBACnG,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC;QACnC,CAAC,CAAC,UAAU,SAAS,CAAC,MAAM,0BAA0B;QACtD,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,MAAM,WAAW,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,MAAM,UAAU,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,MAAM,eAAe,SAAS,CAAC,MAAM,QAAQ,CAAC;IAExN,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC;AAC7D,CAAC;AAED,wEAAwE;AACxE,oBAAoB;AACpB,wEAAwE;AAExE,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;IAC5D,KAAK,EAAE;QACL;YACE,IAAI,EAAE,mBAAmB;YACzB,WAAW,EAAE,2OAA2O;YACxP,WAAW,EAAE;gBACX,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,SAAS,EAAE;wBACT,IAAI,EAAE,QAAQ;wBACd,WAAW,EAAE,+EAA+E;qBAC7F;iBACF;gBACD,QAAQ,EAAE,CAAC,WAAW,CAAC;aACxB;SACF;QACD;YACE,IAAI,EAAE,kBAAkB;YACxB,WAAW,EAAE,0KAA0K;YACvL,WAAW,EAAE;gBACX,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,QAAQ,EAAE;wBACR,IAAI,EAAE,QAAQ;wBACd,WAAW,EAAE,4EAA4E;qBAC1F;iBACF;gBACD,QAAQ,EAAE,CAAC,UAAU,CAAC;aACvB;SACF;QACD;YACE,IAAI,EAAE,iBAAiB;YACvB,WAAW,EAAE,wLAAwL;YACrM,WAAW,EAAE;gBACX,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,QAAQ,EAAE;wBACR,IAAI,EAAE,QAAQ;wBACd,WAAW,EAAE,0EAA0E;qBACxF;iBACF;gBACD,QAAQ,EAAE,CAAC,UAAU,CAAC;aACvB;SACF;KACF;CACF,CAAC,CAAC,CAAC;AAEJ,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;IAChE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAEjD,IAAI,MAAW,CAAC;IAChB,IAAI,IAAI,KAAK,mBAAmB,EAAE,CAAC;QACjC,MAAM,GAAG,eAAe,CAAC,IAAK,CAAC,SAAmB,CAAC,CAAC;IACtD,CAAC;SAAM,IAAI,IAAI,KAAK,kBAAkB,EAAE,CAAC;QACvC,MAAM,GAAG,cAAc,CAAC,IAAK,CAAC,QAAkB,CAAC,CAAC;IACpD,CAAC;SAAM,IAAI,IAAI,KAAK,iBAAiB,EAAE,CAAC;QACtC,MAAM,GAAG,aAAa,CAAC,IAAK,CAAC,QAAkB,CAAC,CAAC;IACnD,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,KAAK,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;aACtC;SACF;KACF,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAC;AACnE,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;IACnC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tomsmart-ai/mcp-audit-claude-code",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for auditing Claude Code configurations (~/.claude.json + skills + hooks). Used internally for AI Orchestration Audit-as-a-Service deliverables, also usable standalone for self-audit.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-audit-claude-code": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts",
|
|
14
|
+
"test": "node test/smoke.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"audit",
|
|
20
|
+
"ai-orchestration",
|
|
21
|
+
"smartflow"
|
|
22
|
+
],
|
|
23
|
+
"author": "Tom Smart <info@smartflowproai.com>",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/smartflowproai-lang/mcp-audit-claude-code"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://smartflowproai.com/work-with-me",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.3.0",
|
|
35
|
+
"tsx": "^4.0.0",
|
|
36
|
+
"@types/node": "^20.0.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mcp-audit-claude-code v0.1.0
|
|
4
|
+
*
|
|
5
|
+
* MCP server exposing audit tools for Claude Code configurations.
|
|
6
|
+
* Primary use: AI Orchestration Audit-as-a-Service (AAA) deliverables.
|
|
7
|
+
* Secondary use: solo self-audit by anyone running Claude Code.
|
|
8
|
+
*
|
|
9
|
+
* Tools:
|
|
10
|
+
* - audit_config_file(file_path) — parses ~/.claude/settings.json, flags anti-patterns
|
|
11
|
+
* - audit_skills_dir(dir_path) — scans skill SKILL.md files for quality issues
|
|
12
|
+
* - audit_hooks_dir(dir_path) — checks hook scripts for safety + error handling
|
|
13
|
+
*
|
|
14
|
+
* Built Wt 26.05.2026 ~18:20 PL ja-side autonomous as Tom Smart's AAA primary new monetization project.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import {
|
|
20
|
+
CallToolRequestSchema,
|
|
21
|
+
ListToolsRequestSchema,
|
|
22
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
|
|
26
|
+
const server = new Server(
|
|
27
|
+
{
|
|
28
|
+
name: "@tomsmart-ai/mcp-audit-claude-code",
|
|
29
|
+
version: "0.1.0",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
capabilities: {
|
|
33
|
+
tools: {},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// =====================================================================
|
|
39
|
+
// Tool: audit_config_file
|
|
40
|
+
// =====================================================================
|
|
41
|
+
|
|
42
|
+
interface ConfigFinding {
|
|
43
|
+
severity: "HIGH" | "MED" | "LOW";
|
|
44
|
+
category: string;
|
|
45
|
+
message: string;
|
|
46
|
+
fix_hint: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function auditConfigFile(filePath: string): { findings: ConfigFinding[]; summary: string } {
|
|
50
|
+
const findings: ConfigFinding[] = [];
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(filePath)) {
|
|
53
|
+
return {
|
|
54
|
+
findings: [{ severity: "HIGH", category: "missing", message: `Config file not found at ${filePath}`, fix_hint: "Verify path; if Claude Code is installed, default location is ~/.claude/settings.json" }],
|
|
55
|
+
summary: "FAIL — config file not found",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let config: any;
|
|
60
|
+
try {
|
|
61
|
+
config = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
62
|
+
} catch (e) {
|
|
63
|
+
return {
|
|
64
|
+
findings: [{ severity: "HIGH", category: "syntax", message: `JSON parse error: ${(e as Error).message}`, fix_hint: "Validate JSON syntax via `jq . settings.json`" }],
|
|
65
|
+
summary: "FAIL — syntax error",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check 1: permissions section structure
|
|
70
|
+
if (!config.permissions) {
|
|
71
|
+
findings.push({
|
|
72
|
+
severity: "MED",
|
|
73
|
+
category: "permissions",
|
|
74
|
+
message: "No permissions block. Claude Code will prompt on every tool call.",
|
|
75
|
+
fix_hint: "Add permissions section z allowlist of safe paths (Write/Edit) and tools (Bash command patterns).",
|
|
76
|
+
});
|
|
77
|
+
} else if (config.permissions.defaultMode === "bypassPermissions") {
|
|
78
|
+
findings.push({
|
|
79
|
+
severity: "HIGH",
|
|
80
|
+
category: "security",
|
|
81
|
+
message: "defaultMode set to 'bypassPermissions' — Claude can execute arbitrary tools without confirmation.",
|
|
82
|
+
fix_hint: "Use 'acceptEdits' for most use cases. Reserve 'bypassPermissions' only for trusted automation contexts (CI, VPS overnight chains).",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check 2: hooks
|
|
87
|
+
if (!config.hooks || Object.keys(config.hooks).length === 0) {
|
|
88
|
+
findings.push({
|
|
89
|
+
severity: "LOW",
|
|
90
|
+
category: "hooks",
|
|
91
|
+
message: "No hooks configured. Missing opportunity for pre-prompt context injection / pre-tool-use safety / stop-hook gates.",
|
|
92
|
+
fix_hint: "Common hooks worth setting: UserPromptSubmit (memory grep / context injection), PreToolUse (audit / safety), PostToolUse (logging).",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check 3: env vars
|
|
97
|
+
if (!config.env) {
|
|
98
|
+
findings.push({
|
|
99
|
+
severity: "LOW",
|
|
100
|
+
category: "env",
|
|
101
|
+
message: "No env vars configured.",
|
|
102
|
+
fix_hint: "Consider setting CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING or other harness-level controls.",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check 4: hardcoded API keys
|
|
107
|
+
const configString = JSON.stringify(config);
|
|
108
|
+
const apiKeyPatterns = [
|
|
109
|
+
/sk_live_[a-zA-Z0-9_]{20,}/g,
|
|
110
|
+
/sk-ant-[a-zA-Z0-9_-]{20,}/g,
|
|
111
|
+
/sk-[a-zA-Z0-9_-]{20,}/g,
|
|
112
|
+
/AIza[a-zA-Z0-9_-]{30,}/g,
|
|
113
|
+
/ghp_[a-zA-Z0-9]{30,}/g,
|
|
114
|
+
];
|
|
115
|
+
for (const pat of apiKeyPatterns) {
|
|
116
|
+
const m = configString.match(pat);
|
|
117
|
+
if (m) {
|
|
118
|
+
findings.push({
|
|
119
|
+
severity: "HIGH",
|
|
120
|
+
category: "secret_leak",
|
|
121
|
+
message: `Possible API key / token found in config: ${m[0].substring(0, 12)}…`,
|
|
122
|
+
fix_hint: "Move secrets to env vars OR external secret store (1Password CLI, age-encrypted file). Never commit raw secrets to settings.json.",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check 5: empty allow list
|
|
128
|
+
if (config.permissions?.allow && config.permissions.allow.length === 0) {
|
|
129
|
+
findings.push({
|
|
130
|
+
severity: "MED",
|
|
131
|
+
category: "permissions",
|
|
132
|
+
message: "permissions.allow is empty — every tool call will prompt.",
|
|
133
|
+
fix_hint: "Populate allow list z safe paths (e.g. Write(/path/to/safe-dir/**)) and Bash command patterns (e.g. Bash(ls *)).",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check 6: plugins enabled
|
|
138
|
+
if (config.enabledPlugins) {
|
|
139
|
+
const pluginCount = Object.keys(config.enabledPlugins).filter(k => config.enabledPlugins[k]).length;
|
|
140
|
+
if (pluginCount > 5) {
|
|
141
|
+
findings.push({
|
|
142
|
+
severity: "LOW",
|
|
143
|
+
category: "plugins",
|
|
144
|
+
message: `${pluginCount} plugins enabled. Each adds token overhead at startup.`,
|
|
145
|
+
fix_hint: "Disable unused plugins. Set value to false in enabledPlugins to keep config but skip load.",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const highCount = findings.filter(f => f.severity === "HIGH").length;
|
|
151
|
+
const medCount = findings.filter(f => f.severity === "MED").length;
|
|
152
|
+
const lowCount = findings.filter(f => f.severity === "LOW").length;
|
|
153
|
+
|
|
154
|
+
let summary: string;
|
|
155
|
+
if (highCount > 0) summary = `BLOCK — ${highCount} HIGH, ${medCount} MED, ${lowCount} LOW`;
|
|
156
|
+
else if (medCount > 0) summary = `WARN — ${medCount} MED, ${lowCount} LOW`;
|
|
157
|
+
else if (lowCount > 0) summary = `POLISH — ${lowCount} LOW`;
|
|
158
|
+
else summary = `PASS — no anti-patterns detected`;
|
|
159
|
+
|
|
160
|
+
return { findings, summary };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// =====================================================================
|
|
164
|
+
// Tool: audit_skills_dir
|
|
165
|
+
// =====================================================================
|
|
166
|
+
|
|
167
|
+
function auditSkillsDir(dirPath: string): { findings: ConfigFinding[]; summary: string; skill_count: number } {
|
|
168
|
+
const findings: ConfigFinding[] = [];
|
|
169
|
+
|
|
170
|
+
if (!fs.existsSync(dirPath)) {
|
|
171
|
+
return {
|
|
172
|
+
findings: [{ severity: "HIGH", category: "missing", message: `Skills directory not found at ${dirPath}`, fix_hint: "Default Claude Code skills location is ~/.claude/skills/" }],
|
|
173
|
+
summary: "FAIL — skills dir not found",
|
|
174
|
+
skill_count: 0,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const skills = fs.readdirSync(dirPath).filter(name => {
|
|
179
|
+
const fullPath = path.join(dirPath, name);
|
|
180
|
+
return fs.statSync(fullPath).isDirectory();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (skills.length === 0) {
|
|
184
|
+
findings.push({
|
|
185
|
+
severity: "LOW",
|
|
186
|
+
category: "empty",
|
|
187
|
+
message: "No skills installed.",
|
|
188
|
+
fix_hint: "Skills provide reusable workflows. Consider building skills for common multi-step tasks.",
|
|
189
|
+
});
|
|
190
|
+
return { findings, summary: "EMPTY — no skills", skill_count: 0 };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const skillName of skills) {
|
|
194
|
+
const skillDir = path.join(dirPath, skillName);
|
|
195
|
+
const skillMdPath = path.join(skillDir, "SKILL.md");
|
|
196
|
+
|
|
197
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
198
|
+
findings.push({
|
|
199
|
+
severity: "MED",
|
|
200
|
+
category: "skill_structure",
|
|
201
|
+
message: `Skill '${skillName}' missing SKILL.md`,
|
|
202
|
+
fix_hint: "Every skill needs SKILL.md with frontmatter (name + description) and body instructions.",
|
|
203
|
+
});
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const skillMd = fs.readFileSync(skillMdPath, "utf-8");
|
|
208
|
+
|
|
209
|
+
// Check frontmatter present
|
|
210
|
+
if (!skillMd.startsWith("---")) {
|
|
211
|
+
findings.push({
|
|
212
|
+
severity: "MED",
|
|
213
|
+
category: "skill_frontmatter",
|
|
214
|
+
message: `Skill '${skillName}' SKILL.md missing YAML frontmatter`,
|
|
215
|
+
fix_hint: "Add ---\\nname: skill-name\\ndescription: When to use\\n--- at top of SKILL.md",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check description quality
|
|
220
|
+
const descMatch = skillMd.match(/description:\s*(.+)/);
|
|
221
|
+
if (!descMatch) {
|
|
222
|
+
findings.push({
|
|
223
|
+
severity: "MED",
|
|
224
|
+
category: "skill_description",
|
|
225
|
+
message: `Skill '${skillName}' missing description field`,
|
|
226
|
+
fix_hint: "description field is what Claude uses to decide if skill matches user request. Be specific about when to invoke.",
|
|
227
|
+
});
|
|
228
|
+
} else if (descMatch[1].trim().length < 20) {
|
|
229
|
+
findings.push({
|
|
230
|
+
severity: "LOW",
|
|
231
|
+
category: "skill_description",
|
|
232
|
+
message: `Skill '${skillName}' description is short (${descMatch[1].trim().length} chars)`,
|
|
233
|
+
fix_hint: "Aim for 50-150 chars in description. Include trigger keywords + use cases.",
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check body length
|
|
238
|
+
const bodyAfterFrontmatter = skillMd.replace(/^---[\s\S]*?---\n/, "").trim();
|
|
239
|
+
if (bodyAfterFrontmatter.length < 100) {
|
|
240
|
+
findings.push({
|
|
241
|
+
severity: "MED",
|
|
242
|
+
category: "skill_body",
|
|
243
|
+
message: `Skill '${skillName}' body very short (${bodyAfterFrontmatter.length} chars)`,
|
|
244
|
+
fix_hint: "Skill body should provide step-by-step instructions Claude follows. Short skills often miss edge cases.",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const summary = findings.length === 0
|
|
250
|
+
? `PASS — ${skills.length} skills, no anti-patterns`
|
|
251
|
+
: `${findings.filter(f => f.severity === "HIGH").length} HIGH / ${findings.filter(f => f.severity === "MED").length} MED / ${findings.filter(f => f.severity === "LOW").length} LOW across ${skills.length} skills`;
|
|
252
|
+
|
|
253
|
+
return { findings, summary, skill_count: skills.length };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// =====================================================================
|
|
257
|
+
// Tool: audit_hooks_dir
|
|
258
|
+
// =====================================================================
|
|
259
|
+
|
|
260
|
+
function auditHooksDir(dirPath: string): { findings: ConfigFinding[]; summary: string; hook_count: number } {
|
|
261
|
+
const findings: ConfigFinding[] = [];
|
|
262
|
+
|
|
263
|
+
if (!fs.existsSync(dirPath)) {
|
|
264
|
+
return {
|
|
265
|
+
findings: [{ severity: "LOW", category: "missing", message: `Hooks directory not found at ${dirPath}`, fix_hint: "Default Claude Code hooks location is ~/.claude/hooks/" }],
|
|
266
|
+
summary: "EMPTY — no hooks dir",
|
|
267
|
+
hook_count: 0,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const hookFiles = fs.readdirSync(dirPath).filter(name => name.endsWith(".sh") || name.endsWith(".py") || name.endsWith(".js"));
|
|
272
|
+
|
|
273
|
+
if (hookFiles.length === 0) {
|
|
274
|
+
return { findings: [{ severity: "LOW", category: "empty", message: "No hook scripts found.", fix_hint: "Consider hooks for UserPromptSubmit (context injection), PreToolUse (audit), PostToolUse (logging)." }], summary: "EMPTY — no hooks", hook_count: 0 };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const hookFile of hookFiles) {
|
|
278
|
+
const fullPath = path.join(dirPath, hookFile);
|
|
279
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
280
|
+
const stats = fs.statSync(fullPath);
|
|
281
|
+
|
|
282
|
+
// Check executable bit (owner OR group OR world executable)
|
|
283
|
+
if (!(stats.mode & 0o111)) {
|
|
284
|
+
findings.push({
|
|
285
|
+
severity: "HIGH",
|
|
286
|
+
category: "permissions",
|
|
287
|
+
message: `Hook '${hookFile}' is not executable. Claude Code will NOT invoke it.`,
|
|
288
|
+
fix_hint: `Run: chmod +x ${fullPath}`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check error handling for bash hooks
|
|
293
|
+
if (hookFile.endsWith(".sh")) {
|
|
294
|
+
if (!content.includes("set -e") && !content.includes("set -euo")) {
|
|
295
|
+
findings.push({
|
|
296
|
+
severity: "MED",
|
|
297
|
+
category: "error_handling",
|
|
298
|
+
message: `Hook '${hookFile}' missing 'set -e' or 'set -euo pipefail'`,
|
|
299
|
+
fix_hint: "Add 'set -euo pipefail' at top of bash hook to fail-fast on errors instead of silent corruption.",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check for hardcoded paths to current user
|
|
305
|
+
const userMatches = content.match(/\/Users\/[a-zA-Z0-9_-]+/g);
|
|
306
|
+
if (userMatches) {
|
|
307
|
+
findings.push({
|
|
308
|
+
severity: "LOW",
|
|
309
|
+
category: "portability",
|
|
310
|
+
message: `Hook '${hookFile}' has hardcoded user path (e.g. ${userMatches[0]})`,
|
|
311
|
+
fix_hint: "Use $HOME or ~/ instead of /Users/<name>/ for portability across machines.",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check for credentials in hook
|
|
316
|
+
const apiKeyPatterns = [/sk_live_[a-zA-Z0-9_]{20,}/, /sk-[a-zA-Z0-9_-]{20,}/];
|
|
317
|
+
for (const pat of apiKeyPatterns) {
|
|
318
|
+
if (content.match(pat)) {
|
|
319
|
+
findings.push({
|
|
320
|
+
severity: "HIGH",
|
|
321
|
+
category: "secret_leak",
|
|
322
|
+
message: `Hook '${hookFile}' contains hardcoded API key.`,
|
|
323
|
+
fix_hint: "Move secrets to env vars OR external secret store. Read via $VAR_NAME at hook runtime.",
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const summary = findings.length === 0
|
|
330
|
+
? `PASS — ${hookFiles.length} hooks, no anti-patterns`
|
|
331
|
+
: `${findings.filter(f => f.severity === "HIGH").length} HIGH / ${findings.filter(f => f.severity === "MED").length} MED / ${findings.filter(f => f.severity === "LOW").length} LOW across ${hookFiles.length} hooks`;
|
|
332
|
+
|
|
333
|
+
return { findings, summary, hook_count: hookFiles.length };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// =====================================================================
|
|
337
|
+
// MCP server wiring
|
|
338
|
+
// =====================================================================
|
|
339
|
+
|
|
340
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
341
|
+
tools: [
|
|
342
|
+
{
|
|
343
|
+
name: "audit_config_file",
|
|
344
|
+
description: "Audit a Claude Code settings.json configuration file for anti-patterns: missing permissions, bypassPermissions risk, hardcoded API keys, empty allow lists, excessive plugins. Returns findings with severity HIGH/MED/LOW and fix hints.",
|
|
345
|
+
inputSchema: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {
|
|
348
|
+
file_path: {
|
|
349
|
+
type: "string",
|
|
350
|
+
description: "Absolute path to Claude Code settings.json (typical: ~/.claude/settings.json)",
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
required: ["file_path"],
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "audit_skills_dir",
|
|
358
|
+
description: "Audit a Claude Code skills directory for quality issues: missing SKILL.md, missing frontmatter, short descriptions, sparse body content. Returns findings + skill count.",
|
|
359
|
+
inputSchema: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
dir_path: {
|
|
363
|
+
type: "string",
|
|
364
|
+
description: "Absolute path to Claude Code skills directory (typical: ~/.claude/skills/)",
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
required: ["dir_path"],
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: "audit_hooks_dir",
|
|
372
|
+
description: "Audit a Claude Code hooks directory: missing executable bit, missing 'set -e' error handling, hardcoded user paths, leaked credentials in hook scripts. Returns findings + hook count.",
|
|
373
|
+
inputSchema: {
|
|
374
|
+
type: "object",
|
|
375
|
+
properties: {
|
|
376
|
+
dir_path: {
|
|
377
|
+
type: "string",
|
|
378
|
+
description: "Absolute path to Claude Code hooks directory (typical: ~/.claude/hooks/)",
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
required: ["dir_path"],
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
}));
|
|
386
|
+
|
|
387
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
388
|
+
const { name, arguments: args } = request.params;
|
|
389
|
+
|
|
390
|
+
let result: any;
|
|
391
|
+
if (name === "audit_config_file") {
|
|
392
|
+
result = auditConfigFile(args!.file_path as string);
|
|
393
|
+
} else if (name === "audit_skills_dir") {
|
|
394
|
+
result = auditSkillsDir(args!.dir_path as string);
|
|
395
|
+
} else if (name === "audit_hooks_dir") {
|
|
396
|
+
result = auditHooksDir(args!.dir_path as string);
|
|
397
|
+
} else {
|
|
398
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
content: [
|
|
403
|
+
{
|
|
404
|
+
type: "text",
|
|
405
|
+
text: JSON.stringify(result, null, 2),
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
async function main() {
|
|
412
|
+
const transport = new StdioServerTransport();
|
|
413
|
+
await server.connect(transport);
|
|
414
|
+
console.error("mcp-audit-claude-code v0.1.0 listening on stdio");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
main().catch((err) => {
|
|
418
|
+
console.error("Fatal error:", err);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ES2020",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|