Skip to content

Repository files navigation

cope

cope ships a card of writing instructions that is on the moment it is installed and scores every reply on two separate counts — how it sounds and how it is shaped — out of an .effigy file you can edit or swap for another; a cope is the upper half of a foundry mould, the half carrying the shape being cast into.

Every file under demo/ is this page written again from a different card, same prompt and same facts, so the card is the only thing that changed — demo/README.md indexes them, and reading two of them against each other shows what a card does faster than the rest of this page explains it. demo/README.claude-maximal.md makes the point in one glance, written from a card that instructs every tic this model is measured to have and hard going on purpose.

Voicing and structure

Voicing. What the sentences sound like — register, rhythm, diction, what a paragraph does with a detail, where flair is licensed. It lives in the card, entirely, so swapping the card swaps every word of it. A sentence reaching for the balanced two-beat is a voicing fault, and clause_symmetry is the rule that catches it.

Structure. The shape of the reply as a thing the reader has to use: where the decision sits, whether the ending gives continue something to refer to, whether an ask is last, whether a claim that the work is done carries anything that could have shown it. Compiled into the binary in internal/scan, the same rules whichever card is loaded. A reply that names an open problem in its last paragraph and then stops is a structural fault, and dangling_end is the rule that catches it. One reply can be clean on one axis and bad on the other, which is the reason to keep them apart.

Card reach into the structure half. Two directions, both written as one line in the card header. @gate <rule_id> off — <why> declines a built-in rule, which exists because a card whose VOICE block asked for something a built-in catches was marked down for obeying itself — card/demo/lecturer.effigy declines clause_symmetry and dangling_end on exactly that ground. @shape <id>: <selector> <predicate> — <why> asserts a structural rule of the card's own, which exists because a card's commitment about how a reply ends had nowhere to be checked — card/demo/handoff.effigy asserts readable_cold, last paragraph words <= 60, because its peak asks the reader to re-enter cold and read the last block only. That vocabulary counts words and sentences and asks whether a block poses a question, so a card wanting a check outside it and outside a POSTPROC regex has nowhere to put it.

The problem

A global CLAUDE.md. Not the system prompt, whatever a reader who has edited one and watched it not stick believes: it arrives as one message attached to the first turn, and the conversation buries it under everything written after. Compare an output style, which goes in the system prompt itself and which the harness re-reminds the model of as the conversation runs. Moving one card between those two places, without changing a word of it, is most of why cope works, and the run that showed it is in MEASUREMENTS.md.

The banned surface form. A global CLAUDE.md banning the construction that denies one thing in order to assert another is read every turn, and the construction still appeared twice in the session that built this, while the ban was the topic. Naming a surface form pushes the move into a variant. Voicing, all of it.

The ending a reader cannot answer. A reply that closes on an open problem and leaves continue nothing to refer to costs a whole round trip. Compare the flip, a phrasing habit an instruction can at least name; this one is not a phrasing habit, and no instruction reaches it.

The evidence. One flip in one session is an anecdote about one rule. The claim rests on the blind discrimination test, where a reader shown only a voice's own description of itself picks which of two replies was written under it, and MEASUREMENTS.md carries the rate and the caveats.

Install

go install github.com/justinstimatze/cope/cmd/cope-gate@latest
cope-gate --setup
# then: /config -> Output style -> claude_voice

cope-gate --setup. Emits the output style, wires the three hooks into ~/.claude/settings.json with absolute paths, and prints the one step left. It backs the settings file up first, adds only what is missing so a second run changes nothing, leaves every other key alone including other tools' hooks on the same events, and refuses to touch a settings file that does not parse — cope-gate --setup --dry-run prints what it would change and writes nothing.

The menu entry. claude_voice, under /config -> Output style, which is the shipped card's id and not the word cope, so a reader scanning that menu for cope will not find it. The standalone /output-style command was removed in Claude Code v2.1.91, so /config is the way; the same thing can be set as "outputStyle": "claude_voice" in .claude/settings.local.json.

