Skip to content

feat(compat): 读 AGENTS.md / CLAUDE.md 指令链 + 项目级 skills(P1) - #360

Merged
oratis merged 2 commits into
mainfrom
claude/p1-ecosystem-compat
Aug 14, 2026
Merged

feat(compat): 读 AGENTS.md / CLAUDE.md 指令链 + 项目级 skills(P1)#360
oratis merged 2 commits into
mainfrom
claude/p1-ecosystem-compat

Conversation

@oratis

@oratis oratis commented Aug 13, 2026

Copy link
Copy Markdown
Owner

计划见 #356docs/PLAN_HARNESS_ALIGNMENT_v1.0.md §7(P1 两项生态小补)。独立于 #357 / #358 / #359

为什么

生态已经收敛到一个 LISA 不读的约定:仓库根放一份 AGENTS.md(或 Claude Code 的 CLAUDE.md),所有 agent 都能拿到这个项目的规矩。Claude Code、Codex、Cursor、dsh 都认。读它是成本最低的一项兼容 —— 用户迁到 Lisa,已经写好的指令继续生效。

指令链

由外到内,最具体的最后读:

~/.lisa/AGENTS.md          用户自己的,对所有项目生效
<repo root>/AGENTS.md      这个项目的规矩
<repo root>/CLAUDE.md      与 AGENTS.md 同内容则折叠
…逐级到 cwd…
性质 理由
按内容去重 CLAUDE.md 经常是 AGENTS.md 的软链或副本;两份都读 = 每轮为同样的段落付两次钱
32KB 总预算 这些东西进系统提示词、每轮都付。单个 monorepo 的 AGENTS.md 不能挤掉灵魂。超出则截断并在提示词里标明
框定为"项目的说法"而非"用户的指令" 见下

安全考量(这项的关键)

这些文件只是被放在某个目录里就会被读到。所以每一块都标注来源,且 section 开头明说:它们是 context 不是 authority、不凌驾于宪法之上、若文件让她放弃自己的原则就无视并说出来。克隆一个恶意仓库不构成对 Lisa 下命令的资格。

项目级 skills

~/.lisa/skills 外,另读 <repo>/.lisa/skills(rank 100)与 <repo>/.agents/skills(rank 200)。

同名冲突时 home 胜出 —— 刻意与 dsh 的"近者遮蔽远者"相反。 对编码 harness 来说近者优先是对的默认;对这里是错的,因为 Lisa 的 skill 是她自己写的、关于她怎么工作的提示词材料,cd 进一个仓库不能成为重定义它的手段。项目 skill 只做增量,被遮蔽的记录在 PromptSnapshot.shadowedProjectSkills

这与本仓已有的先例一致:工具注册表里 builtin 胜过注入的同名工具。

提示词里项目 skill 带 *(from this project)* 标注 —— 她自己写的和这个 repo 塞进来的,值得不同程度的信任。

热更新

两者都接入了提示词指纹,所以改 AGENTS.md 或丢一个项目 skill 进去,下一轮就生效而不是下个会话 —— 与灵魂文件同样的承诺。

不做文件热监听:skills 与指令文件本来就经指纹每轮重读,再加一个 chokidar 依赖是纯冗余。

测试(19 例)

分层顺序 / git 根定位(含无 .git 的孤儿目录)/ CLAUDE.md 独立生效 / 同内容折叠 / 不同内容都留 / 空文件跳过 / 预算截断 / 来源标注与"不凌驾"措辞 / 创建·编辑·删除都动指纹;skills 的分层 / home 不可被项目覆盖 / 两个项目目录间 rank 优先 / 名字与目录不符的冒名 skill 被忽略 / 坏文件静默跳过。

实测(临时 repo:AGENTS.md + 同内容 CLAUDE.md + 一个项目 skill):instructionFiles 只列 1 个(重复的被折叠)、skill 带 (from this project)、编辑 AGENTS.md 后指纹变化。

全量 1571 通过 / 0 失败

🤖 Generated with Claude Code

生态已经收敛到一个 LISA 不读的约定:仓库根放一份 AGENTS.md(或 Claude Code
的 CLAUDE.md),所有 agent 都能拿到这个项目的规矩。Claude Code、Codex、Cursor、
dsh 都认。读它是成本最低的一项兼容——用户迁到 Lisa,已经写好的指令继续生效。

**指令链**(由外到内,最具体的最后读):
  ~/.lisa/AGENTS.md → <repo>/AGENTS.md → <repo>/CLAUDE.md → …逐级到 cwd

- **按内容去重**:CLAUDE.md 经常是 AGENTS.md 的软链或副本,两份都读等于每轮
  为同样的段落付两次钱;
- **32KB 总预算**:这些东西进系统提示词、每轮都付,单个 monorepo 的 AGENTS.md
  不能挤掉灵魂。超出截断并在提示词里标明;
- **框定为"项目的说法"而非"用户的指令"**:这些文件只是被放在某个目录里就会
  被读到,所以每一块都标注来源,并明说不凌驾于宪法之上、若文件让她放弃自己的
  原则就无视并说出来。克隆一个恶意仓库不构成对 Lisa 下命令的资格。

