Anti-Cliché: I used to spend more time fixing than writing

I have used Cursor for a couple of years. I started on Tab. When I moved to agents, a chunk of my time went to reviewing and fixing what they generated. I built Anti-Cliché.

I have used Cursor for a couple of years. At first I wanted one thing: ship faster, mostly with Tab. It finishes the line and you keep going.

I did not trust the chat much. The few times I used it, it made too many mistakes or brought patterns that were not this project's. I kept writing the code myself. Tab as a copilot.

The months went by. The models got better, the tools too, and the market pushes you onto agents. Not only because they work better. But because expectations went up: how much code you ship, how much you plan, how far you get in the same stretch.

At some point finishing a few lines was not enough. You had to hand it whole tasks. More autonomy. That is what I started doing.

There was a problem. Even as the models improved, and even with rules, skills, and project context, the code they sent back still had strange things in it.

A few months ago, in a code review, I found this:

tsx
function preventDefault(event: { preventDefault: () => void }) {
  event.preventDefault();
}

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
  preventDefault(event);
  createUser(formData);
}

const form = (
  <form onSubmit={handleSubmit}>
    <input name="email" type="email" required />
    <button type="submit">Create account</button>
  </form>
);

return <section>{form}</section>;

At a glance it looked fine (Knip, typecheck, ESLint). When you saw what it was actually doing, that function was not needed: a one-line wrapper around preventDefault, the form in a const.

That example made me look harder at what my own agent was writing. It was not a one-off. Decisions that work, but do not respect the conventions, add complexity you did not ask for, or are not what I would have written.

An uncomfortable question showed up. Was I actually faster, or had I just swapped writing time for time spent reviewing and fixing what the model generates? A real chunk of my time went there: review, fix, explain the context again, and ask whether that task should have been delegated at all.

I even thought about going back to Tab. Write more by hand. Have the control again. Before I closed that: maybe the problem was not the agent. Maybe I was using it wrong.

That is when I started looking into how to actually work with code agents: context, rules, skills, the project's knowledge. Not only code that runs. Code that makes sense here. Each time I did not want to see the same pattern, I turned it into a rule. That does not live in a PR comment. It lives in Anti-Cliché. This note is the why. What the agent has to load is the skill.

The fatigue was not about speed

The problem is not that the agent exists. The problem is the average it spits out when nobody draws a border. Claude, Cursor, Anthropic’s models: the brand does not matter. They are already the job.

That average is not just one absurd function. In architecture: a UserService class only to go fetch data. In structure: a <Box> that only puts things in a row, an index.ts that re-exports the whole directory, "use client" copied from habit. And especially in the interface. The purple gradient, the Inter typeface, three icon cards, frosted glass, the sparkles button. Also the obvious tell: numbered lists that are not steps, section_title with an underscore on every title, a rule between every block, code type on text that is not code. You see it and you already know which model built it.

Knip tells you which file nobody uses anymore. Here the file is used. It lands in the project. TypeScript passes. The linter passes. Those tools say the code is well-formed, not that it is useful. It is still the 2024 default pasted into a 2026 project.

useMemo around a piece of text. A try/catch that prints the error to the console and returns null. A STYLES object so the class does not sit next to the HTML. None of that breaks the build. All of it is the dialect.

I got tired of saying the same thing in every review. “Do not extract this.” “The form already does this.” “This does not need a wrapper.” “This looks like a Cursor landing.” The model does not take offense. The model also does not remember the last review unless you leave it in a file it will read again.

What an AI cliché is (and is not)

A cliché is not “code I dislike.” It is a pattern the model picks because it appeared a thousand times in training, not because it solves the problem in this file.

The preventDefault helper is the clearest example I have. The model learned that a submit calls e.preventDefault() so the browser does not reload. Then it learned “extract repetition.” It glued the two lessons and shipped a one-line function. It looks careful. In practice it adds nothing that was not already on submit.

The principle in Anti-Cliché fits in one sentence: every banned pattern substitutes form for content, or complexity for value. The fix is not “write cleaner.” It is to be specific, respect what already exists in the project, and use what the platform already does (React 19, App Router, Tailwind in the HTML).

