
一、介绍
想象一下:你的团队里有个扎着马尾辫、戴着椭圆眼镜的老兵。他比你的版本控制系统还资深,你给他看 50 行代码,他什么也不说,用一行替换了它们——而且它就能工作。
Ponytail 就是把这个人物塞进你的 AI 编程代理里。
项目地址:https://github.com/DietrichGebert/ponytail
作者:Dietrich Gebert,当前版本 4.8.4,MIT 许可证。
它不是一个模型,不是独立工具,也不是框架。它是一套 规则集 + 插件,跨 16+ 个 AI 代理平台工作,让 AI 在写代码之前先问自己:这玩意儿真的需要存在吗?
核心数据(基准测试)
| 代码量 | -54%(最高 -94%) |
| Token 消耗 | -22% |
| 成本 | -20% |
| 速度 | -27% |
| 安全性 | 100%(对比"写一行代码"提示词的 95%) |
测试方法:真实 Claude Code 无头会话,编辑真实开源仓库(tiangolo/full-stack-fastapi-template),12 个功能任务,Haiku 4.5,n=4,git diff 评分。
二、原理:7 级决策阶梯
Ponytail 的核心是一个 决策阶梯(The Ladder)——代理在写任何代码之前,会在第一个适用的阶梯停下来:
阶梯 1: 这东西需要存在吗? → 不需要:跳过(YAGNI)
阶梯 2: 代码库里已经有了? → 复用它,不要重写
阶梯 3: 标准库能做? → 用它
阶梯 4: 原生平台功能? → 用它
阶梯 5: 已安装的依赖? → 用它
阶梯 6: 一行代码能搞定? → 一行代码
阶梯 7: 只有以上都不行:才写最少能工作的代码
关键原则
阶梯在理解问题之后运行,而不是取代理解过程。 先阅读代码、追踪实际流程,再选择阶梯。对解决方案懒惰,对阅读理解从不懒惰。
Bug 修复 = 根因,不是症状。 一个报告只命名了一个症状。修复之前 grep 每一个调用者——共享函数中的一次修复,diff 比每个调用点各加一个 guard 还小,而且只修 ticket 路径会留下兄弟调用者仍然坏掉。
安全底线不可逾越。 信任边界验证、数据丢失处理、安全性和可访问性永远不会被削减。
三、源码关系图
Ponytail 的源码结构清晰,分为四层:
AGENTS.md (核心规则)
↓ 衍生
Skills 系统 (5+1 个技能定义)
↓ 依赖
Hooks 系统 (3 个生命周期钩子)
↓ 依赖
共享模块 (指令构建器 + 配置解析 + 运行时)
↓ 适配
平台适配层 (Claude Code / Pi / OpenCode + 16+ 规则文件)
上面已用可视化图展示了这四层之间的关系。下面逐层拆解。
四、源码拆解
4.1 核心规则层:AGENTS.md
这是整个项目的心脏——一个 ~200 行的 Markdown 文件,定义了"懒惰资深开发者"的行为规范。
文件路径:AGENTS.md
核心内容拆解:
1. Does this need to be built at all? (YAGNI)
2. Does it already exist in this codebase? Reuse it.
3. Does the standard library already do this? Use it.
4. Does a native platform feature cover it? Use it.
5. Does an already-installed dependency solve it? Use it.
6. Can this be one line? Make it one line.
7. Only then: write the minimum code that works.
规则要点:
- 没有未被请求的抽象(接口只有一个实现?不需要;工厂只产一个产品?不需要)
- 没有没人要的样板代码
- 删除优先于添加,无聊优于聪明
- 最短有效 diff 获胜——但前提是你理解了问题。最小改动打错地方不是偷懒,是第二个 bug
- 故意的简化用 ponytail: 注释标记,有已知天花板时标注天花板和升级路径
不可偷懒的区域:
- 理解问题(先完整阅读再选阶梯)
- 信任边界的输入验证
- 防止数据丢失的错误处理
- 安全措施
- 可访问性基础
- 真实硬件的校准需求(时钟会漂移,传感器会读偏)
- 非平凡逻辑必须留一个可运行的检查
4.2 Skills 系统
5 个独立技能 + 1 个主技能,每个都是一个小而精的 SKILL.md:
| ponytail | 懒惰模式本身——7 级阶梯强制执行 | /ponytail |
| ponytail-review | 过度工程审查——针对 diff,一行一个发现 | /ponytail-review |
| ponytail-audit | 全仓库过度工程审计——ranked 排序 | /ponytail-audit |
| ponytail-debt | 收集 ponytail: 注释到欠账本 | /ponytail-debt |
| ponytail-gain | 显示基准测试影响分数板 | /ponytail-gain |
| ponytail-help | 快速参考卡片 | /ponytail-help |
主 Skill:skills/ponytail/SKILL.md
这个文件约 100 行,包含完整的 SKILL.md frontmatter + 规则正文。核心特性:
强度等级:
| lite | 构建请求的东西,但在一行中指出更懒的替代方案。用户选择。 |
| full | 阶梯强制执行。标准库和原生优先。最短 diff,最短解释。默认。 |
| ultra | YAGNI 极端主义者。删除优先于添加。交付一行代码并在同一口气中质疑其余需求。 |
缓存示例(“给这些 API 响应加个缓存”):
- lite: “Done, cache added. FYI: functools.lru_cache covers this in one line.”
- full: “@lru_cache(maxsize=1000) on the fetch function. Skipped custom cache class.”
- ultra: “No cache until a profiler says so. When it does: @lru_cache. A hand-rolled TTL cache class is a bug farm.”
输出格式:
[code] → skipped: [X], add when [Y].
代码在前,最多三行说明:跳过了什么,什么时候该加回来。如果解释比代码还长,删掉解释。
ponytail-review:5 个标签
审查 diff 中的过度工程,5 种标签:
| delete: | 死代码、未使用灵活性、投机功能 | L52-71: delete: retry wrapper around an idempotent local call. Nothing replaces it. |
| stdlib: | 手写了标准库已有的东西 | L12-38: stdlib: 27-line validator class. "@" in email, 1 line. |
| native: | 依赖或代码做了平台已做的事 | L4: native: moment.js imported for one format call. Intl.DateTimeFormat, 0 deps. |
| yagni: | 抽象只有一个实现 | repo.py:L88: yagni: AbstractRepository with one implementation. Inline it. |
| shrink: | 同样逻辑可以更短 | L30-44: shrink: manual loop builds dict. dict(zip(keys, values)), 1 line. |
评分只有一个指标:net: -<N> lines possible.
如果没什么可砍的:Lean already. Ship. 停。
ponytail-audit
和 review 同一套标签,但是扫描整个仓库(而非 diff),按最大可砍量排序输出。
ponytail-debt
每个 ponytail: 注释是一行欠账。这个 skill grep 整个仓库,收集所有标记:
grep -rnE '(#|//) ?ponytail:' .
输出格式:<file>:<line>, <what was simplified>. ceiling: <limit>. upgrade: <trigger>.
关键:没有升级路径的标记会被标为 no-trigger——那些是悄悄腐烂的。
ponytail-gain
显示基准测试分数板,纯 ASCII 条形图:
Lines of code no-skill ████████████████████ 100%
ponytail ██▌················· 6–20% ▼ 80–94%
Cost no-skill ████████████████████ 100%
ponytail █████▌·············· 23–53% ▼ 47–77%
Speed ponytail ▸ 3–6× faster
诚实边界:这些是基准中位数,不是当前仓库。绝不打印 per-repo 节省数字(未写的版本从未存在,没有真实基线可减)。
4.3 Hooks 系统
三个生命周期钩子,构成了 Ponytail 在 Claude Code/Codex/Copilot 中"始终在线"的机制:
hooks/ponytail-activate.js — SessionStart
每次会话启动时执行:
const mode = getDefaultMode();
if (mode === 'off') {
clearMode();
writeHookOutput('SessionStart', 'off', hookOutput);
process.exit(0);
}
setMode(mode);
let output = getPonytailInstructions(mode);
// … statusline 检测 …
writeHookOutput('SessionStart', mode, output);
关键设计:off 模式直接跳过——不写 flag,不注入规则,不阻塞钩子。
hooks/ponytail-subagent.js — SubagentStart
SessionStart 的上下文只在父线程,永远不到达子代理(issue #252)。所以这个钩子把同样的规则集注入到每个 Task-spawned 子代理中:
const mode = readMode();
if (!mode || mode === 'off') process.exit(0);
writeHookOutput('SubagentStart', mode, getPonytailInstructions(mode));
hooks/ponytail-mode-tracker.js — UserPromptSubmit
拦截用户输入中的 /ponytail 命令,切换模式:
if (/^[/@$]ponytail/.test(prompt)) {
// 解析命令和参数
// lite/full/ultra/off → setMode()
// review → setMode('review')
}
// 检测 "stop ponytail" / "normal mode" → clearMode()
踩坑记录(issue #443):Windows 上 Claude Code 通过 PowerShell if {} wrapper 运行此钩子,可能吞掉管道传入的 prompt JSON,导致 stdin end 事件永远不触发,钩子阻塞整个会话。解决方案:1 秒超时 + unref()。
hooks/claude-codex-hooks.json — 钩子注册配置
{
"hooks": {
"SessionStart": [{ "matcher": "startup|resume|clear|compact", "hooks": […] }],
"SubagentStart": [{ "hooks": […] }],
"UserPromptSubmit": [{ "hooks": […] }]
}
}
三个事件 × 两个平台命令(Unix exec node / Windows PowerShell if {} wrapper)。
4.4 共享模块层
hooks/ponytail-config.js — 配置解析
模式解析优先级:
- $XDG_CONFIG_HOME/ponytail/config.json(任何平台,如果设置)
- ~/.config/ponytail/config.json(macOS/Linux)
- %APPDATA%\\ponytail\\config.json(Windows)
关键函数:
const VALID_MODES = ['off', 'lite', 'full', 'ultra', 'review'];
const RUNTIME_MODES = ['off', 'lite', 'full', 'ultra'];
function normalizeMode(mode) // 返回 runtime 模式或 null
function normalizeConfigMode(mode) // 返回 config 模式(含 review)或 null
function normalizePersistedMode(mode) // 合并两者
function isDeactivationCommand(text) // 匹配 "stop ponytail" / "normal mode"
function isShellSafe(p) // 白名单校验路径字符,防止注入
isShellSafe 的设计哲学:只允许普通路径字符 [A-Za-z0-9 _.\\-:/\\\\~],有恶意字符的路径回退到手动设置。比每 shell 转义所有元字符更安全。
isDeactivationCommand 的踩坑:最初匹配短语出现在消息任何位置就会关掉,导致 “add a normal mode toggle” 这种请求意外触发。修复:只匹配整个消息作为命令(忽略大小写和尾部标点)。
hooks/ponytail-instructions.js — 指令构建器
这是所有平台共享的 单一真相源:
function getPonytailInstructions(mode) {
// review 模式 → 引用 /ponytail-review skill
// 其他模式 → 读取 SKILL.md,按当前强度过滤
try {
return 'PONYTAIL MODE ACTIVE — level: ' + effectiveMode + '\\n\\n' +
filterSkillBodyForMode(fs.readFileSync(SKILL_PATH, 'utf8'), effectiveMode);
} catch (e) {
return getFallbackInstructions(effectiveMode); // 内联硬编码备份
}
}
filterSkillBodyForMode:根据当前模式,只保留匹配的强度表行和示例。例如 full 模式只看 full 行,lite 只看 lite 行。其他规则行(不带模式标签的)全部保留。
getFallbackInstructions:如果 SKILL.md 文件不可读(npm 打包时可能不包含),用硬编码的完整规则文本作为备份——确保在任何环境下都能工作。
hooks/ponytail-runtime.js — 运行时抽象
const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA);
const isCodex = Boolean(process.env.PLUGIN_DATA);
function setMode(mode) // 写 .ponytail-active flag 文件
function clearMode() // 删除 flag 文件
function readMode() // 读 flag 文件,null = off
function writeHookOutput(event, mode, context)
writeHookOutput 的三种输出格式:
- Copilot:SessionStart 用 additionalContext,其他事件忽略输出
- Codex:JSON { systemMessage: "PONYTAIL:FULL" } + hookSpecificOutput
- 原生 Claude:SessionStart 用 raw stdout;SubagentStart 需要 JSON hookSpecificOutput
这三种格式的差异是真实的平台 API 限制,不是设计偏好。
4.5 平台适配层
.opencode/plugins/ponytail.mjs — OpenCode 插件
ES Module,通过 createRequire 桥接到 CommonJS 共享模块:
export default async ({ client } = {}) => {
return {
config: async (config) => {
// 注册 slash commands + skills 目录
},
'experimental.chat.system.transform': async (_input, output) => {
const mode = readMode();
if (mode === 'off') return;
output.system.push(getPonytailInstructions(mode));
},
'command.execute.before': async (input) => {
// 持久化 /ponytail <level> 切换
},
};
};
设计选择:模式切换从下一条消息生效(不是当前消息),因为 transform 读的是 command 写的 flag。如果同轮切换有需要,改用同步存储。
pi-extension/index.js — Pi Agent 扩展
最完整的平台适配器,约 120 行,注册 6 个命令 + 状态栏 + 会话事件:
pi.registerCommand("ponytail", { … });
pi.registerCommand("ponytail-review", { … });
pi.registerCommand("ponytail-audit", { … });
pi.registerCommand("ponytail-gain", { … });
pi.registerCommand("ponytail-debt", { … });
pi.registerCommand("ponytail-help", { … });
pi.on("session_start", async (_event, ctx) => { … });
pi.on("agent_start", async (_event, ctx) => { isActive = true; });
pi.on("agent_end", async (_event, ctx) => { isActive = false; });
pi.on("before_agent_start", async (event) => {
return { systemPrompt: `${event.systemPrompt}\\n\\n${getPonytailInstructions(currentMode)}` };
});
状态栏:显示 ● 🐴 ponytail: ⚡ FULL,用主题色区分活跃/非活跃状态。
会话恢复:从 sessionManager.getBranch() 读取历史 ponytail-mode entries,恢复上次模式。
规则文件层
对于不支持插件的平台(Cursor、Windsurf、Cline 等),Ponytail 提供了规则文件副本:
| Cursor | .cursor/rules/ |
| Windsurf | .windsurf/rules/ |
| Cline | .clinerules/ |
| GitHub Copilot (editor) | .github/copilot-instructions.md |
| CodeWhale | 项目根 AGENTS.md(零配置) |
| Aider | AGENTS.md |
| Kiro | .kiro/steering/ |
| Zed | AGENTS.md |
同步机制:修改紧凑规则文本后,必须运行 node scripts/check-rule-copies.js 保持所有副本同步。这是项目维护的纪律——绝不允许副本漂移。
五、安装实践
5.1 Claude Code 安装(推荐路径)
# 安装
/plugin marketplace add DietrichGebert/ponytail
# 启用
/plugin install ponytail@ponytail
# 验证
/ponytail # 应返回当前模式
/ponytail-help # 应显示参考卡片
5.2 npm 全局安装
npm install -g @dietrichgebert/ponytail
5.3 OpenCode 安装
在 opencode.json 中添加:
{ "plugin": ["@dietrichgebert/ponytail"] }
5.4 Pi Agent 安装
pi install git:github.com/DietrichGebert/ponytail
5.5 规则文件安装(Cursor / Windsurf / Cline)
从仓库复制对应的规则文件到项目目录:
- Cursor: .cursor/rules/
- Windsurf: .windsurf/rules/
- Cline: .clinerules/
5.6 配置默认模式
环境变量(最高优先级):
export PONYTAIL_DEFAULT_MODE=ultra
配置文件 (~/.config/ponytail/config.json,Windows: %APPDATA%\\ponytail\\config.json):
{ "defaultMode": "lite" }
设置为 "off" 可禁用自动激活,手动 /ponytail 启用。
解析顺序:环境变量 > 配置文件 > full。
六、踩坑经验
6.1 Windows PowerShell stdin 阻塞(issue #443)
问题:Windows 上 Claude Code 通过 PowerShell if {} wrapper 运行 UserPromptSubmit 钩子,可能吞掉管道传入的 prompt JSON,导致 stdin end 事件永远不触发,钩子阻塞整个会话。
修复:添加 1 秒超时 + unref():
process.stdin.on('data', chunk => { input += chunk; });
process.stdin.on('end', finish);
process.stdin.on('error', () => { finish(); process.exit(0); });
setTimeout(() => { finish(); process.exit(0); }, 1000).unref();
教训:钩子的合约是 best-effort、never-block。任何 stdin 处理必须有超时兜底。
6.2 “normal mode” 误触发关闭(历史 bug)
问题:最初 isDeactivationCommand 匹配短语出现在消息任何位置就会关掉 ponytail。用户说 “add a normal mode toggle” 时意外触发 deactivation。
修复:只匹配整个消息作为命令,忽略大小写和尾部标点:
function isDeactivationCommand(text) {
const t = String(text || '').trim().toLowerCase().replace(/[.!?\\s]+$/, '');
return t === 'stop ponytail' || t === 'normal mode';
}
教训:语义匹配的粒度很重要。一个短语可能出现在完全不同的语义上下文中。
6.3 SubagentStart 钩子缺失(issue #252)
问题:SessionStart 的上下文只在父线程,永远不到达 Task-spawned 子代理。子代理在 ponytail-unaware 状态下运行,产生过度工程代码。
修复:添加 SubagentStart 钩子,把同样的规则集注入每个子代理。
教训:多代理架构中,每个执行线程都需要独立的规则注入。隐式继承不可靠。
6.4 UTF-8 BOM 导致 JSON.parse 失败(Windows)
问题:Windows 上某些编辑器保存 JSON 时前置 UTF-8 BOM (\\uFEFF),导致 JSON.parse 报错。
修复:读取时 strip BOM:
const raw = fs.readFileSync(settingsPath, 'utf8').replace(/^\\uFEFF/, '');
const settings = JSON.parse(raw);
教训:跨平台文件读写,永远 strip BOM 后再解析 JSON。
6.5 Em Dash 污染(commit 88431de, PR #179, #180)
问题:项目刻意用普通标点(逗号)代替 em dash,但后续 PR 在多个文件中重新引入了 em dash(—)。
修复:多次扫除 em dash,替换为普通逗号。项目有专门的 house convention:不用 em dash。
教训:代码风格的 house convention 需要持续维护。lint 规则或 CI check 可以防止漂移。
6.6 Shell 注入风险(statusline 嵌入路径)
问题:statusline 配置需要在 shell 命令中嵌入插件安装路径。如果路径包含 shell 元字符(&, $, `, ; 等),会导致注入。
修复:白名单校验而非转义:
function isShellSafe(p) {
return typeof p === 'string' && /^[A-Za-z0-9 _.\\-:/\\\\~]+$/.test(p);
}
不安全的路径回退到手动设置提示,绝不嵌入。
教训:安全选择白名单 > 尽力转义。不安全的路径宁可不给自动化,也不要赌每 shell 的转义规则。
6.7 Skill 触发词太窄(早期版本)
问题:ponytail skill 的触发词是关键词优先(“lazy”、“boilerplate”、“over-engineering”),普通编码任务(“加个日期选择器”、“写个去重函数”、“修个 bug”)完全不会触发——恰恰是过度工程最常见的场景。
修复:触发描述改为命名任何编码任务 + 否定条款排除非编码请求。
测试结果:编码任务召回从 2/6 提升到 6/6,精度不变(6/6,无误触发)。
教训:skill 的触发词设计决定了它的实际覆盖率。关键词匹配漏掉最大量的目标场景。
6.8 规则副本漂移
问题:修改了紧凑规则文本但忘记同步所有平台副本(.cursor/rules/、.windsurf/rules/ 等)。
工具:node scripts/check-rule-copies.js 检查同步状态。
教训:多平台规则分发必须自动化检查。手动同步永远会漏。
七、总结
Ponytail 是一个 代码简化策略引擎,不是库、不是框架、不是模型。它的全部力量来自一个 ~200 行的规则文件(AGENTS.md)和一个 7 级决策阶梯。
架构精髓:
- 单一真相源:AGENTS.md 是唯一规则定义,所有平台从它衍生
- 共享模块:ponytail-config / ponytail-instructions / ponytail-runtime 三个模块被所有平台适配器复用
- 分层适配:插件平台(Claude Code、Pi、OpenCode)用 JS 钩子注入;规则平台(Cursor、Windsurf、Cline)复制规则文件
- 始终在线:每次会话自动激活,子代理也注入,模式切换在下一条消息生效
设计哲学:
- 对解决方案懒惰,对理解从不懒惰
- 删除优于添加,无聊优于聪明
- 最短路径就是正确路径
- 但安全底线不可逾越
如果你的 AI 代理总是在写 120 行的缓存类、27 行的邮件验证器、或者引入 moment.js 就为了一个格式化调用——给它装上 Ponytail,它会用一行 @lru_cache、一个 "@" in email、或者 Intl.DateTimeFormat 替换掉它们。
“The shortest path to done is the right path.”
参考链接:
- GitHub 仓库
- npm 包
- 基准测试详情

