4.0 KiB
Agent Note: 在 bash seam 上支持 stdin 与额外 env
Status: implemented
English | 中文
问题
钩子子系统以 Claude Code 和 Codex 的方式运行外部钩子命令:钩子是一条 shell 命令,通过 stdin 上的 JSON 接收事件载荷,并从若干环境变量(CLAUDE_PROJECT_DIR、CLAUDE_PLUGIN_ROOT、PLUGIN_ROOT……)读取上下文。harness 已经在 ctx.bash 能力 seam 后面有一个完善的命令执行器(dsh-bash → dsh-bash-local),具备进程组终止、输出截断/溢出处理和凭证擦除功能。复用它来执行钩子意味着钩子桥接层无需重新实现子进程管道——但该 seam 此前无法写入 stdin 或设置额外 env。本次变更添加这两个输入。
stdin 和 env 不构成新的模型能力,因为普通 shell 语法已经能提供两者。环境凭证由 dsh-bash-local 的子环境擦除机制保护,而非靠隐藏这些 Service Definition 字段;模型工具参数是静态 JSON,不会展开 shell 变量。因此这些字段服务于受信的进程内调用方(如钩子桥接层),它们需要传递结构化输入和 CLAUDE_* 变量,而不必将其嵌入模型可见的 shell 文本。环境变量规则见 defensive-patterns.md。
决策
在 BashExecRequest(模型/插件侧请求)和 BashExecSpec(run/start 所作用的已解析 spec)上同时添加 stdin?: string 与 env?: Record<string, string>,并在 dsh-bash-local 中贯穿它们:resolve() 原样传递,run()/start() 将其传给 runBash,后者把字节写入子进程的 stdin 并合并额外 env。
三个有意为之的选择:
-
模型侧工具不暴露
stdin和env。 Shell 语法已覆盖这些需求,重复参数只会增加接口面而不带来权限隔离。工具仅从声明的模型参数、signal 和 owner 构建请求;受信的进程内调用方可以直接设置请求字段。harness 自有变量使用托管环境决策规定的独立dshEnv通道,因此普通env无法替换它们。 -
env在凭证擦除之后合并,因此调用方显式设置的条目即使具有凭证形态的名称也会胜出。 后续的托管命名空间决策负责管理DSH_*:这类环境条目会被移除,受信的dshEnv最后合并,因此普通env条目永远无法顶掉托管值。完整顺序为scrub(process.env, including DSH_*)→ENV_OVERRIDES→ 普通env→dshEnv。 -
stdin/env在已解析 spec 上是 required-absent-OK(普通 optional),而非像owner那样 required-but-nullable。owner之所以是 required-but-nullable,是因为静默缺失的 owner 会产生一个无主、跨会话可读的任务——一个安全隐患,显式的undefined可以防范。stdin/env没有这种风险:缺失意味着「无 stdin / 无额外 env」,这是安全的常规情况(所有模型驱动的调用都如此)。因此它们保持普通 optional,与signal一致。
dsh-bash-local 仅在有字节需要写入时才创建 stdin 管道;否则 fd 0 仍为 /dev/null,保持先前行为。它写入字节后关闭管道。子进程未读取即退出时产生的 EPIPE 被忽略,因为命令退出码和输出决定结果。
曾考虑的替代方案
可配置的环境秘密擦除。 否决,属于推测性需求。受信调用方可以在擦除之后显式提供所需值,无需削弱默认的环境保护。
后果
钩子桥接层通过既有的 bash seam 传递 JSON 载荷和钩子特定变量,保留其进程组终止、截断和溢出行为。模型接口面不变,bash 工具仍是模型调用请求构建的唯一所有者。相关词汇定义见 bash 数据结构参考。