# DietrichGebert/ponytail — codebase unpack

> Canonical page: https://codevetter.com/unpack/ponytail

This is a deterministic Repo Unpack scan of DietrichGebert/ponytail — a structural map, not a review, security audit, or maintainer-endorsed description.

- Scanned commit: e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156 (2026-09-14)
- Last upstream commit: chore: release v4.10.0 (#870)
- Generated by codevetter 1.12.0 on 2026-09-22
- 159 files scanned · 758 KB

## Languages

- Markdown: 57 files
- JavaScript: 48 files
- JSON: 20 files
- YAML: 9 files
- Python: 6 files
- TOML: 6 files
- Shell: 1 files

## Repository structure

- benchmarks/ — 35 files
- tests/ — 16 files
- hooks/ — 12 files
- examples/ — 12 files
- .opencode/ — 8 files
- assets/ — 7 files
- scripts/ — 6 files
- .openclaw/ — 6 files

Probable source entrypoint: `hooks/` (inferred from the tree, not upstream docs)

## Source layout — hooks/

- hooks/claude-codex-hooks.json — 1 file
- hooks/copilot-hooks.json — 1 file
- hooks/cursor-hooks.json — 1 file
- hooks/ponytail-activate.js — 1 file
- hooks/ponytail-config.js — 1 file
- hooks/ponytail-instructions.js — 1 file
- hooks/ponytail-mode-tracker.js — 1 file
- hooks/ponytail-runtime.js — 1 file
- hooks/ponytail-statusline.ps1 — 1 file
- hooks/ponytail-statusline.sh — 1 file
- hooks/ponytail-subagent.js — 1 file
- hooks/qoder-hooks.json — 1 file

## Package

- package.json: @dietrichgebert/ponytail v4.10.0 — 0 declared dependencies

## Structural health signals

Average score 8.4/10 across 35 files; 4 hotspots. These are deterministic review leads, not proof of a bug.

## Recent history

- e3ba2aa6f1e6 (2026-09-14) chore: release v4.10.0 (#870)
- 67a16015536e (2026-09-14) feat: add native Cursor hooks via hooks.json (#817) (#869)
- 356918eba965 (2026-09-07) docs: rename the Retriever logo files so GitHub serves the new version
- 846fd86ff1e5 (2026-09-07) docs: Retriever logo with the name below the icon
- 57f759d7ffbf (2026-09-07) docs: Retriever as one logo image, icon and name together, dark and light

## Overview

Ponytail is a cross-platform 'lazy senior dev' behavior pack for AI coding agents: it injects a YAGNI/stdlib-first/minimal-diff ruleset into ~20 different agent hosts (Claude Code, Codex, Copilot, Cursor, Gemini, OpenCode, pi, Hermes, Qoder, Grok, OpenClaw, Devin, and more). The canonical behavior lives in skills/ponytail/SKILL.md plus a compact copy in AGENTS.md; everything else is a thin per-host adapter — lifecycle hook scripts in hooks/, plugin manifests, copied rule files, a pi extension, a Hermes plugin, an OpenCode plugin, and an MCP server — that resolves an intensity mode (lite/full/ultra/off), filters the ruleset to that level, and injects it into the agent's context. It ships with an extensive benchmark harness (promptfoo single-shot plus a headless Claude Code agentic bench) backing the README's claim of ~54% less code at 100% safety.

## System map

There is no long-running application: ponytail is a distribution of instruction text plus small adapter shims that host agents invoke. The runtime core is five CommonJS scripts in hooks/ (config resolution, instruction building, host detection/output, activation, mode tracking, subagent injection) spawned by host lifecycle hooks. Per-host adapters (Claude/Codex plugin manifest + hooks JSON, Copilot hooks JSON, Cursor hooks installer, Gemini extension manifest, OpenCode .mjs plugin, pi extension, Hermes Python plugin, MCP stdio server, and instruction-only rule files) all funnel into the shared instruction builder. State is a single mode flag file plus an optional config.json in the user's home/config dirs. Tests are node:test files at tests/, plus nested suites in pi-extension/test and ponytail-mcp/test, driven by `npm test` and the test.yml GitHub Action.
- The npm package's only entrypoint is the OpenCode plugin module: package.json sets "main" and exports to ./.opencode/plugins/ponytail.mjs. — [package.json#L19-23](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/package.json#L19-L23)
- The runtime core is five shared CommonJS modules — ponytail-config.js (mode/config resolution), ponytail-instructions.js (ruleset builder), ponytail-runtime.js (host detection, state file, stdout protocol), ponytail-activate.js (SessionStart), ponytail-mode-tracker.js (UserPromptSubmit), ponytail-subagent.js (SubagentStart). — [hooks/ponytail-config.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js) [hooks/ponytail-instructions.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-instructions.js) [hooks/ponytail-runtime.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js) [hooks/ponytail-activate.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js) [hooks/ponytail-mode-tracker.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js) [hooks/ponytail-subagent.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js)
- Claude Code and Codex share one hook map, hooks/claude-codex-hooks.json, wiring SessionStart → ponytail-activate.js, SubagentStart → ponytail-subagent.js, and UserPromptSubmit → ponytail-mode-tracker.js, each run as `node "${CLAUDE_PLUGIN_ROOT}/hooks/…"` with a 5s timeout. — [hooks/claude-codex-hooks.json](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/claude-codex-hooks.json) [.claude-plugin/plugin.json#L9](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.claude-plugin/plugin.json#L9) [.codex-plugin/plugin.json#L14](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.codex-plugin/plugin.json#L14)
- Host detection is env-var based and order-sensitive: COPILOT_PLUGIN_DATA (or a CLAUDE_PLUGIN_ROOT under .vscode/agent-plugins) → Copilot, PLUGIN_DATA → Codex, QODER_SESSION_ID → Qoder, CURSOR_VERSION → Cursor, otherwise native Claude; each host picks a different stateDir and a different stdout JSON shape. — [hooks/ponytail-runtime.js#L13-39](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L13-L39) [hooks/ponytail-runtime.js#L80-131](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L80-L131)
- The single source of truth for the ruleset is skills/ponytail/SKILL.md; getPonytailInstructions() reads it and strips mode-keyed table rows/example bullets for the active level, falling back to an embedded condensed ruleset if the file is unreadable. — [hooks/ponytail-instructions.js#L9-41](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-instructions.js#L9-L41) [hooks/ponytail-instructions.js#L77-92](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-instructions.js#L77-L92) [skills/ponytail/SKILL.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/skills/ponytail/SKILL.md)
- Runtime state is a per-host flag file named .ponytail-active holding the mode string (e.g. ~/.claude/.ponytail-active, $PLUGIN_DATA/.ponytail-active, ~/.cursor/.ponytail-active, ~/.qoder/.ponytail-active); an absent flag means ponytail is off. — [hooks/ponytail-runtime.js#L6-57](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L6-L57)
- User configuration resolves in order: PONYTAIL_DEFAULT_MODE env var, then defaultMode in $XDG_CONFIG_HOME/ponytail/config.json (~/.config/ponytail or %APPDATA%\ponytail), then the built-in 'full'. — [hooks/ponytail-config.js#L54-100](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L54-L100)
- The OpenCode adapter is a server plugin that registers slash commands from .opencode/command/*.md, adds skills/ to config.skills.paths, appends the ruleset via the experimental.chat.system.transform hook, and persists /ponytail switches in command.execute.before to ~/.config/opencode/.ponytail-active. — [.opencode/plugins/ponytail.mjs#L27-98](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.opencode/plugins/ponytail.mjs#L27-L98) [.opencode/plugins/ponytail-frontmatter.cjs](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.opencode/plugins/ponytail-frontmatter.cjs)
- The pi adapter (pi-extension/index.js) registers six slash commands, paints a status-bar indicator via ctx.ui.setStatus, persists session mode through pi.appendEntry('ponytail-mode'), and prepends the ruleset in the before_agent_start event. — [pi-extension/index.js#L63-210](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/pi-extension/index.js#L63-L210)
- The Hermes adapter is the root __init__.py Python plugin: register() exposes the six skills, hooks pre_llm_call for ruleset injection and pre_gateway_dispatch to rewrite authorized /ponytail-* gateway commands into agent prompts. — [__init__.py#L125-217](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/__init__.py#L125-L217) [plugin.yaml](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/plugin.yaml)
- ponytail-mcp is a private, internal-only MCP stdio server that serves the same ruleset as a 'ponytail' prompt and a 'ponytail_instructions' tool, reusing hooks/ponytail-instructions.js via createRequire. — [ponytail-mcp/index.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/ponytail-mcp/index.js) [ponytail-mcp/instructions.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/ponytail-mcp/instructions.js) [ponytail-mcp/package.json](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/ponytail-mcp/package.json)
- Instruction-only hosts get verbatim copies of the compact ruleset: .cursor/rules/ponytail.mdc, .windsurf/rules/ponytail.md, .clinerules/ponytail.md, .agents/rules/ponytail.md, .qoder/rules/ponytail.md, .github/copilot-instructions.md, .kiro/steering/ponytail.md — all byte-compared against AGENTS.md by scripts/check-rule-copies.js. — [scripts/check-rule-copies.js#L15-37](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/check-rule-copies.js#L15-L37) [docs/agent-portability.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/docs/agent-portability.md)
- Build/test: `npm test` runs `node --test tests/*.test.js` then the nested pi-extension and ponytail-mcp suites; CI (.github/workflows/test.yml) additionally runs check-rule-copies.js and check-versions.js on Node 22 / Python 3.12. Release is `npm publish` via publish.yml using OIDC trusted publishing, no token. — [package.json#L37-38](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/package.json#L37-L38) [.github/workflows/test.yml](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.github/workflows/test.yml) [.github/workflows/publish.yml](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.github/workflows/publish.yml)
- Deployment shape is per-host plugin manifests: .claude-plugin/, .codex-plugin/, .devin-plugin/, .github/plugin/ (Copilot), .qoder-plugin/, .grok-plugin/marketplace.json, .agents/plugins/marketplace.json, gemini-extension.json, plugin.yaml (Hermes), .openclaw/skills/ (ClawHub), and the pi package block in package.json. — [.claude-plugin/plugin.json](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.claude-plugin/plugin.json) [.codex-plugin/plugin.json](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.codex-plugin/plugin.json) [.devin-plugin/plugin.json](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.devin-plugin/plugin.json) [.github/plugin/plugin.json](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.github/plugin/plugin.json) [gemini-extension.json](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/gemini-extension.json) [plugin.yaml](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/plugin.yaml) [package.json#L40-43](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/package.json#L40-L43)

## Feature catalog

User-facing surface is six skills/commands plus a mode system. The core skill ponytail enforces the 'ladder' (YAGNI → reuse → stdlib → native → installed dep → one line → minimum code) at three intensities (lite/full/ultra) plus off. Companion skills ponytail-review, -audit, -debt, -gain, -help provide focused prompts. Host adapters add activation, mode switching, subagent injection, a statusline badge, and a Cursor hooks installer. Benchmarks and example before/after docs round out the offering.
- Six skills ship in skills/: ponytail (the ruleset with lite/full/ultra levels), ponytail-review (over-engineering diff review with delete/stdlib/native/yagni/shrink tags), ponytail-audit, ponytail-debt (harvests `ponytail:` comments into a debt ledger), ponytail-gain (benchmark scoreboard), ponytail-help. — [skills/ponytail/SKILL.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/skills/ponytail/SKILL.md) [skills/ponytail-review/SKILL.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/skills/ponytail-review/SKILL.md) [docs/agent-portability.md#L43-49](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/docs/agent-portability.md#L43-L49) [scripts/build-openclaw-skills.js#L19-26](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/build-openclaw-skills.js#L19-L26)
- The ponytail skill defines the 7-rung decision ladder, the 'bug fix = root cause' rule (grep all callers, fix the shared function once), the `ponytail:` ceiling-comment convention, the 'never lazy about' safety carve-outs, and the intensity table keyed by lite/full/ultra. — [skills/ponytail/SKILL.md#L32-118](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/skills/ponytail/SKILL.md#L32-L118)
- Mode switching: `/ponytail lite|full|ultra|off`, `@ponytail`/`$ponytail` aliases, `/ponytail` alone reports the level, `/ponytail default <mode>` persists to config.json, and the standalone phrases 'stop ponytail'/'normal mode' deactivate. — [hooks/ponytail-mode-tracker.js#L40-114](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L40-L114) [hooks/ponytail-config.js#L40-43](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L40-L43)
- File-based commands mirror the skills for hosts that need them: commands/*.toml (Claude/Gemini) and .opencode/command/*.md (OpenCode), with tests/commands.test.js asserting every pi-registered command ships both file forms. — [commands/ponytail.toml](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/commands/ponytail.toml) [.opencode/command/ponytail.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.opencode/command/ponytail.md) [tests/commands.test.js#L17-39](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/commands.test.js#L17-L39)
- A statusline badge feature: ponytail-activate.js detects a missing statusLine in settings.json and nudges the agent once (flag file .ponytail-statusline-nudged) to install hooks/ponytail-statusline.sh (POSIX) or .ps1 (Windows), which renders [PONYTAIL] / [PONYTAIL:ULTRA] from the flag file. — [hooks/ponytail-activate.js#L63-109](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js#L63-L109) [hooks/ponytail-statusline.sh](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-statusline.sh) [tests/hooks.test.js#L161-184](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks.test.js#L161-L184)
- Subagent coverage: ponytail-subagent.js injects the active ruleset into every spawned subagent, scoped optionally by the PONYTAIL_SUBAGENT_MATCHER regex against agent_type (unanchored, case-insensitive, fail-open). — [hooks/ponytail-subagent.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js) [tests/hooks.test.js#L292-358](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks.test.js#L292-L358)
- Cursor support is a manual installer: `node scripts/cursor-hooks.js install [--project]` merges ponytail's sessionStart/beforeSubmitPrompt entries into ~/.cursor/hooks.json (or a project file), replacing PONYTAIL_DIR with the checkout path and preserving unrelated hooks; uninstall removes only ponytail entries. — [scripts/cursor-hooks.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/cursor-hooks.js) [hooks/cursor-hooks.json](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/cursor-hooks.json) [docs/cursor-hooks.md#L15-39](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/docs/cursor-hooks.md#L15-L39)
- Hermes-specific features: bundled skills registered as ponytail:<skill>, slash commands /ponytail and /ponytail-review/-audit/-debt/-gain/-help, gateway command rewriting with a slash-access check (_slash_access_denied), and process-local runtime mode. — [__init__.py#L14-20](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/__init__.py#L14-L20) [__init__.py#L140-217](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/__init__.py#L140-L217) [plugin.yaml#L5-21](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/plugin.yaml#L5-L21) [after-install.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/after-install.md)
- The MCP server exposes a user-invoked 'ponytail' prompt and a read-only 'ponytail_instructions' tool, both taking a lite/full/ultra mode argument validated by a zod enum, serving hosts whose only injection point is the prompt menu. — [ponytail-mcp/index.js#L18-50](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/ponytail-mcp/index.js#L18-L50)
- Gemini/Antigravity use gemini-extension.json pointing contextFileName at AGENTS.md for always-on rules plus auto-discovered commands/*.toml and skills/; the repo deliberately does not ship a root hooks/hooks.json because Gemini would auto-load Claude-named events. — [gemini-extension.json](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/gemini-extension.json) [docs/agent-portability.md#L17](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/docs/agent-portability.md#L17) [README.md#L204-205](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/README.md#L204-L205)
- Qoder full-tier support is a reference template hooks/qoder-hooks.json (UserPromptSubmit → mode-tracker, PreToolUse task|Task → subagent hook) that users merge into .qoder/settings.json with a PONYTAIL_DIR substitution. — [hooks/qoder-hooks.json](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/qoder-hooks.json) [README.md#L207-211](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/README.md#L207-L211)
- Benchmarks are a shipped feature: single-shot promptfoo evals (benchmarks/promptfooconfig*.yaml), a headless-Claude agentic bench (benchmarks/agentic/run.py) scoring LOC/cost/time/safety, an Ollama local runner (benchmarks/benchmark-local.py), a behavior gate (benchmarks/behavior.js + behavior.yaml), and dated result write-ups in benchmarks/results/. — [benchmarks/README.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/README.md) [benchmarks/agentic/run.py](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/run.py) [benchmarks/behavior.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/behavior.js) [tests/behavior.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/behavior.test.js)
- examples/ contains 11 before/after 'survivor' write-ups (date-picker-style over-builds collapsed to native features) referenced from the README; assets/ holds the logos and benchmark SVGs the README renders. — [examples/README.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/examples/README.md) [examples/debounce.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/examples/debounce.md) [README.md#L62-73](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/README.md#L62-L73)

## Data flow

Data flow is prompt-text injection: config resolution produces a mode, the mode filters the canonical SKILL.md body, and the host-specific hook output protocol (raw stdout, additionalContext JSON, or additional_context JSON) carries the ruleset into the model's context. State is minimal: a one-word flag file per host state dir, a config.json for the persisted default, and a nudge marker file. Inputs are env vars (host detection, mode default, subagent matcher) and stdin JSON (prompt text, agent_type); outputs are stdout writes the host harness parses.
- Ruleset path: skills/ponytail/SKILL.md → getPonytailInstructions(mode) → filterSkillBodyForMode strips frontmatter and drops intensity-table rows (`| **lite** |`) and worked-example bullets (`- lite: "…"`) whose label isn't the active mode → prefixed with 'PONYTAIL MODE ACTIVE — level: <mode>'. — [hooks/ponytail-instructions.js#L11-41](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-instructions.js#L11-L41) [hooks/ponytail-instructions.js#L77-92](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-instructions.js#L77-L92)
- Mode resolution path: PONYTAIL_DEFAULT_MODE env → ~/.config/ponytail/config.json (or %APPDATA%\ponytail) defaultMode → 'full'; only RUNTIME_MODES (off/lite/full/ultra) are accepted, so 'review' can never become a default (#377). — [hooks/ponytail-config.js#L16-34](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L16-L34) [hooks/ponytail-config.js#L76-100](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L76-L100)
- Session persistence: ponytail-activate.js writes the resolved mode into <stateDir>/.ponytail-active; ponytail-mode-tracker.js rewrites or deletes it on /ponytail commands; ponytail-subagent.js and the statusline scripts read it. — [hooks/ponytail-activate.js#L53-58](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js#L53-L58) [hooks/ponytail-mode-tracker.js#L85-114](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L85-L114) [hooks/ponytail-subagent.js#L16-21](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js#L16-L21) [hooks/ponytail-statusline.sh#L3-6](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-statusline.sh#L3-L6)
- Stdout protocol is host-shaped: Copilot gets {additionalContext} only on SessionStart, Codex gets systemMessage + hookSpecificOutput, Qoder gets hookSpecificOutput without systemMessage, Cursor gets {additional_context, continue}, native Claude gets raw stdout except SubagentStart which needs hookSpecificOutput JSON. — [hooks/ponytail-runtime.js#L80-131](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L80-L131)
- UserPromptSubmit input arrives as piped stdin JSON; the tracker parses data.prompt, lowercases it, matches ^[/@$]ponytail or the exact deactivation phrases, then emits the mode-change header (Cursor also gets the new level's full ruleset since it has no /ponytail command that loads the skill). — [hooks/ponytail-mode-tracker.js#L21-107](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L21-L107)
- Qoder lacks SessionStart, so its UserPromptSubmit path does double duty: first prompt initializes the flag from getDefaultMode(), and every prompt re-injects the full ruleset, folding the mode-change confirmation into one JSON write. — [hooks/ponytail-mode-tracker.js#L116-138](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L116-L138)
- Subagent scoping input: when PONYTAIL_SUBAGENT_MATCHER is set, the hook reads agent_type from stdin JSON and skips injection only on a definite mismatch; missing/unparseable payloads, regex errors, and timeouts all fail open to injecting. — [hooks/ponytail-subagent.js#L31-77](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js#L31-L77)
- Config writes: only `/ponytail default <mode>` calls writeDefaultMode, which mkdirs the config dir, merges defaultMode into any existing JSON (preserving unrelated keys), and refuses non-runtime modes; plain switches stay session-scoped. — [hooks/ponytail-config.js#L136-151](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L136-L151) [hooks/ponytail-mode-tracker.js#L55-66](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L55-L66) [tests/hooks.test.js#L429-469](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks.test.js#L429-L469)
- OpenCode data flow: command.execute.before persists the /ponytail argument to ~/.config/opencode/.ponytail-active; experimental.chat.system.transform reads it each turn and appends the filtered ruleset to the last system message (or pushes one), so a mode switch applies from the next message. — [.opencode/plugins/ponytail.mjs#L27-44](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.opencode/plugins/ponytail.mjs#L27-L44) [.opencode/plugins/ponytail.mjs#L74-97](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.opencode/plugins/ponytail.mjs#L74-L97)
- pi data flow: session_start replays persisted 'ponytail-mode' entries via resolveSessionMode to recover the mode, before_agent_start returns {systemPrompt: base + ruleset}, and the input event listener turns the deactivation phrase into setMode('off'). — [pi-extension/index.js#L24-37](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/pi-extension/index.js#L24-L37) [pi-extension/index.js#L174-210](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/pi-extension/index.js#L174-L210)
- Hermes data flow: _pre_llm_call returns {'context': build_injected_context(mode)} each turn; mode lives in the module-global _current_mode set by the /ponytail command handler (process-local, not persisted); review mode injects the ponytail-review SKILL.md body instead of the filtered ladder. — [__init__.py#L27](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/__init__.py#L27) [__init__.py#L105-128](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/__init__.py#L105-L128) [__init__.py#L167-177](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/__init__.py#L167-L177)
- OpenClaw/ClawHub artifacts are generated: scripts/build-openclaw-skills.js copies each skills/<name>/SKILL.md body verbatim into .openclaw/skills/ with a rewritten single-line <160-char description frontmatter, so the ruleset can't drift from canonical. — [scripts/build-openclaw-skills.js#L1-60](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/build-openclaw-skills.js#L1-L60)
- Benchmark data flow: run.py seeds a temp workspace (fixture repo or seed files + git baseline), runs `claude -p` with --plugin-dir for skill arms or --append-system-prompt for prompt arms, then scores the git diff / file stats for correctness+safety and reads cost/tokens from the CLI's JSON output. — [benchmarks/agentic/run.py#L283-339](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/run.py#L283-L339) [benchmarks/agentic/run.py#L257-281](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/run.py#L257-L281) [benchmarks/agentic/tasks.py#L1-31](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/tasks.py#L1-L31)

## Behavior traces

Three traced flows: (1) session activation — host fires SessionStart → ponytail-activate.js resolves mode → writes flag → emits filtered ruleset; (2) mid-session mode switch — UserPromptSubmit → ponytail-mode-tracker.js parses stdin → updates flag/config → confirms; (3) subagent spawn — SubagentStart → ponytail-subagent.js reads flag → optionally filters on agent_type → injects. Also traced: OpenCode turn injection and the Cursor hooks install/uninstall path.
- Startup flow (Claude Code): claude-codex-hooks.json SessionStart spawns `node hooks/ponytail-activate.js` → getDefaultMode() resolves env→config→'full' → 'off' exits early after clearMode() → setMode(mode) writes $CLAUDE_CONFIG_DIR/.ponytail-active → getPonytailInstructions(mode) builds the ruleset → statusline check appends a one-time setup nudge → writeHookOutput('SessionStart', mode, output). — [hooks/claude-codex-hooks.json#L3-15](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/claude-codex-hooks.json#L3-L15) [hooks/ponytail-activate.js#L28-115](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js#L28-L115)
- Cursor startup differs: ponytail-activate.js first checks cursorRulePath() for .cursor/rules/ponytail.mdc in CURSOR_PROJECT_DIR; if the always-on rule exists it emits only a cursorRuleNotice (no flag write, no ruleset) so hook and rule can't contradict. — [hooks/ponytail-activate.js#L38-51](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js#L38-L51) [hooks/ponytail-runtime.js#L59-78](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L59-L78)
- Mode-switch flow: UserPromptSubmit → ponytail-mode-tracker.js accumulates stdin → finish() JSON-parses (BOM-stripped) → /^[/@$]ponytail/ match → 'default' arg calls writeDefaultMode and returns early; a level arg calls setMode + emits 'PONYTAIL MODE CHANGED'; 'off' calls clearMode + 'PONYTAIL MODE OFF'; bare /ponytail is report-only. — [hooks/ponytail-mode-tracker.js#L21-107](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L21-L107)
- Deactivation flow: isDeactivationCommand requires the whole trimmed message to be 'stop ponytail' or 'normal mode' (after dropping trailing punctuation), so an incidental phrase like 'add a normal mode toggle' cannot turn ponytail off — regression-tested in tests/hooks.test.js. — [hooks/ponytail-config.js#L36-43](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L36-L43) [hooks/ponytail-mode-tracker.js#L109-114](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L109-L114) [tests/hooks.test.js#L110-125](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks.test.js#L110-L125)
- Subagent flow: SubagentStart → ponytail-subagent.js → readMode() returns flag contents; absent/'off' exits 0 silently → optional PONYTAIL_SUBAGENT_MATCHER compiled in try/catch (invalid → inject-everywhere) → if matcher set, stdin agent_type tested; definite mismatch exits, everything else → inject() → writeHookOutput('SubagentStart', mode, ruleset). — [hooks/ponytail-subagent.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js)
- Never-hang contract: both stdin-reading hooks attach stdin 'error' → finish()+exit and a 1000ms unref'd setTimeout fallback, because Windows PowerShell `if {}` wrappers can swallow piped JSON so 'end' never fires (#443); the no-matcher subagent path never touches stdin at all. — [hooks/ponytail-mode-tracker.js#L144-155](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L144-L155) [hooks/ponytail-subagent.js#L41-48](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js#L41-L48) [hooks/ponytail-subagent.js#L73-77](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js#L73-L77)
- OpenCode turn flow: plugin config() hook scans .opencode/command/*.md via parseCommandFile into config.command and appends skills/ to config.skills.paths → per turn, experimental.chat.system.transform reads ~/.config/opencode/.ponytail-active (falling back to getDefaultMode) and appends getPonytailInstructions(mode) to the system prompt → command.execute.before writes the new mode when the user runs /ponytail. — [.opencode/plugins/ponytail.mjs#L51-98](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.opencode/plugins/ponytail.mjs#L51-L98) [.opencode/plugins/ponytail-frontmatter.cjs#L13-21](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.opencode/plugins/ponytail-frontmatter.cjs#L13-L21)
- pi session flow: session_start → getDefaultMode + getHideStatus + resolveSessionMode(entries) → syncStatus paints '🐴 ponytail: <icon> <MODE>' → before_agent_start returns systemPrompt with ruleset appended (guard against null event/systemPrompt, #439/#440) → agent_start/agent_end toggle the ●/○ active dot. — [pi-extension/index.js#L183-210](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/pi-extension/index.js#L183-L210) [pi-extension/index.js#L71-99](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/pi-extension/index.js#L71-L99)
- Cursor install flow: scripts/cursor-hooks.js install → readConfig (malformed JSON → SyntaxError → refuse) → stripPonytail removes stale ponytail entries → ponytailEntries() renders cursor-hooks.json with PONYTAIL_DIR→forward-slash root after isShellSafe() rejects metachar paths → merged entries appended per event → writeConfig; uninstall reverses and deletes the file if nothing else remains. — [scripts/cursor-hooks.js#L44-105](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/cursor-hooks.js#L44-L105)
- Cleanup flow: scripts/uninstall.js removes .ponytail-active from the Claude and Cursor dirs, the config file, strips ponytail entries from ~/.cursor/hooks.json via cursorHooks.uninstall('user'), and surgically removes only the ponytail segment from a possibly-combined statusLine command in settings.json. — [scripts/uninstall.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/uninstall.js)
- Agentic benchmark flow: run.py run_cell copies the fixture repo (or writes seed files) → git snapshot → spawns `claude -p <prompt> --model <m> --permission-mode bypassPermissions --disallowedTools Bash --strict-mcp-config` (+ --plugin-dir for skill arms) → stdout→_claude.json file (never a PIPE, so timeouts work) → 300s CELL_TIMEOUT with _tree_kill → score_workspace computes git_diff_stats/code_stats + per-task deterministic scorers. — [benchmarks/agentic/run.py#L283-339](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/run.py#L283-L339)

## Testing signals

Tests are plain node:test/assert suites — no framework, matching the project's own dogma. tests/*.test.js covers hook behavior end-to-end via spawnSync (every host env permutation), manifest/command/packaging drift guards, and grader self-checks; pi-extension/test and ponytail-mcp/test cover the adapters' pure functions. CI runs the suite plus two consistency scripts on every push/PR. Coverage is strong on the hooks protocol and drift guards; weaker on the Hermes Python plugin logic depth and on actual model behavior (benchmarks are manual, API-spend).
- Test framework is node:test + assert with no dependencies; `npm test` = `node --test tests/*.test.js && npm test --prefix pi-extension && npm test --prefix ponytail-mcp`. — [package.json#L37-38](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/package.json#L37-L38) [tests/commands.test.js#L8-9](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/commands.test.js#L8-L9) [pi-extension/package.json#L5-7](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/pi-extension/package.json#L5-L7)
- tests/hooks.test.js is an integration suite that spawnSyncs the real hook scripts under fabricated HOME/env dirs, asserting flag-file placement, per-host stdout JSON shapes, statusline nudge once-only behavior, CLAUDE_CONFIG_DIR override, and Copilot-vs-Codex state isolation. — [tests/hooks.test.js#L21-253](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks.test.js#L21-L253)
- Subagent matcher behavior is fully covered: case-insensitive unanchored match, anchored rejection of superset names, missing agent_type fail-open, invalid-regex fail-open, and the no-matcher path's independence from stdin. — [tests/hooks.test.js#L292-358](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks.test.js#L292-L358)
- Drift guards: tests/commands.test.js greps pi-extension/index.js for registerCommand calls and fails if any registered command lacks commands/<name>.toml or .opencode/command/<name>.md; tests/package.test.js guards scripts/uninstall.js staying in the npm files list. — [tests/commands.test.js#L17-39](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/commands.test.js#L17-L39) [tests/package.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/package.test.js)
- Per-host manifest tests exist for every adapter: gemini-extension.test.js, qoder-plugin.test.js, grok-plugin.test.js, hermes-plugin.test.js, copilot-plugin.test.js, opencode-plugin.test.js, openclaw-skills.test.js, plus hooks-windows.test.js and cursor-hooks.test.js for platform edges. — [tests/gemini-extension.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/gemini-extension.test.js) [tests/hermes-plugin.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hermes-plugin.test.js) [tests/opencode-plugin.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/opencode-plugin.test.js) [tests/cursor-hooks.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/cursor-hooks.test.js) [tests/hooks-windows.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks-windows.test.js)
- Two consistency scripts run in CI before tests: scripts/check-rule-copies.js byte-compares seven rule-file copies against AGENTS.md and asserts 9 rule invariants survive in SKILL.md+AGENTS.md; scripts/check-versions.js requires all 8 version-bearing manifests to share one pinned semver equal to the release tag. — [scripts/check-rule-copies.js#L19-76](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/check-rule-copies.js#L19-L76) [scripts/check-versions.js#L21-78](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/check-versions.js#L21-L78) [.github/workflows/test.yml#L29-36](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.github/workflows/test.yml#L29-L36)
- Benchmark self-verification: `python benchmarks/agentic/run.py --selftest` validates every task scorer (good ref passes, bad ref caught on its declared axis), the plugin-dir resolver, and the tree-kill — before any API spend; tests/correctness.test.js and benchmarks/correctness.test.js cover scorer units. — [benchmarks/agentic/run.py#L183-244](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/run.py#L183-L244) [benchmarks/agentic/tasks.py#L15-21](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/tasks.py#L15-L21) [benchmarks/correctness.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/correctness.test.js)
- tests/behavior.test.js unit-tests the promptfoo behavior grader (benchmarks/behavior.js) by feeding known behavior-present/absent outputs through each probe checker, making the behavior.yaml eval trustworthy without an API key. — [tests/behavior.test.js#L1-50](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/behavior.test.js#L1-L50) [benchmarks/behavior.yaml](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/behavior.yaml)
- CI (.github/workflows/test.yml) runs on push to main, tags, and PRs: checkout → Node 22 + Python 3.12 + pandas → npm install for ponytail-mcp → check-rule-copies → check-versions → npm test. There is no coverage tool, lint step, or Windows/macOS CI matrix. — [.github/workflows/test.yml](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.github/workflows/test.yml)
- The Hermes Python plugin (__init__.py) has a dedicated test (tests/hermes-plugin.test.js) but the deep logic — _filter_skill_body_for_mode parity with the JS version, gateway access denial — is exercised through JS-side assertions rather than a Python suite; there is no pytest configuration in the repo. (inferred) — [tests/hermes-plugin.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hermes-plugin.test.js) [__init__.py#L70-122](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/__init__.py#L70-L122)
- Uncovered-by-default areas: live model behavior (benchmarks require ANTHROPIC_API_KEY/manual runs and are not in CI), the .ps1 statusline path on Windows (guarded only by hooks-windows.test.js shape checks), and whether hosts actually deliver injected context (docs/cursor-hooks.md notes that's a live-session check). (inferred) — [.env.example](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.env.example) [benchmarks/README.md#L9-19](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/README.md#L9-L19) [tests/hooks-windows.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks-windows.test.js) [docs/cursor-hooks.md#L8-10](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/docs/cursor-hooks.md#L8-L10)

## Risk map

Primary risk surface is that ponytail code runs inside the user's home directory on every prompt: it writes state files under ~/.claude, ~/.cursor, ~/.qoder, plugin data dirs, and config dirs, and it edits user-owned settings.json/hooks.json files. Mitigations are real — a shell-safe path allowlist, BOM-tolerant parsing, fail-open hooks with hard timeouts, and surgical uninstalls — but the blast radius of a bug is the user's agent config, not the repo. Secondary risks: drift between 8 version manifests and 7+ rule copies (guarded), env-detection order fragility, and benchmark runs that shell out to claude with bypassPermissions.
- Hooks write outside the repo: setMode/clearMode mutate <stateDir>/.ponytail-active under ~/.claude, $PLUGIN_DATA, $COPILOT_PLUGIN_DATA, ~/.qoder, or ~/.cursor; writeDefaultMode mutates the user's global ponytail config.json; uninstall.js and cursor-hooks.js edit or delete ~/.cursor/hooks.json and ~/.claude/settings.json. — [hooks/ponytail-runtime.js#L31-48](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L31-L48) [hooks/ponytail-config.js#L136-151](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L136-L151) [scripts/uninstall.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/uninstall.js) [scripts/cursor-hooks.js#L24-47](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/cursor-hooks.js#L24-L47)
- Shell-injection guard: isShellSafe() allowlists path characters before the install path is embedded in a statusline shell command or a Cursor hooks.json entry; a checkout path with quotes/&/$/backtick/; falls back to manual-setup text instead of emitting a command. — [hooks/ponytail-config.js#L45-52](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L45-L52) [hooks/ponytail-activate.js#L84-105](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js#L84-L105) [scripts/cursor-hooks.js#L53-65](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/cursor-hooks.js#L53-L65) [tests/hooks.test.js#L15-19](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks.test.js#L15-L19)
- Host detection is a single ordered cascade of env checks in ponytail-runtime.js; a host that sets a foreign env var (e.g. CLAUDE_PLUGIN_ROOT under VS Code Copilot, CURSOR_VERSION leaking into a nested Claude session) is misrouted — the file documents two such bugs already fixed (#528, #817), so new hosts must be inserted with care. — [hooks/ponytail-runtime.js#L8-29](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L8-L29)
- Fail-silent posture: nearly every hook error path is an empty catch ('Silent fail — don't block session start'), which keeps sessions alive but means real breakage (unwritable state dir, EPIPE) leaves no signal beyond missing output. — [hooks/ponytail-activate.js#L54-58](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js#L54-L58) [hooks/ponytail-activate.js#L107-115](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-activate.js#L107-L115) [hooks/ponytail-mode-tracker.js#L139-141](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L139-L141) [hooks/ponytail-subagent.js#L26-29](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js#L26-L29)
- A hung hook would freeze the user's prompt submission; both stdin-consuming hooks defend with an 'error' handler plus a 1s unref'd timeout that force-finishes, a workaround for Windows PowerShell swallowing piped stdin (#443). — [hooks/ponytail-mode-tracker.js#L147-155](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L147-L155) [hooks/ponytail-subagent.js#L75-77](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js#L75-L77)
- settings.json/hooks.json edits are surgical but fragile: uninstall.js splits a combined statusLine command on '&&'/';' (documented ponytail: simplification — piped statuslines are unsupported), and cursor-hooks.js identifies ponytail entries only by the /ponytail-[\w-]+\.js/ regex on the command string. — [scripts/uninstall.js#L48-66](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/uninstall.js#L48-L66) [scripts/cursor-hooks.js#L18-22](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/cursor-hooks.js#L18-L22)
- Version drift has shipped before (v4.8.0 stayed at 4.7.0 across all manifests, #260/#262); check-versions.js now pins 8 files and validates the release tag, but it only runs in CI — a local tag push before CI could still publish stale manifests. (inferred) — [scripts/check-versions.js#L1-30](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/check-versions.js#L1-L30) [.github/workflows/publish.yml](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.github/workflows/publish.yml)
- Rule-copy drift: seven per-host rule files must stay byte-identical to AGENTS.md and nine invariant phrases must exist in SKILL.md; check-rule-copies.js is a canary, not full equality for SKILL.md (noted in its own comment), so subtle ruleset wording changes can slip if they dodge the invariants. — [scripts/check-rule-copies.js#L39-69](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/check-rule-copies.js#L39-L69)
- The agentic benchmark deliberately runs `claude -p` with --permission-mode bypassPermissions on seeded repos and force-kills process trees on timeout — safe by design (temp dirs, Bash disallowed, per-session killpg/taskkill /T) but it is the only code path that executes an agent with full permissions. — [benchmarks/agentic/run.py#L225-244](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/run.py#L225-L244) [benchmarks/agentic/run.py#L310-336](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/run.py#L310-L336)
- Mode-filtering correctness is load-bearing: filterSkillBodyForMode drops SKILL.md lines whose table/example label is a mode name; a future SKILL.md edit that labels a normal rule 'full:'-style (without a quoted value) is safe, but the Python port in __init__.py uses a slightly different example regex (`:\s*` vs `:\s*"`), so the two filters can diverge on edge lines. (inferred) — [hooks/ponytail-instructions.js#L11-41](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-instructions.js#L11-L41) [__init__.py#L70-87](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/__init__.py#L70-L87)
- PONYTAIL_SUBAGENT_MATCHER is user-supplied regex compiled with 'i'; catastrophic backtracking on a hostile pattern is theoretically possible but bounded by the 1s timeout and fail-open design. (inferred) — [hooks/ponytail-subagent.js#L31-48](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-subagent.js#L31-L48)
- Cursor rule conflict is handled but detection only checks the first workspace root (CURSOR_PROJECT_DIR || cwd) — a documented ponytail: simplification; a ponytail.mdc in a secondary folder of a multi-root workspace goes undetected and would double-inject. — [hooks/ponytail-runtime.js#L59-70](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L59-L70)

## Extension points

The repo is built for adapter growth: a new host means a manifest plus (ideally) a reuse of the shared hooks/ scripts, and the test suite auto-discovers some surfaces. Adding a skill requires touching five places (skills/, SKILL_COMMANDS in __init__.py, pi registerCommand, commands/*.toml, .opencode/command/*.md, DESCRIPTIONS in build-openclaw-skills.js) — commands.test.js catches the file gaps. Adding a mode means extending RUNTIME_MODES/VALID_MODES and the SKILL.md intensity table. Adding a host version manifest requires updating VERSION_FILES in check-versions.js.
- New host adapters should reuse the shared builder: every existing adapter (OpenCode, pi, MCP, hooks, Hermes) funnels through getPonytailInstructions()/getDefaultMode() so a new host needs only host detection + output shaping, per the documented 'Adapter Rule: keep adapters thin'. — [docs/agent-portability.md#L35-39](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/docs/agent-portability.md#L35-L39) [hooks/ponytail-instructions.js#L77-92](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-instructions.js#L77-L92) [ponytail-mcp/instructions.js#L4-8](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/ponytail-mcp/instructions.js#L4-L8)
- New version-bearing manifests must be registered in the VERSION_FILES array in scripts/check-versions.js ('Add new host manifests here so a future ecosystem can't drift unnoticed'). — [scripts/check-versions.js#L19-30](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/check-versions.js#L19-L30)
- New rule-file copies must be added to the copies array in scripts/check-rule-copies.js with a normalizer (stripFrontmatter or trim), or they won't be drift-checked against AGENTS.md. — [scripts/check-rule-copies.js#L19-37](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/check-rule-copies.js#L19-L37)
- New skills plug in at skills/<name>/SKILL.md with frontmatter (name/description/argument-hint); the OpenCode plugin auto-discovers the skills/ dir, and Hermes's register() iterates skills/ children — but SKILL_COMMANDS in __init__.py, pi registerCommand calls, commands/*.toml, .opencode/command/*.md, and DESCRIPTIONS in build-openclaw-skills.js must be added by hand. — [__init__.py#L14-20](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/__init__.py#L14-L20) [__init__.py#L195-201](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/__init__.py#L195-L201) [pi-extension/index.js#L114-172](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/pi-extension/index.js#L114-L172) [scripts/build-openclaw-skills.js#L19-26](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/build-openclaw-skills.js#L19-L26) [.opencode/plugins/ponytail.mjs#L55-71](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.opencode/plugins/ponytail.mjs#L55-L71)
- tests/commands.test.js enforces the command-file contract automatically: it regexes registerCommand names out of pi-extension/index.js and requires matching commands/<name>.toml and .opencode/command/<name>.md files — a registered command with no adapter files fails CI. — [tests/commands.test.js#L15-39](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/commands.test.js#L15-L39)
- New intensity modes extend RUNTIME_MODES (runtime levels) vs VALID_MODES (adds 'review' for config) in ponytail-config.js, the SKILL.md intensity table/example bullets (auto-filtered by label), and the pi status icon map — Hermes mirrors RUNTIME_MODES/CONFIG_MODES in __init__.py. — [hooks/ponytail-config.js#L16-34](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L16-L34) [skills/ponytail/SKILL.md#L79-88](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/skills/ponytail/SKILL.md#L79-L88) [pi-extension/index.js#L84](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/pi-extension/index.js#L84) [__init__.py#L11-13](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/__init__.py#L11-L13)
- INDEPENDENT_MODES in ponytail-instructions.js is the extension seam for modes that don't share the ladder ruleset (currently 'review', which emits only a pointer to its skill); adding a mode there bypasses SKILL.md filtering. — [hooks/ponytail-instructions.js#L8](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-instructions.js#L8) [hooks/ponytail-instructions.js#L80-82](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-instructions.js#L80-L82)
- Hook output protocol for a new host is one branch in writeHookOutput() plus detection flags at the top of ponytail-runtime.js; Cursor's branch was added exactly this way ({additional_context, continue} shape). — [hooks/ponytail-runtime.js#L19-37](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L19-L37) [hooks/ponytail-runtime.js#L111-122](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L111-L122)
- Config surface extension: getQuietStartup and getHideStatus are the pattern for new user toggles — env var (PONYTAIL_*) beats config.json field, both BOM-tolerant, both exported for the pi extension. — [hooks/ponytail-config.js#L102-134](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L102-L134) [pi-extension/index.js#L8-9](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/pi-extension/index.js#L8-L9)
- Benchmark arms are a registry: the ARMS dict in run.py maps arm name → skill loader (file read or literal prompt); PLUGIN_ARMS selects which arms activate via --plugin-dir vs --append-system-prompt, and MODELS maps friendly names to model IDs. — [benchmarks/agentic/run.py#L35-61](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/benchmarks/agentic/run.py#L35-L61)
- OpenClaw packaging is generated, not hand-maintained: extend DESCRIPTIONS in build-openclaw-skills.js and rerun to emit .openclaw/skills/<name>/SKILL.md; tests/openclaw-skills.test.js fails on stale committed copies. — [scripts/build-openclaw-skills.js#L19-44](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/build-openclaw-skills.js#L19-L44) [tests/openclaw-skills.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/openclaw-skills.test.js)

## Contributor handoff

This repo edits its own dogma: AGENTS.md demands the smallest correct diff and `ponytail:` comments marking deliberate ceilings — match that style. The golden rules: SKILL.md and AGENTS.md are the sources of truth and every copy must stay in sync (CI enforces it); all shared logic lives in the CJS hooks/ modules so adapters stay thin; hooks must never hang or crash a session (fail-open, BOM-strip, timeouts); and any version bump touches 8 files. Always run `npm test` plus the two check scripts before claiming done.
- Read first, in order: AGENTS.md (the ruleset and the repo's own working style), skills/ponytail/SKILL.md (canonical long-form rules), hooks/ponytail-runtime.js + ponytail-config.js + ponytail-instructions.js (the shared core every adapter calls), then docs/agent-portability.md for the host→file map. — [AGENTS.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/AGENTS.md) [skills/ponytail/SKILL.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/skills/ponytail/SKILL.md) [hooks/ponytail-runtime.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js) [hooks/ponytail-config.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js) [hooks/ponytail-instructions.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-instructions.js) [docs/agent-portability.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/docs/agent-portability.md)
- After editing AGENTS.md or any rule copy (.cursor/rules/ponytail.mdc, .windsurf, .clinerules, .agents, .qoder, .github/copilot-instructions.md, .kiro), run `node scripts/check-rule-copies.js` — it fails CI on drift; after touching SKILL.md verify the 9 INVARIANTS phrases still appear verbatim. — [scripts/check-rule-copies.js#L19-76](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/check-rule-copies.js#L19-L76)
- Version bumps touch 8 files: .claude-plugin/plugin.json, .codex-plugin/plugin.json, .devin-plugin/plugin.json, .github/plugin/plugin.json, .qoder-plugin/plugin.json, gemini-extension.json, package.json, ponytail-mcp/package.json — run `node scripts/check-versions.js` to verify they agree. — [scripts/check-versions.js#L21-30](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/check-versions.js#L21-L30)
- Test command: `npm test` (root node:test suite + pi-extension + ponytail-mcp); the MCP suite needs `npm install --prefix ponytail-mcp` first (only directory with real deps: @modelcontextprotocol/sdk + zod). — [package.json#L37-38](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/package.json#L37-L38) [ponytail-mcp/package.json#L9-12](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/ponytail-mcp/package.json#L9-L12) [.github/workflows/test.yml#L26-27](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.github/workflows/test.yml#L26-L27)
- Hook contract traps: strip UTF-8 BOM before JSON.parse everywhere; never block on stdin without the 1s unref'd timeout + error handler (Windows swallows piped input, #443); native Claude SubagentStart requires hookSpecificOutput JSON not raw stdout; Cursor treats empty stdout as no-op and raw text as a parse error. — [hooks/ponytail-mode-tracker.js#L147-155](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L147-L155) [hooks/ponytail-runtime.js#L111-131](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L111-L131) [tests/cursor-hooks.test.js#L1-10](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/cursor-hooks.test.js#L1-L10)
- Mode semantics: 'review' is session-only and must never be a default (#377) — validate against RUNTIME_MODES for defaults and VALID_MODES for persisted values; isDeactivationCommand only fires on the whole-message phrase, not substring matches. — [hooks/ponytail-config.js#L16-43](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L16-L43) [tests/hooks.test.js#L466-475](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks.test.js#L466-L475)
- Safe edit boundaries: per-host manifests and hook JSONs are isolated (edit one without touching others); the shared hooks/ modules and skills/ are high blast-radius — a bug there affects every host. Anything in .openclaw/skills/ is generated — edit skills/ and rerun scripts/build-openclaw-skills.js instead. — [scripts/build-openclaw-skills.js#L1-12](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/build-openclaw-skills.js#L1-L12) [docs/agent-portability.md#L35-39](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/docs/agent-portability.md#L35-L39)
- House style: CommonJS in hooks/ and scripts/ (ESM only where the host requires it — ponytail.mjs and pi-extension/index.js bridge via createRequire), no dependencies outside ponytail-mcp, `ponytail:` comments mark deliberate simplifications with a named ceiling and upgrade path, and non-trivial logic leaves one runnable check behind. — [hooks/ponytail-runtime.js#L1-4](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L1-L4) [.opencode/plugins/ponytail.mjs#L12-24](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/.opencode/plugins/ponytail.mjs#L12-L24) [ponytail-mcp/package.json#L9-12](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/ponytail-mcp/package.json#L9-L12) [AGENTS.md](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/AGENTS.md)
- Issue-number archaeology matters: comments cite GitHub issues (#34 CLAUDE_CONFIG_DIR, #252 subagents, #377 review default, #443 Windows stdin, #483 nudge-once, #506 matcher, #528 VS Code Copilot, #573 Codex JSON shape, #817 Cursor) — when changing that code, re-check the referenced constraint or a regression test will catch it. — [hooks/ponytail-runtime.js#L8-29](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-runtime.js#L8-L29) [hooks/ponytail-config.js#L79-83](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-config.js#L79-L83) [hooks/ponytail-mode-tracker.js#L147-149](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-mode-tracker.js#L147-L149) [tests/hooks.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks.test.js)
- Windows is a first-class target: tests set both HOME and USERPROFILE, paths go through os.homedir(), cursor-hooks.js normalizes to forward slashes, the statusline has a .ps1 twin, and hooks-windows.test.js covers platform edges — don't introduce POSIX-only assumptions. — [tests/hooks.test.js#L53-59](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks.test.js#L53-L59) [scripts/cursor-hooks.js#L53-58](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/scripts/cursor-hooks.js#L53-L58) [hooks/ponytail-statusline.ps1](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/hooks/ponytail-statusline.ps1) [tests/hooks-windows.test.js](https://github.com/DietrichGebert/ponytail/blob/e3ba2aa6f1e6f0bc4d69eb09c9f0d0a93af56156/tests/hooks-windows.test.js)

Source repository: https://github.com/DietrichGebert/ponytail
Full evidence corpus: https://github.com/Codevetter/codevetter/tree/main/benchmarks/repo-unpacks/ponytail.json

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
