claude_native/rules/
tooling.rs1use crate::rules::*;
2use crate::scan::ProjectContext;
3
4pub 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
31pub 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
58pub 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
85pub 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
112pub 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
139pub 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
166pub 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}