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