Ponytail hooks 能力调研
调研对象: DietrichGebert/ponytail ,本地克隆路径 D:\code\store\ponytail__DietrichGebert,调研日期 2026-08-31。
一句话结论: ponytail 的 hooks 全部是"指令注入型"生命周期钩子,它们不做任何代码检查、不拦截任何工具调用,只负责在正确的时机、把一份"懒资深工程师"规则文本(按 lite/full/ultra 强度过滤后)塞进主对话、子代理和系统提示词里,从而让 agent 少写代码、避免过度工程。
1. 仓库定位
ponytail 是一个面向约 20 种 agent 开发工具(Claude Code、Codex、GitHub Copilot CLI、OpenCode、Gemini CLI、Qoder、Hermes Agent、pi、Grok Build、Devin CLI 等)的插件。它的核心理念是一个"懒资深工程师"人格:写代码前先过一遍七级阶梯——
1. 这东西需要存在吗? → 不需要就跳过(YAGNI)
2. 代码库里已经有了吗? → 复用,不重写
3. 标准库能做吗? → 用标准库
4. 平台原生特性能覆盖吗? → 用原生特性
5. 已安装的依赖能解决吗? → 用现成依赖
6. 能一行写完吗? → 一行写完
7. 以上都不行:写能工作的最小实现官方基准测试(真实 Claude Code 会话编辑 FastAPI + React 仓库,Haiku 4.5,n=4)显示平均少写约 54% 代码(过度工程场景最高 94%)、省约 20% 成本、快约 27%,同时不牺牲任何安全护栏(信任边界校验、防数据丢失的错误处理、安全、可访问性永不简化)。
而 hooks 在整个体系里的角色,就是把这套规则"常驻"地注入到 agent 的上下文中——skills 是按需触发的,hooks 是每回合都在场的。
2. hooks 总览
Claude Code / Codex 适配层的 hook 注册表在 hooks/claude-codex-hooks.json,共 3 个事件、3 个脚本:
| hook 事件 | 执行脚本 | 触发时机 | 核心作用 |
|---|---|---|---|
SessionStart | ponytail-activate.js | 会话启动、恢复、/clear、上下文压缩(matcher: startup|resume|clear|compact) | 解析默认模式、写状态标志文件、向主对话注入规则集、一次性 statusline 安装提示 |
SubagentStart | ponytail-subagent.js | 每次通过 Task/Agent 工具派生子代理时 | 向子代理注入同一份规则集(SessionStart 上下文天然到不了子代理) |
UserPromptSubmit | ponytail-mode-tracker.js | 用户每提交一条 prompt 时 | 识别 /ponytail 系列命令切换强度、识别"stop ponytail"停用、Qoder 宿主下兼职激活 |
三个 hook 都是 type: command 的 Node.js 脚本,超时 5 秒,全部遵循"best-effort、绝不阻塞会话"的契约——任何一步失败都静默吞掉(源码里大量 catch (e) {} 注释写着 silent fail)。
3. 逐个 hook 详解
3.1. SessionStart:ponytail-activate.js(激活钩子)
这是最重要的一个 hook,每次会话冷启动、恢复、清空、压缩后都会重新跑一遍,流程如下:
- 解析默认模式:优先级为环境变量
PONYTAIL_DEFAULT_MODE→ 配置文件~/.config/ponytail/config.json(Windows 为%APPDATA%\ponytail\config.json)的defaultMode字段 → 兜底full。合法值只有off/lite/full/ultra四个运行时级别(review是会话级模式,不允许做默认值)。 off短路退出:默认模式是 off 时直接清掉标志文件、不注入任何内容。- 写状态标志文件:把当前模式写入
.ponytail-active标志文件。Claude Code 下位于~/.claude(可用CLAUDE_CONFIG_DIR覆盖);Codex 下写PLUGIN_DATA;Copilot 下写COPILOT_PLUGIN_DATA;Qoder 下写~/.qoder。这个文件是全家的"单一状态源"——statusline 徽章、mode-tracker、subagent 注入器都读它。 - 注入规则集:调用
ponytail-instructions.js生成规则文本,作为 SessionStart 的 additionalContext 输出给主对话。 - statusline 安装提示(仅一次):检测
settings.json里有没有statusLine配置;没有就写一个.ponytail-statusline-nudged标志(保证提示最多出现一次,避免每会话骚扰),然后在注入内容末尾追加一段"STATUSLINE SETUP NEEDED",引导 agent 主动帮用户配置状态栏徽章。这里还有个安全细节:安装路径必须通过isShellSafe白名单校验(只允许普通路径字符),含 shell 元字符的路径不会拼进命令片段,改走手工配置提示,防止注入。
3.2. SubagentStart:ponytail-subagent.js(子代理注入钩子)
存在的理由写在脚本头注释里:SessionStart 注入的上下文只属于父线程,永远到不了子代理,不补这个 hook 的话每个 Task 派生的 agent 都是"ponytail 失忆"状态(issue #252)。
逻辑非常克制:
- 读
.ponytail-active标志文件,没有或者 off → 立即退出,什么都不注入。 - 可选作用域过滤(issue #506):环境变量
PONYTAIL_SUBAGENT_MATCHER是一个不锚定、大小写不敏感的正则,与子代理的agent_type匹配。比如explore|general匹配两类、^general$精确匹配。fail-open 设计:正则非法、平台没上报 agent_type、stdin 出错、超时——全部回退为"照常注入",宁可多注入也不静默丢规则。 - 输出格式:Claude Code 原生宿主下 SubagentStart 事件必须用
hookSpecificOutput.additionalContext的 JSON 形态输出,裸 stdout 会被丢弃。
3.3. UserPromptSubmit:ponytail-mode-tracker.js(模式跟踪钩子)
每条用户 prompt 都会触发,从 stdin 读取 JSON({ prompt: "..." }),做三件事:
- 识别
/ponytail命令族:/ponytail lite|full|ultra|off→ 会话级切换模式,写标志文件,输出确认"PONYTAIL MODE CHANGED — level: X";/ponytail default <mode>→ 持久化默认模式到配置文件(唯一会写配置的路径,重启后仍生效);/ponytail(无参数)→ 只报告当前级别;/ponytail-review→ 进入 review 模式。- 命令前缀同时兼容
/、@(Codex)、$三种写法。
- 识别自然语言停用:整条消息恰好是
stop ponytail或normal mode(忽略大小写和尾部标点)才停用。必须是完整独立命令——早期版本在消息任意位置匹配,导致用户说"加一个 normal mode 开关"时 ponytail 被误关。 - Qoder 宿主兼职激活:Qoder 没有 SessionStart 事件,所以这个 hook 在 Qoder 下身兼两职——首条 prompt 时初始化默认模式,之后每条 prompt 都注入完整规则集。
这个脚本还有一个精彩的防御性细节(issue #443):Windows 上 Claude Code 通过 PowerShell if {} 包装器执行 hook,可能吞掉管道输入导致 stdin 的 end 事件永不触发、hook 挂死、冻结整个会话。解法是 stdin error 回调 + 1 秒 setTimeout(...).unref() 兜底,超时后处理已收到的数据并退出。所有生命周期 hook 都遵守这个"永不阻塞"契约。
4. 注入的内容是什么
三个 hook 注入的都是同一份文本,由 ponytail-instructions.js 统一生成:
- 单一事实源:直接读
skills/ponytail/SKILL.md,去掉 frontmatter,然后按当前强度做行级过滤——强度对照表(| **lite** | ... |)只保留当前模式那一行,工作示例(- lite: "...")也只保留当前模式的。普通规则行原样保留。这样 lite/full/ultra 三档共享一份 skill 文档,不会漂移。 - 内嵌兜底:skill 文件读不到时,退回脚本里内嵌的一份完整规则文本(含七级阶梯、bug 修复要修根因、
ponytail:注释标记刻意简化、输出格式"代码优先、解释最多三行"、永不简化的安全清单等)。 review模式特殊:它不注入阶梯规则,只声明"行为由 /ponytail-review skill 定义",走 skill 按需触发路径。
5. 状态栏脚本(配套但非 hook)
ponytail-statusline.sh / .ps1 是 Claude Code statusLine 命令,每次状态栏刷新时执行:读 .ponytail-active 标志文件,off/文件不存在就输出空,否则输出绿色 [PONYTAIL] 或 [PONYTAIL:LITE] 等徽章(ultra 用琥珀色突出)。它是 hook 写的标志文件的消费方,本身不参与生命周期。
6. 其它宿主的 hook 等价物
ponytail 的多宿主适配不是复制代码,而是同一套 hooks/*.js 共享模块 + 每个宿主一层薄适配:
| 宿主 | hook 机制 | 触发时机 | 与 Claude Code 版的差异 |
|---|---|---|---|
| GitHub Copilot CLI | copilot-hooks.json:sessionStart + userPromptSubmitted | 会话启动、每条 prompt | 无 SubagentStart 事件,输出用 additionalContext JSON,仅在 SessionStart 生效 |
| Qoder | qoder-hooks.json 模板:UserPromptSubmit + PreToolUse(matcher task|Task) | 每条 prompt、每次 Task 工具调用 | 用 PreToolUse 模拟子代理注入;UserPromptSubmit 兼职激活(见 3.3) |
| OpenCode | 插件 ponytail.mjs:experimental.chat.system.transform + command.execute.before | 每次 LLM 调用前改写系统提示词;斜杠命令执行前持久化模式 | 每回合把规则追加进 system prompt,模式标志存在 ~/.config/opencode/.ponytail-active |
| pi agent | pi-extension/index.js:pi.on("session_start")、before_agent_start、input、agent_start/agent_end | 会话开始、每个 agent 回合开始前、用户输入、agent 起止 | before_agent_start 直接改写 systemPrompt 注入;模式从会话 entry 历史恢复 |
| Hermes Agent | plugin.yaml 声明 pre_llm_call + pre_gateway_dispatch | 每次 LLM 调用前、网关分发前 | 共享网关下建议用访问控制限制 /ponytail 命令 |
| Gemini CLI / Antigravity | 无 hooks | — | 故意不提供 hooks/hooks.json:Gemini 会自动加载该路径,而其生命周期事件名不兼容;改为 AGENTS.md 常驻上下文 + commands |
| Grok Build | 无 hooks | — | Grok 的 SessionStart 输出无法注入指令,改走 skills 自动触发 |
7. 设计模式与借鉴价值
调研下来,这套 hooks 体系有几个非常值得抄的设计:
- 指令注入,绝不拦截:Claude Code/Codex 适配层没有任何 PreToolUse 拦截、不 block 工具调用、不改写文件。hook 只做"在正确时机把规则放进上下文"这一件事,行为改变完全靠模型遵循注入的指令。副作用风险接近零,卸载即无痕。
- 单一状态源:一个
.ponytail-active标志文件串起激活、跟踪、子代理注入、statusline 四个消费方,跨进程共享状态,简单到不会坏。 - 永不阻塞契约:所有 hook 5 秒超时、静默失败、stdin 超时兜底(专门为 Windows PowerShell 吞管道的 bug 打的补丁,issue #443)。hook 崩了最坏结果只是"这一回合没注入规则",绝不冻结会话。
- fail-open 的作用域过滤:子代理 matcher 一切不确定情况都回退为注入,宁可冗余注入也不静默丢规则。
- 一次性打扰预算:statusline 安装提示用标志文件保证用户最多见到一次,把"有用提示"和"骚扰"的边界量化了。
- 一套核心多宿主薄适配:宿主探测(环境变量特征)+ 输出格式适配(每宿主一种 JSON 形态)集中在
ponytail-runtime.js,规则生成集中在ponytail-instructions.js,新增宿主只需写一层注册表适配。 - 文本即产品:注入的规则文本按强度做行级过滤、单一事实源(skill 文档)、内置兜底副本,保证各强度各宿主持久一致(仓库还配了
scripts/check-rule-copies.js做多副本一致性校验)。