Same mechanism in the interface: purple gradient, 3D blob, “Generate” button with sparkles, gray text on white because it “looks clean.” Numbered lists that are not steps. Underscores. Extra rules. Code type where there is no code. Same in prose: delve, tapestry, landscape, pivotal, the jaw that tightens, the light that spills. Different places, same mechanism.

I am not fighting React. I am fighting the reflex to wrap a native call and give it a system name.

Constants for everything, including JSX

The const form did not arrive alone. The model extracts to feel organized, not to name a concept. The same reflex, one step further back, is the constants file.

It starts harmless: CARD_PADDING = 'p-6'. Then a STYLES object that is a parallel stylesheet. wrapper, header, body, footer — each key is a Tailwind class that already describes itself. Now, to see how it looks, you jump to another file. The editor does not follow the key. And three weeks later you no longer remember what STYLES.hint is.

ts
const STYLES = {
  wrapper: 'flex flex-col gap-4 rounded-lg border p-6',
  title: 'text-lg font-semibold',
  hint: 'text-sm text-muted-foreground',
};

<div className={STYLES.wrapper}>
  <h2 className={STYLES.title}>{title}</h2>
  <p className={STYLES.hint}>{hint}</p>
</div>

That does not add meaning. It renames. A class that never changes is written next to the HTML. cn() exists for what changes by case, not to paste two fixed strings. CVA exists for real variants, not to hide p-6.

If it behaves like a component —it has HTML, a role, it will grow or repeat— it is declared as a component. With function. Not const Icon = () =>. Not JSX stuffed in a variable and pasted later.

The criterion is short. Class that does not vary: next to the HTML. Domain number or text used in three places: a constant named after the domain. A chunk of interface with a face and a name: function. If extracting only puts an alias on it, do not extract. The model thinks const is hygiene. On the page, it is a half-component.

What the skill actually does

Anti-Cliché is not the review anecdote. It is the protocol the agent runs before writing. References by topic: architecture, components, server, Tailwind, interface, prose.

I started with editor rules. Then a longer document. Then cuts, because kilometer-long rules dilute. I ended on a skill that is not perfect. But the first draft arrives without the empty helper, without JSX in a const, without the interface you recognize from a mile away.

That does not close it. Every new cliché in a diff (what changed in the file) goes in. If the average moves from purple to cream + serif, the skill moves too. The criterion does not change: if nobody chose it, it does not ship.

The skill does not say “write nicely.” It says the question. Does this already exist in the project? Is it used more than once? Does this constant add meaning, or just name a string of text? Is "use client" here because the browser needs it, or from habit? The rest (React 19, the index.ts that re-exports everything, Result, sparkles, delve) lives in the file the agent loads. This note does not replace that text.

None of those rules is original as an idea. The value is having them together, before the agent touches the project, not in a PR comment that arrived late.

Turning it into a gate

A skill that is only read when someone remembers is useless. Same rule as Git Auditor and Knip: if it matters, it runs without being asked.

The canonical text is published. It installs like this:

bash
npx skills add enderpuentes/ai-agent-skills --skill anti-cliche-agent

The pointer on this site is the Anti-Cliché resource. This note does not replace that text. If the article and the skill disagree, the skill wins.

In practice, in my projects, Anti-Cliché sits in the repo (ts, tsx, and md files), the agent loads it on interface and review work, and the human pass stops repeating “this is an empty wrapper.” It does not replace TypeScript. It does not replace Knip. It cuts the dialect before it enters the change.

What it does not do

It does not format. It does not argue about indentation. It does not lock you to a framework. If the project already has a pattern, the skill says respect it — inventing a “cleaner” parallel is also a cliché.

It is also not an automatic detector in CI (the check that runs when you push changes). It is instruction for the model, not a program that inspects the code. If you want the build to fail, that is ESLint or a test. I wanted the first draft not to arrive with the dialect.

Close

I used to spend more time fixing than writing. Not anymore. Anti-Cliché exists because I got tired of returning that average by hand, and because the catalog does not finish: every new cliché goes in.

Without that border, I go back to Tab. With it, the agent writes.

If the agent is going to write faster than you, someone has to name the clichés. I wrote them down: this note for the why, the skill for the protocol. Load the second. When the next cliché shows up, it goes in there too.

More notes on AI Engineering.