cope-gate --output-style. Writes the loaded card to ~/.claude/output-styles/<card>.md for anyone who would rather not have their settings written to, and COPE_CARD=<name> in front of it emits a different one.

Style load time. A style is read once at session start, so a new selection or a re-emitted card applies at the next session or after /clear. An output style goes at the end of the system prompt and the harness re-reminds the model of it during the conversation, which is why the card lands here.

The hooks block of ~/.claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "cope-gate", "timeout": 10, "async": true }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          { "type": "command", "command": "cope-gate --refresher" }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "mcp__linear__save_comment|mcp__linear__save_document|mcp__linear__save_issue|mcp__linear__save_project|mcp__linear__save_status_update",
        "hooks": [
          { "type": "command", "command": "cope-gate --pretool", "timeout": 10 }
        ]
      }
    ]
  }
}

Hook payoff. Stop buys a score on the reply after it is written. UserPromptSubmit buys a mid-session restatement of the rules that have actually been firing, which a file written once cannot do. The voice works without either; these are the measurement half.

Two notes on the commands. They assume go install's target directory is on PATH in the environment the hook runs in, and a hook that silently does nothing usually wants the absolute path instead. A clone builds with make install and needs no effigy checkout, because card/rules.json is committed and compiled in.

--inject is the superseded delivery, kept for anyone who wants it, and it stands down on its own when a cope output style is active.

Another voice

.effigy, read directly. The gate parses the notation as written, so a card is usable as authored and needs no Python and no effigy checkout. Cards dropped in $XDG_CONFIG_HOME/cope/cards are reached by --card <name> or COPE_CARD; --rules takes a path from anywhere; make cards installs the demo set. A name that resolves to nothing is an error and nothing is injected.

card/demo/lecturer.effigy. The one to read first, since it differs from the shipped card on register alone, which makes it the rival the discrimination run measured. What a card changes is the voicing half described above, and the numbers are in MEASUREMENTS.md.

The shortest way to see what a card does without writing one is demo/README.md, where every file is this README written again from a different card, same prompt and same facts, so the voice is the only thing that varies between them.

card/demo/handoff.effigy is the exception in that directory: a hypothesis rather than a voice, keeping the shipped card's handoff rules and dropping everything about prose, and meant to be run through make pairs against the full card rather than rendered. make cards installs it with the rest, so a reader listing their cards will find it there and should know it is not a register to write in.

The hooks

Stop. Scores the reply just written and appends which rules fired to the session's rolling state, plus one record per violation to the log.

UserPromptSubmit. Reads the rolling state — not the violations log — and injects the card items gated on what has been firing, naming the counts. Falls back to the standing CONTINUE TEST when the session has no history yet, and stays quiet until the last injection has aged past --refresh-every.

Gated injection. Mid-session text chosen from measured output rather than fixed in advance, which a pasted CLAUDE.md cannot do. One mechanism, not a guarantee: the A/B in this repo does not separate the refresher from no refresher.

The notation, and the neighbours

effigy. A character-card notation for game NPCs, used here off-label, because three of its blocks do what a prose gate needs: POSTPROC is regex rules with a warn action applied after generation, WRONG holds an anti-pattern beside its replacement, and TEST holds a named question with fail and pass examples, which is how a rule names a move instead of one wording of it.

basanite. The same problem answered the other way round, and the one to reach for if this one is too blunt. cope bans: a rule fires or it does not, the card says never, and the register is fixed the moment you pick it. basanite measures instead — lemma frequency against a baseline over real transcripts, so it reports what you have been leaning on lately and leaves the judgement to you, which its own README calls awareness rather than prohibition. Which one fits is a question about mood more than about correctness: a heavy hand is what you want when a habit is annoying you today, a moving measurement when you would rather watch the drift than legislate it. They compose — different hooks, no shared state — and running both is reasonable.

humanizer. A skill, by a different author, that rewrites AI-sounding prose against 35 patterns taken from Wikipedia's "Signs of AI writing", the page WikiProject AI Cleanup maintains. Compare cope, which fires at a hook, scores what was already written, and edits nothing; humanizer is called on a text and hands back a rewrite, its pattern list is the wider one, and a reader who wants a rewrite rather than a score should go there. The formatting patterns are where the two disagree on purpose: cope's bold_label rule banned humanizer's bold mini-headings until 52 blind pairs put bold and bullets among the three things that decided a reply for this repo's reader, and the rule was deleted rather than tuned.

