← Blog

2026.10.08 · 12 min · ai · agents · javascript · craft

Does this code need to exist?

Ponytail is an agent skill that makes AI coders walk a seven-rung ladder before they write anything: does it need to exist, is it already here, does the language or the browser do it. The ladder works just as well without the tool. Here is each rung, with code.

Ask an AI agent for a date picker and you can get two very different diffs. In one, it builds a calendar and a picker by hand: 335 lines. In the other, it notices that the repo already has an Input component and that every browser ships a date picker, and puts the two together: one file, 10 lines. Same request, same model. The only difference is a prompt that tells the agent to ask a few questions before it types.

Watch the 60-second version on Instagram.

335 LINES10 LINESCALENDARPICKERINPUT TYPE=DATEFIG. 01 / TWO DIFFS, TO SCALE

That prompt is Ponytail, and the questions are what this post is about. I think they are worth more than the tool, because they work the same way when the one writing the code is you.

The laziest senior dev in the room

Ponytail is an MIT-licensed agent skill by Dietrich Gebert. Its own tagline sets the tone: the agent should "think like the laziest senior dev in the room". The README describes him as the developer with the long ponytail who looks at your fifty lines, says nothing and replaces them with one.

The repo appeared on GitHub on 2026.06.12 and had passed 160,000 stars by October. It installs into Claude Code as a plugin (/plugin marketplace add DietrichGebert/ponytail, then /plugin install ponytail@ponytail), into Codex the same way, and into the rest of the roughly 20 agents it supports (Cursor, Copilot, Gemini, Windsurf and others) by copying its AGENTS.md or SKILL.md into the project. Version 5.0.0 shipped on 2026.10.08, "rebuilt from the ground up": rules about half as long, a map of the existing codebase handed to the agent at session start, and a rebuilt /ponytail-review and /ponytail-audit.

Strip the packaging away and the whole thing is one prompt. Its core is a ladder.

The ladder

Before writing code, walk down seven questions in order and stop at the first one that holds:

1234567STOPWRITE CODEFIG. 02 / STOP AT THE FIRST RUNG THAT HOLDS
RungQuestionIf yes
1Does this need to exist?Skip it, and say what you skipped.
2Is it already in this codebase?Reuse the helper, component or service.
3Does the standard library do it?Use the language.
4Is it a native platform feature?Use the browser, the OS, the runtime.
5Does an installed dependency do it?Use what is already in package.json.
6Can it be one line?Write the one line.
7Only then: the minimum that works.Write it, plus one small test if it has logic.

That is the README's version. The SKILL.md that actually goes into the agent merges rungs 3 and 4 ("standard library or a platform feature") and adds a priority rule between them: a house component beats a native widget. If the project already wraps <input> in its own Input, use the wrapper. That is exactly what happened in figure 1: rung 2 and rung 4 together.

The order matters. Each rung is cheaper than the one below it. Code that doesn't exist costs nothing. Code that already exists has been reviewed, tested and debugged by someone. A standard API is maintained by a standards body and a few browser teams. A dependency you already ship adds nothing to the bundle. Only the last rung adds something new that you, specifically, now have to own.

Here is what each rung looks like in practice.

Rung 1: does it need to exist?

The ticket says "Add a Save button to the form." Here is what tends to come back:

button.tsx (asked)
type ButtonProps = {
  label: string;
  theme?: Theme;
  size?: Size;
  variant?: Variant;
  // ...six more options
};

Ten options, one of which the ticket needed. Each unused prop is a branch somebody has to read, test and keep working. The smallest complete version:

button.tsx (needed)
type ButtonProps = {
  label: string;
};

This is YAGNI ("you aren't gonna need it"), which Extreme Programming made famous long before agents. Ponytail's twist is the second half of the rule: skip the extras and name them in one line, so the person asking can say "actually, I do need size". Nothing is hidden. It just isn't built until someone asks.

Rung 1 also covers vague requests. "Build me a dashboard" gets the smallest version that does the core job, not a framework for dashboards.

I have been on the wrong side of this rule. When I built products, I used to take the generalised route, in case a customer wanted something more. The clearest example is a multi-tenant system I built for a product that didn't need one. Almost a year and a half later, it still runs a single tenant.

Rung 2: is it already in this codebase?

Before writing a date formatter, search for one:

grep -rn "formatDate" src
# src/utils/date.ts:3:export function formatDate(d: Date) { ...

If it is there, the new code is an import:

invoice.tsx
import { formatDate } from "@/utils/date";
 
const due = formatDate(invoice.dueDate);

The hand-written version would have been five lines, and worse than five lines: a second date format in the app that drifts from the first. Duplicated helpers are how a codebase ends up showing "08/10" on one screen and "10/8" on the next.

For humans this rung is mostly discipline. For agents it is a real gap, because an agent starts every session knowing nothing about your repo beyond what it reads. Ponytail 5's codebase map exists for this rung: at session start the agent gets a short inventory of what already exists, "so it reuses before it writes".

Rung 3: does the standard library do it?

JavaScript ships far more than it did when most of us learned it, and much of the helper code in older projects predates the built-ins that replace it. Grouping an array:

group.ts
// before: hand-rolled
const byRole = {};
for (const u of users) {
  byRole[u.role] ??= [];
  byRole[u.role].push(u);
}
 
// after: built in
const byRole = Object.groupBy(users, (u) => u.role);

A few more that retire whole helper files or dependencies:

Instead ofUseBaseline since
a deepClone() helper, or JSON.parse(JSON.stringify(x))structuredClone(x)2022.03
a "time ago" librarynew Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(-1, "day") → "yesterday"2020.09
a groupBy loop or lodash's groupByObject.groupBy(items, fn)2024.03

"Baseline" is the web-features project's label for a feature that works in every major browser; the date is when it first did. Check it before you choose a built-in, and read the fine print. Built-ins are not drop-in copies of the helpers they replace:

  • structuredClone throws a DataCloneError on functions and DOM nodes. It also keeps Date, Map, Set and circular references, which JSON round-tripping silently breaks.
  • Object.groupBy returns an object with a null prototype, so byRole.hasOwnProperty("admin") throws. Use Object.hasOwn(byRole, "admin") or "admin" in byRole.

Rung 4: is it a native platform feature?

The browser is a platform with its own component library, and it keeps growing. A modal is the classic case:

modal-lib.tsx
// npm i modal-lib
import Modal from "modal-lib";
 
<Modal isOpen={open} onClose={close}>…</Modal>;
dialog.html
<dialog id="confirm">…</dialog>
 
<script>
  document.getElementById("confirm").showModal();
</script>

showModal() puts the dialog in the top layer, above everything regardless of z-index. It makes the rest of the page inert, closes on Escape and gives you a ::backdrop to style. Those are the parts modal libraries mostly exist to get right. <dialog> has been Baseline since 2022.03. Others on the same shelf: <details> for disclosure widgets, <input type="date"> (figure 1), and the popover attribute for menus and tooltips, which is Baseline too but only since 2025.01, so check your support targets first.

The honest caveat: native is not always enough. <input type="date"> looks different in every browser and can only be styled so far. If the design calls for a calendar that looks identical everywhere, the native input does not answer the request, and rung 1 is about what was actually asked for. The ladder doesn't say "always use the platform". It says check the platform before you write your own.

Where the line falls depends on the control. I used to build time pickers from a text input and a stack of divs, and they were always trouble; a native <input type="time"> with some custom CSS has worked better every time. Date pickers are the opposite for me: the native one has never been enough, and I reach for a library.

Rung 5: does an installed dependency do it?

Open package.json before running npm install:

package.json
{
  "dependencies": {
    "date-fns": "^4.1.0",
    "react": "^19.0.0"
  }
}

date-fns is already there, so formatting a date is an import, not a second date library:

due-date.ts
// not: npm i dayjs
import { format } from "date-fns";
 
format(invoice.dueDate, "d MMM");

Two libraries for the same job means two APIs to learn, two sets of locale data in the bundle and two upgrade paths. The rule in SKILL.md is blunt about the inverse, too: "Never add a dependency for a few lines."

Rungs 6 and 7: only then, write

When all five rungs above come up empty, you write code. Rung 6 asks whether it fits in one line:

is-adult.ts
// before
function isAdult(user) {
  if (user.age >= 18) {
    return true;
  } else {
    return false;
  }
}
 
// after
const isAdult = (user) => user.age >= 18;

The rung says one line a reader gets at a glance. A chained, nested, regex-heavy one-liner that needs decoding is not short. It just has fewer line breaks.

