fix(rules-injector): respect claude_code config for user rule loading

When claude_code integration is disabled in oh-my-opencode.json, the
rules-injector now skips loading rules from ~/.claude/rules/ to prevent
Claude Code-specific instructions from leaking into non-Claude agents
(e.g., GPT-5.4 Sisyphus), which causes agent hallucination.

Also adds ~/.sisyphus/rules/ and ~/.opencode/rules/ as OpenCode-native
user rule directories that are always searched regardless of config.

Fixes #2920

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tran Quang
2026-03-29 10:13:55 +07:00
parent 9fc56ab544
commit a1a2bb4b07
6 changed files with 52 additions and 18 deletions
+2
View File
@@ -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"];
+1 -1
View File
@@ -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";
+4
View File
@@ -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 (
+4 -2
View File
@@ -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<void>;
} {
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;
+34 -14
View File
@@ -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<string>();
@@ -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)
+7 -1
View File
@@ -91,9 +91,15 @@ export function createToolGuardHooks(args: {
? safeHook("empty-task-response-detector", () => createEmptyTaskResponseDetectorHook(ctx))
: null
const claudeCodeDisabled = pluginConfig.claude_code
&& !pluginConfig.claude_code.hooks
&& !pluginConfig.claude_code.skills
&& !pluginConfig.claude_code.agents
const rulesInjector = isHookEnabled("rules-injector")
? safeHook("rules-injector", () =>
createRulesInjectorHook(ctx, modelCacheState))
createRulesInjectorHook(ctx, modelCacheState, {
skipClaudeUserRules: claudeCodeDisabled ?? false,
}))
: null
const tasksTodowriteDisabler = isHookEnabled("tasks-todowrite-disabler")