caveman. A separate project, by a different author, that compresses agent replies to cut output tokens — a fourth axis. cope shapes prose, basanite tracks vocabulary, humanizer rewrites, caveman shortens. A reader wanting fewer tokens rather than different structure goes there instead.

The rules

Voicing:

  • flip — the not-A-but-B flip in its common surface forms, including not-only-but-also and the inverted "A, not B". The Economist measured this family across 55,940 sentences on 2026-07-30 and put Claude at the top of it among every writer in the study, human or machine.
  • load_bearing — reflexive intensifier for important or central, at 25.6 per 1k the heaviest measured lean in this register. Say what the thing carries instead.
  • worth_noting — announces that something deserves attention instead of letting it earn the attention, at 6.5 per 1k.
  • clause_symmetry — comma- or semicolon-joined clauses of near-equal length that repeat a content word across the joint.
  • apology — the reply performs contrition instead of stating the correction and moving on.
  • self_postmortem — the reply turns to account for its own errors, which is a story the reader did not ask for.
  • announced_length — the reply announces its own length rather than cutting it.
  • cross_turn_repeat — a turn of phrase this reply shares with several earlier ones in the same session. The only rule that reads the window rather than the reply, so it cannot fire until a session has a history.
  • repeated_opening — three or more sentences in one reply opening on the same two words. Compare cross_turn_repeat, which reads the session window for a construction reused across turns; this one reads a reply against itself, and two is left alone because two is a rhythm.
  • fragment_run — three consecutive sentences of five words or fewer with no finite verb in any of them. One fragment is emphasis; a run of three is the staccato blind judges read as generated. Neither clipped demo card trips it, so neither declines it.
  • trailing_hedge — a complete clause with a short both/all/each/neither/either tag comma-appended to it, adding nothing the clause did not already have. A bare count before the tag is left alone, since "two bugs, both instructive:" previews an enumeration.

Structure:

  • labelled_opening — a prose paragraph opening on a short verbless fragment that the rest of it unpacks; an ordinal counts as the label. List blocks and paragraphs under twelve words are skipped, and so is the bolded form: the card dropped its bold_label rule in July 2026 after blind readers named bold and bullets as something they wanted, so an opener written as a bold label is deliberately unpoliced.
  • paragraph_uniformity — four or more prose paragraphs whose lengths have a coefficient of variation below --min-cv.
  • ask_not_last — a question or request for the reader sitting in an earlier block while the reply carries on past it.
  • dangling_end — an open problem named in the closing blocks with no question, offer, or explicit all-clear anywhere, leaving continue nothing to refer to.
  • buried_decision — an open problem landing after the last question or offer, burying the decision point above it.
  • forked_end — two or more things to act on in the closing blocks with nothing marking which comes first, so answering continue means picking one. Sentences opening on "or", questions inside list items and table cells, and bare deference tags like "your call" are read as continuing the decision above.
  • unverified_done — says the work is done with nothing on the page that could have shown it: no command, no count, no file.
  • loop_ask — an unattended run ends by asking, so the answer lands in a log and the next iteration reads the question as an instruction to itself.
  • echoed_heading — a heading of two or more content words whose first sentence below repeats every one of them.

Pattern and Go. A POSTPROC pattern matches a span of text, so it can only ever describe wording, and every voicing rule that needed more than a pattern had to be written in Go beside the structure rules. That is why the shipped card carries three of them. A reader expecting a long list of banned phrases should know the list lives in basanite on purpose.

Lanes. The structure rules vary in one way, and it is not by card: it is by who is going to read the turn.

  • interactive — any turn that is not a loop turn, because somebody is waiting at a terminal and the ending is where they decide what happens next.
  • loop — the prompt that opened the turn was /loop or /goal, or the sentinel a dynamic-pacing loop sends itself. Drops ask_not_last, buried_decision, dangling_end, forked_end, and adds unverified_done and loop_ask. Nobody is reading yet: a report that correctly names what it left open and stops would fail three of the dropped rules, and a question in it lands in a log where the next iteration reads it as an instruction to itself. What replaces them is the claim check — a report saying the work is done has to say what it ran.
  • external — the --pretool entry, scoring the description, body or content field an external write is about to post rather than a reply, matched against the Linear save tools named in the settings block. Warn-only: it returns additionalContext and never a permissionDecision, so the call goes through and the model learns what the prose scored, and it writes no session state. Drops ask_not_last, buried_decision, dangling_end, forked_end, short_close. A ticket has a reader and no ending they can answer, read days later by somebody who was not in the session, which is the condition every surviving rule was written for. short_close is the shipped card's own @shape rule rather than a built-in, and its premise is the same mismatch one step narrower — a short recap of a body the reader just read in the same turn — so a document's last section, being real content, drops it too. Nothing is swapped in, where the loop lane swapped four for two.

Clustering. Printed by the Stop hook, by --check, and by --pretool. Breadth fires on three or more distinct rules in a single paragraph. Density fires on one rule hitting the same block three times. Breadth wins when both hold, since naming three rules tells a reader to rewrite the block rather than hunt one construction. Every rule fires on its own and knows nothing about the others, so a report is otherwise a flat list a reader works down hit by hit — three hits across three paragraphs are three small edits, three inside one paragraph are one paragraph to write again. The density half has a measurement behind it. --check over 107 tracked documents produced 114 flip hits. Seven were worth changing, each one visible as three in a paragraph rather than as anything about the form. Three is the floor on both conditions, because two rules on a paragraph is ordinary and two hits of one is a coincidence a reader can see unaided. Nothing about what fires or how it is scored changes.

Flags

Flag Default Does
--ab false rotate refresher windows through each variant in turn, recording which one a turn was written under
--ab-arms (empty) comma-separated variants to rotate through, implying -ab (default inject,hold; positive is the third)
--ab-report (empty) read a turn log and report how each variant did; - reads the default path
--author-docs false print a prompt for writing this repo's docs: the card, the introspected facts, the sections
--backfill (empty) score every assistant turn in this transcript and exit
--block false exit 2 on a violation whose action is reject (default warn-only)
--card (empty) name of an installed card to write in, from the cards directory; also $COPE_CARD
--card-from-sample (empty) print a prompt for writing a card from this writing sample; - reads stdin
--cards false list the installed cards with the aim each one states, and exit
--check (empty) score a prose file against the card and exit; - reads stdin; comma-separated for more than one
--check-lane (empty) score -check in the given lane: interactive (default), loop, or external
--describe false print the card's voice as a target to recognise: the aim and the register, without the machinery
--display false MessageDisplay entry: rewrite what the reader sees, leaving the transcript alone
--display-preview false read prose on stdin and print it as --display would rewrite it
--dry-run false with --setup, print what would change and write nothing
--inject false print the card as prompt text for a SessionStart hook
--json false with -check, print one JSON object per line instead of prose
--log $HOME/.local/state/cope/violations.jsonl append violations here; empty disables
--min-cv 0.35 flag paragraph-length coefficient of variation below this
--output-style false write the card to ~/.claude/output-styles as a Claude Code output style, which puts it in the system prompt rather than in one turn-zero message
--output-style-dir (empty) directory to write the output style into (default ~/.claude/output-styles)
--pretool false PreToolUse entry: score the prose an external write is about to post, warn-only
--refresh-every 30m0s minimum age of the last card or refresher injection before the refresher fires
--refresher false UserPromptSubmit entry: inject the compact reminder once the last injection has aged
--render-arm (empty) print the mid-session reminder one variant would inject, and exit
--render-for (empty) comma-separated rule ids to render -render-arm against
--render-lane (empty) render -render-arm as the given lane sees it: interactive (default) or loop
--rules (empty) read the card from this .effigy or .json file instead of the one built into the binary
--setup false emit the output style and wire the hooks, then print the one step left
--version false print version and exit

