DietrichGebert/ponytail
github.com/DietrichGebert/ponytail ↗ · 159 files · 758 KB
Ponytail He says nothing. He writes one line.
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.
- 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).
- 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.
- 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.
- 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.
- 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.
- 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'.
- 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.
- 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.
- 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.
- 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.
- 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.
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.
- 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.
- 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.
- 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.
- 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.
- 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).
- 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.
- 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.
- 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.
- 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.
- 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.
- 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/.
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>'.
- 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).
- 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.
- 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.
- 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).
- 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.
- 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.
- 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.
- 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.
- 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').
- 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.
- 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.
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).
- 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.
- 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.
- 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.
- 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).
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
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`.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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)
- 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)
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.
- 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.
- 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.
- 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.
- 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).
- 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.
- 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)
- 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.
- 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.
- 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)
- 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)
- 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.
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'.
- 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').
- 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.
- 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.
- 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.
- 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.
- 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.
- 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).
- 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.
- 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.
- 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.
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.
- 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.
- 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.
- 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).
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
Languages
| Language | Files | Share of source |
|---|---|---|
| Markdown | 57 | docs/config |
| JavaScript | 48 | 79% |
| JSON | 20 | docs/config |
| YAML | 9 | docs/config |
| Python | 6 | 10% |
| TOML | 6 | 10% |
| Shell | 1 | 2% |
Start reading here
Probable source entrypoint: hooks/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
hooks/ (12 files) assets/ (7 files) skills/ (6 files)
Source layout — hooks/
| Path | Files |
|---|---|
hooks/claude-codex-hooks.json | 1 |
hooks/copilot-hooks.json | 1 |
hooks/cursor-hooks.json | 1 |
hooks/ponytail-activate.js | 1 |
hooks/ponytail-config.js | 1 |
hooks/ponytail-instructions.js | 1 |
hooks/ponytail-mode-tracker.js | 1 |
hooks/ponytail-runtime.js | 1 |
hooks/ponytail-statusline.ps1 | 1 |
hooks/ponytail-statusline.sh | 1 |
hooks/ponytail-subagent.js | 1 |
hooks/qoder-hooks.json | 1 |
Repository structure
| Top-level path | Files |
|---|---|
benchmarks/ | 35 |
tests/ | 16 |
hooks/ | 12 |
examples/ | 12 |
.opencode/ | 8 |
assets/ | 7 |
scripts/ | 6 |
.openclaw/ | 6 |
Docs found: AGENTS.mdREADME.mddocs/agent-portability.mddocs/cursor-hooks.mddocs/platform-native.md
Package
package.json — @dietrichgebert/ponytail v4.10.0, 0 declared dependencies.
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructions.env.example— Required env vars.github/workflows/publish.yml— GitHub Actions workflow.github/workflows/test.yml— GitHub Actions workflowpackage.json— npm scripts: testpi-extension/package.json— npm scripts: testponytail-mcp/package.json— npm scripts: test
Structural health signals
Average score 8.4/10 across 35 analyzed source files; 4 hotspots surfaced.
benchmarks/agentic/run.py— 471 lines, score 5.2 (Large file, Deep nesting)benchmarks/agentic/tasks.py— 951 lines, score 5.4 (Large file, Deep nesting)benchmarks/robustness-audit.js— 205 lines, score 5.9 (Deep nesting, High churn)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
e3ba2aa6f1e62026-09-14 — chore: release v4.10.0 (#870)67a16015536e2026-09-14 — feat: add native Cursor hooks via hooks.json (#817) (#869)356918eba9652026-09-07 — docs: rename the Retriever logo files so GitHub serves the new version846fd86ff1e52026-09-07 — docs: Retriever logo with the name below the icon57f759d7ffbf2026-09-07 — docs: Retriever as one logo image, icon and name together, dark and light