Claude Code plugin

OPUS BUT GOOD

Stops Claude inventing shorthand for its shorthand.

Deterministic linter · no local model · no network · ~20ms per chunk

The problem

Opus 5 writes like it is compressing notes for itself and forgetting anyone else has to read them. It coins a term, then coins a shorter term for that term, and three sentences later you are maintaining a glossary of words that exist nowhere but this conversation.

what it says The registry row had five layers of superseded state, so I rewrote it canonical-truth-first. The two-field change set is done and the blank-means-ours wording now hangs off the caller. what it meant I replaced the status notes with what is actually true now. The caller adds the name when the field is blank.

Every invented noun is a debt the reader pays. You cannot look up blank-means-ours. It was made up forty tokens ago.

What it catches

Plus binary flips (“it's not X, it's Y”), pseudo-technical hedging (modulo, non-trivial, by construction), and the standard AI vocabulary (seamless, robust, leverage, utilize, delve).

What you see

Nothing, when the reply is clean. When it trips, one line at the bottom:

The seam here is load-bearing, so I rewrote it canonical-truth-first. Fair pushback. ⌜ jargon 12 · canonical-truth-first · fair pushback · load-bearing · seam · /plain to redo this in English

The label says jargon rather than a clever name, because a plugin arguing “stop inventing vocabulary” should not open by inventing vocabulary.

How it works

Three parts, and the third feeds the second. That loop is the whole idea.

1
An output style Sets the register up front. Rules about vocabulary, coining terms, being concrete, and not performing.
2
A UserPromptSubmit hook Puts the rules back in front of the model on every turn, immediately before it writes.
3
A MessageDisplay hook Scores what actually came out and appends the receipt line. Then hands the score back to step 2.
step 3 → step 2  ·  the loop closes here

Clean replies get no reminder at all, because context is not free. Once the linter catches something, the next injection names the exact words and their counts. Keep drifting and it gets blunter. Stop and it goes quiet again.

Why putting it in CLAUDE.md doesn't work

This is the part people get stuck on, and it is not a prompting skill issue.

CLAUDE.md is read once, at the top of the session. Forty exchanges later it sits under a hundred thousand tokens of transcript, competing with everything that arrived after it. The instruction is still technically in context. It is just no longer the loudest thing there.

A UserPromptSubmit hook injects the same words immediately before the model writes, every single turn. Same rules, different position, completely different result.

Observed live. After one flagged reply, the model was told to avoid load-bearing, seam and fair pushback. Asked on the next turn what it had been warned about, it answered: “the one about a joint bearing weight, the one about a joint between two parts, and the two-word concession about your objection being fair.”

It would not reuse the words even to name them.

Precision over recall, on purpose

The first version flagged any hyphenated compound as coinage. That caught canonical-truth-first. It also caught first-party, logged-in, click-through and back-button.

Measured against a corpus of real human writing, it scored 66 on a document written entirely by hand. A linter that noisy gets switched off within a day, which makes it worth exactly nothing.

Now only compounds of three or more parts count as coined. Two-word compounds are ordinary English, and named offenders like load-bearing are listed explicitly in the lexicon instead, where they can be exact.

~1hits per 1,000 characters of human prose
~78hits per 1,000 characters of real Opus jargon
0false positives across 12 hand-written blog posts
40/40tests passing, including 10 fail-open checks

Code fences, inline code, URLs, file paths and markdown image alt text are stripped before scanning. A variable named seam is not a writing problem.

What it will not catch

The original example, the two-field change set, is not caught. It is a two-word compound, and two-word compounds are ordinary English.

What made that phrase bad was never its shape. It was that it named something the reader had never been told about. No regular expression can see that. That half is the output style's job, and the output style is a nudge, not a guarantee.

The honest risk. Banning words can produce circumlocution instead of clarity. A model told never to write load-bearing may write the joint bearing weight, which is worse. If replies start reading like a hostage note, soften the correction from “never use these” to “prefer the plain word.”

It cannot eat your output

Both hooks fail open. On any problem at all, malformed JSON, a missing lexicon, an unwritable cache, a bug in the plugin itself, they print nothing and exit 0, which leaves the original text on screen untouched.

A display hook that can swallow an answer is worse than no display hook, so ten of the forty tests exist only to prove this.

Nothing leaves the machine. There is no model call and no network request. The scanner is regular expressions over the finished message, which is why it costs about 20 milliseconds instead of the 45 seconds a local model needs.

Install

claude plugin marketplace add <path-to-repo>
claude plugin install opus-but-good@opus-but-good-marketplace

Then turn on the output style in /config under Output style. The hooks work without it; the style is the optional half.

Commands

CommandDoes
/plainSay that last answer again in plain English. Keeps every fact, expands every coined term.
/obgSession report. How many replies were flagged, which words came up most.

Modes

OBG_MODEBehaviour
flagDefault. Streams normally, one receipt line when it trips. Silent when clean.
stripApplies the swap table and shows the cleaned text. No added latency, since the swaps are regex.
silentScores everything, shows nothing. The loop still runs.
offNothing at all.

Settings

VariableDefaultMeaning
OBG_ENABLED1Master switch.
OBG_LABELjargonThe word on the receipt line.
OBG_MIN_SCORE2Receipt threshold.
OBG_INJECT1Set 0 to keep the linter but stop injecting rules.
OBG_LEXICONbundledPoint at your own word list.

Everything is tunable in lexicon.json. Each group has an on/off flag and a weight. One group ships disabled because its words are also real words: contract, primitive, harness, canonical, invariant, plumbing.

Built because the complaint is real and widely shared: r/ClaudeCode, “Opus 5 is exhausting”.

No tracking, no analytics, no fonts loaded from anywhere.