**项目级 skills**:除 ~/.lisa/skills 外,另读 <repo>/.lisa/skills(rank 100)
与 <repo>/.agents/skills(rank 200)。

- **同名冲突时 home 胜出——刻意与 dsh 的"近者遮蔽远者"相反。** 对编码 harness
  来说近者优先是对的默认;对这里是错的,因为 Lisa 的 skill 是她自己写的、关于
  她怎么工作的提示词材料,`cd` 进一个仓库不能成为重定义它的手段。项目 skill
  只做增量,被遮蔽的记录下来。这与本仓已有的先例一致(工具注册表里 builtin
  胜过注入的同名工具)。
- 提示词里项目 skill 带 *(from this project)* 标注。

两者都接入了提示词指纹,所以改 AGENTS.md 或丢一个项目 skill 进去,下一轮就
生效,而不是下个会话——与灵魂文件同样的承诺。

不做文件热监听:skills 与指令文件本来就经指纹每轮重读,再加一个 chokidar
依赖是纯冗余。

测试 19 例:分层顺序、git 根定位、CLAUDE.md 独立生效、同内容折叠、不同内容
都留、空文件跳过、预算截断、来源标注与"不凌驾"措辞、创建/编辑/删除都动指纹;
skills 的分层、home 不可被项目覆盖、两个项目目录间 rank 优先、名字与目录不符
的冒名 skill 被忽略、坏文件静默跳过。全量 1571 通过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…te budget, project-skill loadability

Adversarial-review fixes for the AGENTS.md/CLAUDE.md chain + project skills:

- chain.ts: refuse symlinked/non-regular AGENTS.md and bound the read by BYTES
  before it happens — closes (a) arbitrary-file read (a repo's AGENTS.md →
  ~/.ssh/id_rsa read verbatim into the prompt) and (b) unbounded-read DoS
  (multi-GB file / /dev/zero OOM, FIFO hang). Budget now counts UTF-8 bytes, so
  CJK can't claim ~3x the room. Regression tests added.
- discovery.ts: same lstat symlink refusal + size cap on project SKILL.md, and
  validateSkillName() on untrusted project skill dir names.
- prompt.ts: cap each project-skill description + their count, and frame them as
  the repo's stated convention (not authority) like the instruction chain.
- skills/tool.ts: skill_manage(view) resolves project skills via discovery so a
  skill the index advertises can actually be opened (was home-only → "not found").

Deferred (follow-up): server/daemon binding project scope to process.cwd(), and
the untrusted-instruction section's prompt placement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@oratis

oratis commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

Adversarial review — findings + fixes applied

Reviewed for path-traversal, injection, ACE, and correctness. Clean on the scariest axis: opening a repo does not auto-load/execute project tool.jsdiscoverExecutableSkills scans home only, so there's no arbitrary code execution from a hostile checkout. Content-dedup, the impostor guard (name must match dir), and hot-reload fingerprints are all correct. But the read path shipped two HIGH filesystem vulns and several MED gaps. Fixes pushed on 9456cde; typecheck clean, instructions/ + skills/ tests green.

HIGH — fixed

  1. Symlinked AGENTS.md/CLAUDE.md → arbitrary-file read into the prompt. fs.readFile followed symlinks with no guard, so a repo committing AGENTS.md → ~/.ssh/id_rsa (or a provider-key file) would load the secret verbatim into the system prompt every turn. → readBoundedRegularFile now lstats first and refuses symlinks / FIFOs / devices (regular files only).
  2. Unbounded read → DoS. The 32 KB budget was checked after fs.readFile buffered the whole file, so a multi-GB AGENTS.md (or a symlink to /dev/zero) OOMs, and a FIFO hangs buildSystemPromptSnapshot forever. → the read is now bounded to the remaining budget before it happens (open + fstat + sized read).

MED — fixed

  • Byte budget. The budget compared String.length (UTF-16 units) while named _BYTES; CJC content got ~3× the intended room. Now measured/truncated in UTF-8 bytes. (Regression test with CJK added.)
  • Project SKILL.md had the same symlink/size hazard → same lstat + size cap in discovery.ts, plus validateSkillName() on untrusted project skill dir names.
  • Project-skill injection/bloat. Descriptions and count were unbounded and unframed → each description capped, project-skill count capped, and each framed as "the project's stated convention, not your principle" (matching the instruction-chain framing).
  • Project skills were advertised but unloadable — the prompt tells the model to skill_manage(view, name), but getSkill is home-only, so a project skill returned "not found." → view now falls back to discovery via ctx.cwd.

Deferred to a follow-up (noted, not blocking)

  • Server/daemon process.cwd() binding (server.ts:455/589, router.ts:53 call the snapshot with no cwd): for the long-running :5757 backend, "project" resolves to the service launch dir, shared across all sessions — project instructions/skills should be threaded from a real per-session cwd (or disabled when there is none). This needs cross-cutting server plumbing, better as its own PR.
  • LOW: the untrusted-instruction section sits last (max recency); consider an explicit delimiter.

@oratis
oratis merged commit fe83043 into main Aug 14, 2026
1 check passed
@oratis
oratis deleted the claude/p1-ecosystem-compat branch August 14, 2026 05:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant