Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CLAUDE.local.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# CLAUDE.local.md

---

## [最高行为准则 - 严禁污染上游]

- 严禁以任何形式修改、暂存(stage)或提交项目全局的 `AGENTS.md` 或 `CLAUDE.md`,它们对你而言是绝对只读的。
- 所有个人偏好、本地工作流或提示词调整,只能写入当前本地文件(`AGENTS.local.md` 或 `CLAUDE.local.md`)。

---
13 changes: 13 additions & 0 deletions packages/opencode/src/session/instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Config.S
}
}
}

// .local.md files are project-local personal overrides (typically gitignored),
// stacked on top of the main AGENTS.md/CLAUDE.md. Like the main files, local
// overrides are first-match-wins across candidates: AGENTS.local.md beats CLAUDE.local.md.
if (!Flag.MIMOCODE_DISABLE_CLAUDE_CODE_PROMPT) {
for (const name of ["AGENTS.local.md", "CLAUDE.local.md"]) {
const local = yield* fs.findUp(name, ctx.directory, ctx.worktree)
if (local.length > 0) {
local.forEach((item) => paths.add(path.resolve(item)))
break // first-match-wins
}
}
}
}

for (const file of globalFiles()) {
Expand Down
80 changes: 80 additions & 0 deletions packages/opencode/test/session/instruction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,86 @@ describe("Instruction.system", () => {
}
}
})

test("loads CLAUDE.local.md alongside AGENTS.md (stacking)", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(path.join(dir, "AGENTS.md"), "# Project Instructions")
await Bun.write(path.join(dir, "CLAUDE.local.md"), "# Personal Local")
},
})
await Instance.provide({
directory: tmp.path,
fn: () =>
run(
Instruction.Service.use((svc) =>
Effect.gen(function* () {
const paths = yield* svc.systemPaths()
expect(paths.has(path.join(tmp.path, "AGENTS.md"))).toBe(true)
expect(paths.has(path.join(tmp.path, "CLAUDE.local.md"))).toBe(true)

const rules = (yield* svc.system()).content
expect(rules).toContain(
`Instructions from: ${path.join(tmp.path, "CLAUDE.local.md")}\n# Personal Local`,
)
}),
),
),
})
})

test("loads CLAUDE.local.md alongside CLAUDE.md fallback (no AGENTS.md)", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(path.join(dir, "CLAUDE.md"), "# Claude Instructions")
await Bun.write(path.join(dir, "CLAUDE.local.md"), "# Personal Local")
},
})
await Instance.provide({
directory: tmp.path,
fn: () =>
run(
Instruction.Service.use((svc) =>
Effect.gen(function* () {
const paths = yield* svc.systemPaths()
expect(paths.has(path.join(tmp.path, "CLAUDE.md"))).toBe(true)
expect(paths.has(path.join(tmp.path, "CLAUDE.local.md"))).toBe(true)
}),
),
),
})
})

test("prefers AGENTS.local.md over CLAUDE.local.md (first-match-wins)", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(path.join(dir, "AGENTS.md"), "# Project Instructions")
await Bun.write(path.join(dir, "AGENTS.local.md"), "# Agents Local")
await Bun.write(path.join(dir, "CLAUDE.local.md"), "# Claude Local")
},
})
await Instance.provide({
directory: tmp.path,
fn: () =>
run(
Instruction.Service.use((svc) =>
Effect.gen(function* () {
const paths = yield* svc.systemPaths()
expect(paths.has(path.join(tmp.path, "AGENTS.local.md"))).toBe(true)
expect(paths.has(path.join(tmp.path, "CLAUDE.local.md"))).toBe(false)

const rules = (yield* svc.system()).content
expect(rules).toContain(
`Instructions from: ${path.join(tmp.path, "AGENTS.local.md")}\n# Agents Local`,
)
expect(rules).not.toContain(
`Instructions from: ${path.join(tmp.path, "CLAUDE.local.md")}\n# Claude Local`,
)
}),
),
),
})
})
})

describe("Instruction.systemPaths MIMOCODE_CONFIG_DIR", () => {
Expand Down