Skip to main content

claude_native/rules/
tooling.rs

1use crate::rules::*;
2use crate::scan::ProjectContext;
3
4// ── Rule 4.1: MCP servers configured ────────────────────────────────
5
6pub struct McpServersConfigured;
7
8impl Rule for McpServersConfigured {
9    fn id(&self) -> &str { "4.1" }
10    fn name(&self) -> &str { "MCP servers configured" }
11    fn dimension(&self) -> Dimension { Dimension::Tooling }
12    fn severity(&self) -> Severity { Severity::Low }
13
14    fn check(&self, ctx: &ProjectContext) -> RuleResult {
15        if ctx.mcp_json_path.is_some() {
16            self.pass()
17        } else {
18            self.warn(
19                "No .claude/.mcp.json found",
20                Suggestion {
21                    priority: SuggestionPriority::NiceToHave,
22                    title: "Configure MCP servers".into(),
23                    description: "If your project uses external services (GitHub, databases, APIs), create .claude/.mcp.json to give Claude direct access. MCP tools are more reliable than shell workarounds.".into(),
24                    effort: Effort::Hour,
25                },
26            )
27        }
28    }
29}
30
31// ── Rule 4.2: Auto-format hook ──────────────────────────────────────
32
33pub struct AutoFormatHook;
34
35impl Rule for AutoFormatHook {
36    fn id(&self) -> &str { "4.2" }
37    fn name(&self) -> &str { "Hooks for auto-formatting" }
38    fn dimension(&self) -> Dimension { Dimension::Tooling }
39    fn severity(&self) -> Severity { Severity::Medium }
40
41    fn check(&self, ctx: &ProjectContext) -> RuleResult {
42        if ctx.has_post_tool_use_hook_for_format() {
43            self.pass()
44        } else {
45            self.fail(
46                "No PostToolUse hook for auto-formatting after edits",
47                Suggestion {
48                    priority: SuggestionPriority::HighImpact,
49                    title: "Add auto-format hook".into(),
50                    description: "Add a PostToolUse hook in .claude/settings.json that runs your formatter (prettier, black, rustfmt, gofmt) after Edit/Write operations. This prevents style-related linter errors.".into(),
51                    effort: Effort::Minutes,
52                },
53            )
54        }
55    }
56}
57
58// ── Rule 4.3: Dangerous operation protection ────────────────────────
59
60pub struct DangerousOpProtection;
61
62impl Rule for DangerousOpProtection {
63    fn id(&self) -> &str { "4.3" }
64    fn name(&self) -> &str { "Hooks for dangerous operation protection" }
65    fn dimension(&self) -> Dimension { Dimension::Tooling }
66    fn severity(&self) -> Severity { Severity::Low }
67
68    fn check(&self, ctx: &ProjectContext) -> RuleResult {
69        if ctx.has_pre_tool_use_protection_hook() {
70            self.pass()
71        } else {
72            self.warn(
73                "No PreToolUse hooks for blocking dangerous operations",
74                Suggestion {
75                    priority: SuggestionPriority::NiceToHave,
76                    title: "Add protection hooks".into(),
77                    description: "Add PreToolUse hooks to block editing sensitive files (.env, lock files, CI configs). Prevention is cheaper than correction.".into(),
78                    effort: Effort::Hour,
79                },
80            )
81        }
82    }
83}
84
85// ── Rule 4.4: Custom skills ─────────────────────────────────────────
86
87pub struct CustomSkills;
88
89impl Rule for CustomSkills {
90    fn id(&self) -> &str { "4.4" }
91    fn name(&self) -> &str { "Custom skills for workflows" }
92    fn dimension(&self) -> Dimension { Dimension::Tooling }
93    fn severity(&self) -> Severity { Severity::Low }
94
95    fn check(&self, ctx: &ProjectContext) -> RuleResult {
96        if ctx.has_claude_skills_dir {
97            self.pass()
98        } else {
99            self.warn(
100                "No .claude/skills/ directory found",
101                Suggestion {
102                    priority: SuggestionPriority::NiceToHave,
103                    title: "Create custom skills".into(),
104                    description: "Create .claude/skills/ with SKILL.md files for repetitive workflows (deploy, review, test). Skills load on-demand and encode complex multi-step processes.".into(),
105                    effort: Effort::Hour,
106                },
107            )
108        }
109    }
110}
111
112// ── Rule 4.5: Permission allow-list ─────────────────────────────────
113
114pub struct PermissionAllowList;
115
116impl Rule for PermissionAllowList {
117    fn id(&self) -> &str { "4.5" }
118    fn name(&self) -> &str { "Permission allow-list configured" }
119    fn dimension(&self) -> Dimension { Dimension::Tooling }
120    fn severity(&self) -> Severity { Severity::Medium }
121
122    fn check(&self, ctx: &ProjectContext) -> RuleResult {
123        if ctx.settings_has_permissions() {
124            self.pass()
125        } else {
126            self.fail(
127                "No permission allow-list in .claude/settings.json",
128                Suggestion {
129                    priority: SuggestionPriority::QuickWin,
130                    title: "Configure permission allow-list".into(),
131                    description: "Add permissions.allow to .claude/settings.json for safe commands (test runners, build tools, git). Every permission prompt interrupts Claude's flow.".into(),
132                    effort: Effort::Minutes,
133                },
134            )
135        }
136    }
137}
138
139// ── Rule 4.6: Path-scoped rules ────────────────────────────────────
140
141pub struct PathScopedRules;
142
143impl Rule for PathScopedRules {
144    fn id(&self) -> &str { "4.6" }
145    fn name(&self) -> &str { ".claude/rules/ for path-scoped instructions" }
146    fn dimension(&self) -> Dimension { Dimension::Tooling }
147    fn severity(&self) -> Severity { Severity::Low }
148
149    fn check(&self, ctx: &ProjectContext) -> RuleResult {
150        if ctx.has_claude_rules_dir {
151            self.pass()
152        } else {
153            self.warn(
154                "No .claude/rules/ directory found",
155                Suggestion {
156                    priority: SuggestionPriority::NiceToHave,
157                    title: "Create path-scoped rules".into(),
158                    description: "Create .claude/rules/ with topic-specific .md files that use paths: frontmatter. Rules load only when Claude works with matching files, keeping CLAUDE.md small.".into(),
159                    effort: Effort::Hour,
160                },
161            )
162        }
163    }
164}
165
166// ── Rule 4.7: Subagent configuration ────────────────────────────────
167
168pub struct SubagentConfig;
169
170impl Rule for SubagentConfig {
171    fn id(&self) -> &str { "4.7" }
172    fn name(&self) -> &str { "Subagent configuration (.claude/agents/)" }
173    fn dimension(&self) -> Dimension { Dimension::Tooling }
174    fn severity(&self) -> Severity { Severity::Low }
175
176    fn check(&self, ctx: &ProjectContext) -> RuleResult {
177        if ctx.has_claude_agents_dir {
178            self.pass()
179        } else {
180            self.warn(
181                "No .claude/agents/ directory for custom subagents",
182                Suggestion {
183                    priority: SuggestionPriority::NiceToHave,
184                    title: "Create custom subagents".into(),
185                    description: "Create .claude/agents/ with .md files defining specialized subagents. Subagents isolate expensive operations (research, testing) from your main context window.".into(),
186                    effort: Effort::Hour,
187                },
188            )
189        }
190    }
191}