1616import static io .github .cowwoc .requirements13 .java .DefaultJavaValidators .requireThat ;
1717
1818/**
19- * Injects audience-filtered rules from {@code .claude/cat/rules/} into main agent context.
19+ * Injects audience-filtered rules from plugin-bundled and project-local rule directories into main
20+ * agent context.
2021 * <p>
21- * Discovers all rule files, filters to those with {@code mainAgent: true}, applies any {@code paths}
22- * restrictions, and injects matching content as additional context.
22+ * Discovers all rule files from both {@code ${CLAUDE_PLUGIN_ROOT}/rules/} (plugin-bundled) and
23+ * {@code ${projectDir}/.claude/cat/rules/} (project-local), concatenates them (plugin-bundled
24+ * first, project-local second), filters to those with {@code mainAgent: true}, applies any
25+ * {@code paths} restrictions, and injects matching content as additional context.
2326 */
2427public final class InjectMainAgentRules implements SessionStartHandler
2528{
@@ -39,6 +42,13 @@ public InjectMainAgentRules(JvmScope scope)
3942
4043 /**
4144 * Discovers and injects CAT rules applicable to the main agent.
45+ * <p>
46+ * Reads from two sources in order (plugin-bundled first, project-local second):
47+ * <ol>
48+ * <li>{@code ${CLAUDE_PLUGIN_ROOT}/rules/} — plugin-bundled rules</li>
49+ * <li>{@code ${projectDir}/.claude/cat/rules/} — project-local rules</li>
50+ * </ol>
51+ * Both sources are concatenated; no filename-based deduplication is performed.
4252 *
4353 * @param input the hook input
4454 * @return a result with the injected rules content, or empty if no rules apply
@@ -49,11 +59,12 @@ public Result handle(HookInput input)
4959 {
5060 requireThat (input , "input" ).isNotNull ();
5161
52- Path rulesDir = scope .getClaudeProjectDir ().resolve (".claude/cat/rules" );
62+ Path pluginRulesDir = scope .getClaudePluginRoot ().resolve ("rules" );
63+ Path projectRulesDir = scope .getClaudeProjectDir ().resolve (".claude/cat/rules" );
5364 // Rules with paths: restrictions are injected dynamically by InjectPathRules (PreToolUse hook)
5465 // when matching files are accessed. Only non-paths rules are injected here at session start.
55- String content = RulesDiscovery .getCatRulesForAudience (rulesDir , scope . getYamlMapper ( ),
56- RulesDiscovery ::filterForMainAgent , List .of ());
66+ String content = RulesDiscovery .getCatRulesForAudience (List . of ( pluginRulesDir , projectRulesDir ),
67+ scope . getYamlMapper (), RulesDiscovery ::filterForMainAgent , List .of ());
5768 if (content .isBlank ())
5869 return Result .empty ();
5970
0 commit comments