Rung 7 is the minimum that works, and it comes with an obligation: if the code has a branch, a loop, a parser, money or security in it, it leaves one small test behind. Debounce is a good example of something that really reaches this rung. Neither JavaScript nor the browser has a native debounce, so if the codebase and the installed dependencies don't have one either, you write it:

debounce.ts
export const debounce = <A extends unknown[]>(fn: (...a: A) => void, ms: number) => {
  let t: ReturnType<typeof setTimeout>;
  return (...a: A) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
};
debounce.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { debounce } from "./debounce.ts";
 
test("calls once, with the last arguments", (t) => {
  t.mock.timers.enable({ apis: ["setTimeout"] });
  const fn = t.mock.fn();
  const d = debounce(fn, 100);
  d(1); d(2); d(3);
  t.mock.timers.tick(100);
  assert.equal(fn.mock.callCount(), 1);
  assert.deepEqual(fn.mock.calls[0].arguments, [3]);
});

Five lines of code, one test, no dependency, and both run on Node's built-in test runner. That is rung 7 done properly: still lazy about the solution, not lazy about the check.

Skip code, not care

The obvious objection is that "lazy" is a polite word for cutting corners. Ponytail answers it with a list of things that are never on the chopping block: validation at trust boundaries, error handling that prevents data loss, security, accessibility and anything the person explicitly asked for.

I think this list is the most important part of the whole idea, because it tells you what the ladder is actually measuring. It measures code, not care. A <dialog> is less code than a modal library and more accessible by default. Reusing formatDate is less code and one less place for a bug. When the smaller version would drop a check on user input, the smaller version is wrong, and the ladder doesn't ask you to take it.

The same goes for the tests. The project's own benchmark (below) found that the agent with the skill wrote about half the code and shipped tests for more of its risky logic than the agent without it. Fewer lines left room for the lines that matter.

Does it work on agents?

Ponytail's author publishes a benchmark for version 5: the same agent (Claude Code, Opus 5.5) with and without the skill, 39 tasks including 12 feature tickets in the open-source FastAPI + React full-stack template, 5 runs each, 585 sessions in total.

vs no skillPonytail 5
Lines of code−53%
Time−41%
Cost−26%
Output tokens−45%
Risky logic shipped with a test98% (no skill: 68%)
Hidden correctness checks passed (18 tasks)87/90 (no skill: 86/90)

Two things to read alongside the headline numbers. First, it is the author's own benchmark, run on one agent, with the shell disabled for every arm, so it measures what the agent writes, not how it debugs. The write-up is careful about this and publishes its method and limits. Second, correctness is level, not better: on the tasks with hidden checks, both arms passed about 96%. The fair summary is "half the code, no less correct", which is exactly what the ladder promises and no more.

I do use Ponytail, and the change I notice most is cost: on complex apps it has cut my token consumption significantly. Less code written means less for the agent to read back, review and revise.

The ladder without the tool

You don't need an agent to use any of this. The seven questions fit on a sticky note, and they are the same questions a good reviewer asks of a pull request: did we need this, didn't we already have it, doesn't the platform do it. Asking them before writing is just moving the review earlier, when it is cheapest.

What the tool adds is consistency. Agents are fast, eager and context-blind: left alone they reach for npm install or a fresh helper because writing new code is the path of least resistance for them. A prompt that forces the questions every time makes the cheap answers show up first. The same is true of people on a deadline, which is most of us.

The short version

  1. Before writing code, walk the ladder: need → codebase → standard library → platform → installed dependency → one line → minimum.
  2. Stop at the first rung that holds. Every rung above the last one is cheaper to own.
  3. Skip features, but name what you skipped.
  4. Check Baseline and the fine print before trusting a built-in.
  5. Code with logic leaves one small test.
  6. Skip code, never care: validation, security, accessibility and data safety stay.

Sources: Ponytail on GitHub (README) · Ponytail SKILL.md · Ponytail v5.0.0 release · Ponytail 5 agentic benchmark, method and limits · GitHub API, repository metadata · MDN, structuredClone() · MDN, Object.groupBy() · MDN, Intl.RelativeTimeFormat · MDN, <dialog> · web-features (Baseline data) · Node.js test runner, mocking timers · YAGNI