Ponytail: A Ruleset That Argues With Your AI Agent Before It Over-Builds

June 29, 2026

|repo-review

by Florian Narr

Ponytail: A Ruleset That Argues With Your AI Agent Before It Over-Builds

What it does

Ponytail is a Claude Code / Codex / Copilot plugin (and a static ruleset for a dozen other agent hosts) that forces coding agents through a fixed decision ladder before they write anything: does this need to exist, does it already exist in the codebase, does stdlib do it, does the platform do it, does an installed dependency do it, can it be one line — only then write code. 106k stars, two months old as of this writing.

Why I starred it

Every agent session I run has the same failure mode: ask for a date input, get a flatpickr install, a wrapper component, and a stylesheet. Ponytail's own before/after example is exactly that bug — the fix is <input type="date">. What made me actually read the code instead of skimming the README was the claim that it works across 20 different agent runtimes with wildly different plugin systems, and that it re-injects itself into subagents, which is the part that usually breaks.

How it works

The core abstraction isn't code, it's a markdown file: skills/ponytail/SKILL.md. That's the actual ruleset — the seven-rung ladder, the intensity table (lite/full/ultra), worked examples. hooks/ponytail-instructions.js:77 reads it on every SessionStart and slices it down to the active mode with filterSkillBodyForMode():

// hooks/ponytail-instructions.js:19-40
return withoutFrontmatter
  .split(/\r?\n/)
  .filter((line) => {
    const tableLabel = line.match(/^\|\s*\*\*(.+?)\*\*\s*\|/);
    if (tableLabel) {
      const labelMode = normalizeMode(tableLabel[1].trim());
      if (labelMode) return labelMode === effectiveMode;
    }
    const exampleLabel = line.match(/^-\s*([^:]+):\s*"/);
    if (exampleLabel) {
      const labelMode = normalizeMode(exampleLabel[1].trim());
      if (labelMode) return labelMode === effectiveMode;
    }
    return true;
  })
  .join('\n');

That's a line-filter over the skill's own markdown, keyed off two very specific shapes: intensity-table rows (| **full** | ... |) and quoted worked examples (- full: "..."). The comment above it in the source explains why the regex is that narrow — an ordinary bullet that happens to start with "Full:" as prose would get silently dropped in every other mode if the match were looser. Small detail, but it's the kind of edge case you only find after someone filed a bug about it.

The problem ponytail actually had to solve for real is issue #252: SessionStart context in Claude Code is parent-thread only, so a rule injected at session start never reaches a Task-spawned subagent — every subagent runs ponytail-unaware. hooks/ponytail-subagent.js fixes this by hooking SubagentStart directly and re-injecting the same ruleset. It also ships an opt-in scoping mechanism, PONYTAIL_SUBAGENT_MATCHER, an env-var regex tested against agent_type so you can exclude read-only search agents from getting the persona — and it fails open on purpose: bad regex, missing agent_type, stdin timeout, all default to injecting rather than silently dropping the ruleset (hooks/ponytail-subagent.js:50-71).

The other real engineering is writeHookOutput() in hooks/ponytail-runtime.js:51-90, which normalizes output across four incompatible host protocols in one function — Copilot wants { additionalContext } only on SessionStart and ignores everything else; Codex wants a systemMessage plus hookSpecificOutput; Qoder wants hookSpecificOutput without the systemMessage; native Claude wants raw stdout for SessionStart but the wrapped JSON form for SubagentStart, or the context gets silently dropped. Four if branches, four different JSON shapes, one shared code path. That's the unglamorous work that actually makes "works with 20 agents" true instead of a README claim.

One more thing worth calling out: isShellSafe() in hooks/ponytail-config.js:50-52 is a one-line regex allowlist (/^[A-Za-z0-9 _.\-:/\\~]+$/) gating whether the statusline setup nudge embeds the plugin's own install path into a shell command string. If the path has shell metacharacters, it falls back to telling the agent to wire the statusline up by hand instead of building an unsafe command string. Small, but it's the kind of check that's easy to skip and only bites someone with an unusual install path.

Using it

/plugin marketplace add DietrichGebert/ponytail
/plugin install ponytail@ponytail

Two separate prompts — the README is explicit that combining them in one message doesn't work, which reads like a lesson learned from an issue thread. Once active, it's ambient — no per-request flag needed:

> Add a cache for these API responses.

`@lru_cache(maxsize=1000)` on the fetch function.
 skipped: custom cache class, add when lru_cache measurably falls short.

/ponytail ultra drops the hedging further — same request gets "no cache until a profiler says so." Turning it off is a plain-English stop ponytail, tracked by hooks/ponytail-mode-tracker.js, which also handles the one persistence subtlety in the whole system: /ponytail default ultra writes to ~/.config/ponytail/config.json and survives restarts, while a bare /ponytail ultra is session-scoped only — the code comment calls out that review is deliberately excluded from what can be set as a default (issue #377).

Rough edges

The correctness story is more honest than most benchmark repos I've read. The initial "80–94% less code" number was a single-shot comparison; issue #126 pointed out the no-skill baseline pads its answer with prose, so the gap was partly a conversational artifact. Rather than delete the old number, they kept it in a collapsed <details> block with the caveat spelled out and replaced the headline claim with an agentic benchmark — real Claude Code sessions editing a FastAPI+React template, 12 tasks, n=4 on Haiku 4.5, scored on the actual git diff. Final numbers: -54% LOC, -22% tokens, -20% cost, -27% time, and a separate adversarial safety tier where ponytail stays at 100% while a bare "write one-liners" prompt drops to 95%. benchmarks/correctness.js backs the safety claim by actually executing generated code against test fixtures rather than just counting lines — the CSV-sum checker spins up Python, patches the file path, and runs it.

Test suite is node:test only, no framework, true to its own philosophy: 84 tests across 15 files. I ran it locally and got 83 passing; the one failure was the pandas-dependent CSV checker, because I hadn't installed pandas — not a repo bug, just an undocumented test dependency worth knowing about before you assume npm test is fully self-contained.

Git history is younger than the star count suggests — 210 commits since June 12, 2026, one dominant author (DietrichGebert, 77 commits) plus a long tail of one- and two-commit contributors. That's a healthy open-source curve for two months old, but it also means the "works with 20 agent hosts" surface area is maintained by essentially one person triaging community PRs, which is a lot of adapters to keep in sync as each host's plugin API shifts.

Bottom line

If your agent workflows keep drifting toward unrequested abstractions, this is a well-engineered way to push back — not because the prompt is clever (it's a well-written markdown ruleset, nothing more), but because the plumbing that keeps it alive across subagents and a dozen incompatible hook protocols is done properly. Worth installing if you run Claude Code subagents specifically; the SubagentStart fix alone solves a real gap most similar "persona" plugins don't handle.

DietrichGebert/ponytail on GitHub
DietrichGebert/ponytail