State on disk

$XDG_STATE_HOME/cope/violations.jsonl, mode 0600. One JSON record per violation, carrying the matched text and about 70 characters either side. The log quotes replies back.

$XDG_STATE_HOME/cope/refresher-<session-id>, mode 0600. An empty file whose mtime is the refresher clock.

$XDG_STATE_HOME/cope/session-<session-id>.json, mode 0600. The rolling record the mid-session injection is chosen from: turn count, characters, and which rules fired over a 20-turn window. No prose is stored, only rule names and counts.

Editing the card

card/claude_voice.effigy. The shipped card, in effigy notation, with effigy owning the grammar. make rules regenerates card/rules.json, and make check-rules is what CI runs, so the enforced rules and the injected rules cannot drift.

The NEVER budget, 10. Anything over it is reported at load rather than dropped silently. The budget is charged against each injection separately and not against the card file, since the always-on rules and the evidence-gated ones print through different paths and no code path renders their union — so a card may hold more NEVER rules in total than the budget and still be healthy.

@gate <rule_id> off — <why>, one per line in the card header. The id has to be one the gate already has, and a wrong id is reported at load rather than ignored. A declined rule still runs, and only this card's score drops it, so a backfill still reports what it would have caught.

@shape <id>: <selector> <predicate> — <why>, one per line in the card header. The id must not collide with one the gate already has. Selectors: first paragraph, last paragraph, every paragraph, some paragraph, reply. Predicates: words <= N, words >= N, sentences <= N, sentences >= N, asks, does not ask. A violation is reported in the card's own words rather than in any sentence the binary supplies.

The reason after the dash is required in both forms, because a rule a card wrote and a rule a card refused are equally unreviewable without one.

Calibration

cope-gate --backfill. Scores a whole session transcript at once, and is how the rules were chosen. tools/backfill-sweep.sh runs it over the N largest transcripts found anywhere under ~/.claude/projects, which is not the same as one per project. Hits per character is the metric worth watching. Compare the share of turns hit, which tracks reply length instead. The rates are in MEASUREMENTS.md.

Limits

labelled_opening is not a tagger. It reads a short verbless opener and the paragraph that unpacks it, and it will miss forms a human reader would call the same move.

ask_not_last says nothing about the order of several asks. It knows one is early and the reply continued past it, and no more.

The hit rate is roughly four fifths structure, and the A/B run found that four fifths tracks what a reply was for rather than how it was written — a description of the output rather than a judgement of it. The judgement lives in the discrimination test, which covers voicing only.

The largest limit is the one the voicing-and-structure split above describes. A card can decline a built-in rule and can write one of its own, and the vocabulary it writes in counts words and sentences and asks whether a block poses a question, so the compiled rules remain the only place a check like clause_symmetry can live, and a card wanting something outside that vocabulary and outside a POSTPROC regex has nowhere to put it. Both directions are the card marking its own homework: a decline lowers that card's score, an assertion raises it, and both are worth reading with the reason attached, which is why the syntax requires one. MEASUREMENTS.md has what was run.

Layout

Path What
card/claude_voice.effigy the shipped card, in effigy notation
card/rules.json generated from it; embedded in the binary
card/demo/ other cards, each written to sound like something else
cmd/cope-gate/ the hook binary
internal/scan/ the structure rules, the card's regex rules, and the card renderer
internal/effigy/ the .effigy reader, so a card is usable as written
internal/transcript/ Claude Code JSONL reader, and which lane a turn was written in
replay/ the blind-pairs and discrimination harnesses, and their own README
demo/ this README written again under each demo card
tools/ card compiler, effigy-backed scorer, cross-project sweep
MEASUREMENTS.md what was run, on how much text, and what it said

Written by tools/generate_readme.py from the prompt cope-gate --author-docs emits, and checked with cope-gate --check.

MIT. justin@justinstimatze.com

About

load-bearing, load-bearing, load-bearing, load-bearing, load-bearing, load-bearing, load-bearing, load-bearing.

Topics

Resources

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages