Conversation
Adds support for loading *.md rule files from:
- ~/.opencode/rules/ (global, all projects)
- {project}/.opencode/rules/ (project-specific, overrides global by filename)
Rules are injected into the system prompt alongside AGENTS.md content.
Closes #48
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
This PR doesn't fully meet our contributing guidelines and PR template. What needs to be fixed:
Please edit this PR description to address the above within 2 hours, or it will be automatically closed. If you believe this was flagged incorrectly, please let a maintainer know. |
There was a problem hiding this comment.
Pull request overview
Adds automatic loading of Markdown “rules” files from both the user’s global rules directory and the project’s rules directory, so they can be incorporated into the session system instructions alongside existing instruction sources.
Changes:
- Extend
Instruction.systemPaths()to include~/.opencode/rules/*.mdand{project}/.opencode/rules/*.md, with project files overriding global files by filename. - Respect
OPENCODE_DISABLE_PROJECT_CONFIGso project rules don’t cross the trust boundary when project config is disabled. - Add a dedicated test suite validating rules discovery, overriding behavior, and file filtering.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/opencode/src/session/instruction.ts | Adds global + project rules directory scanning and override logic to Instruction.systemPaths(). |
| packages/opencode/test/session/instruction-rules.test.ts | Adds tests for rules discovery, override precedence, trust-boundary behavior, and .md-only filtering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const projectRuleFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG | ||
| ? [] | ||
| : yield* fs | ||
| .glob("*.md", { | ||
| cwd: path.join(Instance.directory, ".opencode", "rules"), | ||
| absolute: true, | ||
| include: "file", | ||
| }) | ||
| .pipe(Effect.catch(() => Effect.succeed([] as string[]))) | ||
|
|
There was a problem hiding this comment.
Project rules are globbed from path.join(Instance.directory, ".opencode", "rules"), which only works when the instance directory is the project root. If the user runs OpenCode from a subdirectory, rules placed at {worktree}/.opencode/rules/*.md won’t be discovered. Consider using Instance.worktree (git root) for the project rules directory, or fs.findUp(".opencode/rules", Instance.directory, Instance.worktree) to locate the rules dir within the project boundary.
| const projectRuleFiles = Flag.OPENCODE_DISABLE_PROJECT_CONFIG | |
| ? [] | |
| : yield* fs | |
| .glob("*.md", { | |
| cwd: path.join(Instance.directory, ".opencode", "rules"), | |
| absolute: true, | |
| include: "file", | |
| }) | |
| .pipe(Effect.catch(() => Effect.succeed([] as string[]))) | |
| const projectRulesDir = Flag.OPENCODE_DISABLE_PROJECT_CONFIG | |
| ? [] | |
| : yield* fs.findUp(".opencode/rules", Instance.directory, Instance.worktree) | |
| const projectRuleFiles = | |
| Flag.OPENCODE_DISABLE_PROJECT_CONFIG || projectRulesDir.length === 0 | |
| ? [] | |
| : yield* fs | |
| .glob("*.md", { | |
| cwd: path.resolve(projectRulesDir[0]), | |
| absolute: true, | |
| include: "file", | |
| }) | |
| .pipe(Effect.catch(() => Effect.succeed([] as string[]))) |
| test("loads *.md files from project rules directory", async () => { | ||
| await using homeTmp = await tmpdir() | ||
| await using projectTmp = await tmpdir({ | ||
| init: async (dir) => { | ||
| await Bun.write(path.join(dir, ".opencode", "rules", "local.md"), "# Local Rules") | ||
| }, | ||
| }) | ||
|
|
||
| const originalHome = process.env.OPENCODE_TEST_HOME | ||
| process.env.OPENCODE_TEST_HOME = homeTmp.path | ||
|
|
||
| try { | ||
| await Instance.provide({ | ||
| directory: projectTmp.path, | ||
| fn: async () => { | ||
| const paths = await Instruction.systemPaths() | ||
| expect(paths.has(path.join(projectTmp.path, ".opencode", "rules", "local.md"))).toBe(true) | ||
| }, | ||
| }) | ||
| } finally { | ||
| process.env.OPENCODE_TEST_HOME = originalHome | ||
| } | ||
| }) |
There was a problem hiding this comment.
The test suite doesn’t currently cover the common case where OpenCode is invoked from a subdirectory: Instance.directory is nested but rules live in {worktree}/.opencode/rules/*.md. Add a test that provides an instance rooted in a nested directory and verifies project rules in the worktree root are still loaded (and still override globals by filename).
| import path from "path" | ||
| import { Instruction } from "../../src/session/instruction" | ||
| import { Instance } from "../../src/project/instance" | ||
| import { Global } from "../../src/global" |
There was a problem hiding this comment.
Global is imported but not used in this test file. Removing the unused import will keep the test code tidy and avoid unused-import lint failures if/when stricter TS options or a linter are enabled.
| import { Global } from "../../src/global" |
What
~/.opencode/rules/.md と {project}/.opencode/rules/.md からルールファイルを自動読み込み。
Why
Claude Code は ~/.claude/rules/ からルールを読み込むが、OpenCode には対応機能がなかった。
Closes #48
Changes
Review
Test plan