diff --git a/src/hooks/rules-injector/constants.ts b/src/hooks/rules-injector/constants.ts index 6ac2cbbbc..0c07169fe 100644 --- a/src/hooks/rules-injector/constants.ts +++ b/src/hooks/rules-injector/constants.ts @@ -26,4 +26,6 @@ export const GITHUB_INSTRUCTIONS_PATTERN = /\.instructions\.md$/; export const USER_RULE_DIR = ".claude/rules"; +export const OPENCODE_USER_RULE_DIRS = [".sisyphus/rules", ".opencode/rules"]; + export const RULE_EXTENSIONS = [".md", ".mdc"]; diff --git a/src/hooks/rules-injector/finder.ts b/src/hooks/rules-injector/finder.ts index 4cd19fb35..b7a4ea22a 100644 --- a/src/hooks/rules-injector/finder.ts +++ b/src/hooks/rules-injector/finder.ts @@ -1,3 +1,3 @@ export { findProjectRoot } from "./project-root-finder"; export { calculateDistance } from "./rule-distance"; -export { findRuleFiles } from "./rule-file-finder"; +export { findRuleFiles, type FindRuleFilesOptions } from "./rule-file-finder"; diff --git a/src/hooks/rules-injector/hook.ts b/src/hooks/rules-injector/hook.ts index fec4ffd2a..f46af4570 100644 --- a/src/hooks/rules-injector/hook.ts +++ b/src/hooks/rules-injector/hook.ts @@ -32,6 +32,7 @@ const TRACKED_TOOLS = ["read", "write", "edit", "multiedit"]; export function createRulesInjectorHook( ctx: PluginInput, modelCacheState?: { anthropicContext1MEnabled: boolean }, + options?: { skipClaudeUserRules?: boolean }, ) { const truncator = createDynamicTruncator(ctx, modelCacheState); const { getSessionCache, clearSessionCache } = createSessionCacheStore(); @@ -39,6 +40,9 @@ export function createRulesInjectorHook( workspaceDirectory: ctx.directory, truncator, getSessionCache, + ruleFinderOptions: options?.skipClaudeUserRules + ? { skipClaudeUserRules: true } + : undefined, }); const toolExecuteAfter = async ( diff --git a/src/hooks/rules-injector/injector.ts b/src/hooks/rules-injector/injector.ts index 6b295000b..58340f0d9 100644 --- a/src/hooks/rules-injector/injector.ts +++ b/src/hooks/rules-injector/injector.ts @@ -2,6 +2,7 @@ import { readFileSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { relative, resolve } from "node:path"; import { findProjectRoot, findRuleFiles } from "./finder"; +import type { FindRuleFilesOptions } from "./rule-file-finder"; import { createContentHash, isDuplicateByContentHash, @@ -82,6 +83,7 @@ export function createRuleInjectionProcessor(deps: { workspaceDirectory: string; truncator: DynamicTruncator; getSessionCache: (sessionID: string) => SessionInjectedRulesCache; + ruleFinderOptions?: FindRuleFilesOptions; }): { processFilePathForInjection: ( filePath: string, @@ -89,7 +91,7 @@ export function createRuleInjectionProcessor(deps: { output: ToolExecuteOutput ) => Promise; } { - const { workspaceDirectory, truncator, getSessionCache } = deps; + const { workspaceDirectory, truncator, getSessionCache, ruleFinderOptions } = deps; async function processFilePathForInjection( filePath: string, @@ -103,7 +105,7 @@ export function createRuleInjectionProcessor(deps: { const cache = getSessionCache(sessionID); const home = homedir(); - const ruleFileCandidates = findRuleFiles(projectRoot, home, resolved); + const ruleFileCandidates = findRuleFiles(projectRoot, home, resolved, ruleFinderOptions); const toInject: RuleToInject[] = []; let dirty = false; diff --git a/src/hooks/rules-injector/rule-file-finder.ts b/src/hooks/rules-injector/rule-file-finder.ts index a08b12a10..98bd6942b 100644 --- a/src/hooks/rules-injector/rule-file-finder.ts +++ b/src/hooks/rules-injector/rule-file-finder.ts @@ -4,10 +4,20 @@ import { PROJECT_RULE_FILES, PROJECT_RULE_SUBDIRS, USER_RULE_DIR, + OPENCODE_USER_RULE_DIRS, } from "./constants"; import type { RuleFileCandidate } from "./types"; import { findRuleFilesRecursive, safeRealpathSync } from "./rule-file-scanner"; +export interface FindRuleFilesOptions { + /** + * When true, skip loading rules from ~/.claude/rules/. + * Use when claude_code integration is disabled to prevent + * Claude Code-specific instructions from leaking into non-Claude agents. + */ + skipClaudeUserRules?: boolean; +} + /** * Find all rule files for a given context. * Searches from currentFile upward to projectRoot for rule directories, @@ -25,6 +35,7 @@ export function findRuleFiles( projectRoot: string | null, homeDir: string, currentFile: string, + options?: FindRuleFilesOptions, ): RuleFileCandidate[] { const candidates: RuleFileCandidate[] = []; const seenRealPaths = new Set(); @@ -89,22 +100,31 @@ export function findRuleFiles( } } - // Search user-level rule directory (~/.claude/rules) - const userRuleDir = join(homeDir, USER_RULE_DIR); - const userFiles: string[] = []; - findRuleFilesRecursive(userRuleDir, userFiles); + // Search user-level rule directories + // Always search OpenCode-native dirs (~/.sisyphus/rules, ~/.opencode/rules) + const userRuleDirs: string[] = OPENCODE_USER_RULE_DIRS.map((dir) => join(homeDir, dir)); - for (const filePath of userFiles) { - const realPath = safeRealpathSync(filePath); - if (seenRealPaths.has(realPath)) continue; - seenRealPaths.add(realPath); + // Only search ~/.claude/rules when claude_code integration is not disabled + if (!options?.skipClaudeUserRules) { + userRuleDirs.push(join(homeDir, USER_RULE_DIR)); + } - candidates.push({ - path: filePath, - realPath, - isGlobal: true, - distance: 9999, // Global rules always have max distance - }); + for (const userRuleDir of userRuleDirs) { + const userFiles: string[] = []; + findRuleFilesRecursive(userRuleDir, userFiles); + + for (const filePath of userFiles) { + const realPath = safeRealpathSync(filePath); + if (seenRealPaths.has(realPath)) continue; + seenRealPaths.add(realPath); + + candidates.push({ + path: filePath, + realPath, + isGlobal: true, + distance: 9999, // Global rules always have max distance + }); + } } // Sort by distance (closest first, then global rules last) diff --git a/src/plugin/hooks/create-tool-guard-hooks.ts b/src/plugin/hooks/create-tool-guard-hooks.ts index 78460ff5f..2eba9eb30 100644 --- a/src/plugin/hooks/create-tool-guard-hooks.ts +++ b/src/plugin/hooks/create-tool-guard-hooks.ts @@ -91,9 +91,16 @@ export function createToolGuardHooks(args: { ? safeHook("empty-task-response-detector", () => createEmptyTaskResponseDetectorHook(ctx)) : null + const cc = pluginConfig.claude_code + const claudeCodeDisabled = cc != null + && cc.hooks === false + && cc.skills === false + && cc.agents === false const rulesInjector = isHookEnabled("rules-injector") ? safeHook("rules-injector", () => - createRulesInjectorHook(ctx, modelCacheState)) + createRulesInjectorHook(ctx, modelCacheState, { + skipClaudeUserRules: claudeCodeDisabled ?? false, + })) : null const tasksTodowriteDisabler = isHookEnabled("tasks-todowrite-disabler")