Follow symlinked instruction files
跟随符号链接指向的指令文件
The [agent-instructions plugin](2026-06-24-workspace-context.md) probed each instruction candidate with `ctx.fs.lstat` before resolving, rejecting any final-component symlink so a repository-owned link could not point instruction loading at content outside the workspace. That no-follow invariant blocked a deliberate, supported setup: a user who symlinks `$DSH_HOME/AGENTS.md` — or a project `AGENTS.md` — to a canonica
English
Problem
The agent-instructions plugin probed each instruction candidate with ctx.fs.lstat before resolving, rejecting any final-component symlink so a repository-owned link could not point instruction loading at content outside the workspace. That no-follow invariant blocked a deliberate, supported setup: a user who symlinks $DSH_HOME/AGENTS.md — or a project AGENTS.md — to a canonical instruction file kept elsewhere, sharing one house-style file across tools and homes, saw the link silently ignored. It also forced content dedup to treat the ubiquitous CLAUDE.md → AGENTS.md mirror as a special skipped case rather than an ordinary duplicate. The repository owner asked to follow symlinked instruction files unconditionally across every scope, accepting the residual trust-boundary risk recorded below.
Decision
Instruction discovery no longer inspects the final component with lstat. Every candidate — the user-global $DSH_HOME/AGENTS.md, each base candidate, and each local-overlay candidate — is resolved and its resolved target is stat-ed, at baseline composition and at each tools/post-execute reconciliation alike. A symlink whose target is a regular file loads that target's content; a resolved non-file target (including a link to a directory) is a confirmed absence that removes the scope like a missing file; a resolve or stat exception is classified as temporarily unavailable and never removes an already-loaded scope. nodeStatFile calls stat (host path) and fsStatFile calls resolve then stat (provider path); neither calls lstat.
A followed symlink is an ordinary file for every downstream step. It participates in per-directory content dedup (load-all + dedup note), so a CLAUDE.md that symlinks its sibling AGENTS.md now resolves to identical content and collapses like any byte-identical real duplicate instead of being skipped as a special case.
Trust boundary and residual risk
Following repository-owned links crosses the plugin's trust boundary: a cloned, untrusted repository can carry an AGENTS.md whose symlink target is any file the process can read, surfacing off-tree content as workspace guidance. That content enters only as a lower-authority user-role prefix framed by the system-reminder pattern; it never overrides system, developer, or direct user instructions, and it is treated as data, not authority. The mitigating boundary is the filesystem layer, not this plugin: confine ctx.fs with the dsh-fs-observation-policy gate or an OS sandbox (cross-family fs sandbox) when a deployment loads untrusted repositories. This is an explicit, owner-accepted trade-off, not an oversight.
Alternatives considered
Keep the lstat no-follow invariant. Rejected by the repository owner: it blocks the supported symlink-to-canonical-file setup and forces the symlink-mirror case to be a skipped special case rather than a plain duplicate. The read-authority boundary it approximated belongs in the filesystem policy and sandbox layer, which contains the same risk more precisely.
Follow only the user-global $DSH_HOME candidate and keep no-follow for project files. Rejected: the owner asked for uniform behavior across every scope, and a split rule is harder to reason about than one consistently applied policy plus a documented boundary. A project the user chose to open is not meaningfully more trusted than the user's own home.
Follow symlinks but reject targets that resolve outside the project root. Rejected: it reintroduces a partial trust boundary in the wrong layer — path geometry rather than read authority — breaks the legitimate $DSH_HOME-to-elsewhere case, and duplicates containment the filesystem policy gate already owns.
Consequences
A symlinked instruction file is now loaded and rendered like its target, enabling shared canonical instruction files across tools and homes, and the CLAUDE.md → AGENTS.md mirror deduplicates through content instead of being skipped. The plugin no longer depends on ctx.fs.lstat for instruction loading; a resolved non-file is a confirmed absence and only a provider exception is temporarily unavailable. The trust boundary moves out of this plugin into the filesystem policy and sandbox layers, which must confine ctx.fs when a deployment loads untrusted repositories. The agent-instructions note and the package README carry the same follow behavior and residual-risk statement.
中文
问题
agent-instructions 插件在解析前用 ctx.fs.lstat 探测每个指令候选,拒绝任何末段的符号链接,从而使仓库自有的链接无法把指令加载指向工作区之外的内容。这条「不跟随」不变式挡住了一个有意为之、且受支持的配置:用户若把 $DSH_HOME/AGENTS.md(或某个项目的 AGENTS.md)符号链接到别处保存的一个规范指令文件,以便在多个工具与多个 home 之间共享同一份规范文件,就会看到该链接被悄悄忽略。它还迫使内容去重把无处不在的 CLAUDE.md → AGENTS.md 镜像当作一个被跳过的特例来处理,而非一个普通的重复文件。仓库所有者要求在每个 scope 上无条件跟随符号链接指向的指令文件,并接受下文记录的残余信任边界风险。
决策
指令发现不再用 lstat 检查末段。每个候选(用户全局的 $DSH_HOME/AGENTS.md、每个基础候选,以及每个本地覆盖候选)都会被解析,并对其解析后的目标做 stat,基线组合时与每一轮 tools/post-execute 协调时一视同仁。一个目标为常规文件的符号链接会加载该目标的内容;一个解析后的非文件目标(包括指向目录的链接)是被确认的缺失,会像缺失文件一样移除该 scope;一个 resolve 或 stat 异常被归类为暂时不可用,且从不移除已加载的 scope。nodeStatFile 调用 stat(宿主路径),fsStatFile 先 resolve 再 stat(提供方路径);两者都不调用 lstat。
一个被跟随的符号链接对下游每一步都是普通文件。它参与按目录的内容去重(加载全部并去重说明),因此一个符号链接指向其同级 AGENTS.md 的 CLAUDE.md 现在会解析到相同内容,并像任何逐字节相同的真实副本一样被合并,而不再作为特例被跳过。
信任边界与残余风险
跟随仓库自有的链接会越过插件的信任边界:一个被克隆的、不受信任的仓库可以携带一个 AGENTS.md,其符号链接目标是该进程能读取的任意文件,从而把树外内容作为工作区指导暴露出来。该内容仅作为一条被 system-reminder 模式框定、权威性较低的 user 角色前缀进入;它绝不覆盖 system、developer 或用户的直接指令,并被当作数据而非权威依据。起缓解作用的边界在文件系统层,而非本插件:在部署加载不受信任的仓库时,用 dsh-fs-observation-policy 防护机制或一个操作系统沙箱(跨家族 fs 沙箱)约束 ctx.fs。这是一个明确的、由所有者接受的取舍,而非疏漏。
备选方案
保留 lstat 的「不跟随」不变式。 被仓库所有者否决:它挡住了受支持的「符号链接到规范文件」配置,并迫使符号链接镜像场景成为一个被跳过的特例而非普通重复。它所近似的读取权限边界属于文件系统策略与沙箱层,那里能更精确地遏制同一风险。
只跟随用户全局的 $DSH_HOME 候选,项目文件保持不跟随。 否决:所有者要求在每个 scope 上行为一致,而一条分裂的规则比一条一致应用的策略加一条有文档记录的边界更难推理。用户选择打开的项目并不比用户自己的 home 更值得信任。
跟随符号链接,但拒绝解析到项目根之外的目标。 否决:这会在错误的层(路径几何而非读取权限)重新引入一条局部的信任边界,破坏合理的「$DSH_HOME 指向别处」场景,并重复文件系统策略门已经拥有的遏制。
后果
一个符号链接指向的指令文件现在会像其目标一样被加载和渲染,从而支持在多个工具与多个 home 之间共享规范指令文件,而 CLAUDE.md → AGENTS.md 镜像会通过内容去重而非被跳过。指令加载不再依赖 ctx.fs.lstat;一个解析后的非文件是被确认的缺失,只有提供方异常才是暂时不可用。信任边界从本插件移出,进入文件系统策略与沙箱层。当部署加载不受信任的仓库时,它们必须约束 ctx.fs。agent-instructions note 与包 README 承载相同的跟随行为与残余风险声明。