'No X, but Y' and Other Tells: Wiring an LLM to Write in Your Voice
How to use LLMs as part of your writing workflow.
It seems that nowadays every other article, tweet, or in the case of LinkedIn posts every single one of them, is written by a language model. Some of them are more refined, better structured, others are pure sloppy garbage. But the common thread is that they all have a fingerprint, a tell, that makes them read like a machine generated them.
This uncanny valley of writing is a problem both for the reader and for the writer. No one wants to read slop; it is jarring and it is a waste of time. And no one wants to write slop, but it's undeniable that we cannot put the genie back in the bottle. LLMs are useful tools, for research, for drafting, for ideation. But if you want to write in your own voice, this is trickier than you might expect.
Why you'd want a model in your voice at all
Now we can debate whether we should be using models to write at all, but I don't think that is a very useful question; at least not until we figure out how closely a model can write in your voice.
Writing has two modalities: we write to think, and we write to communicate. The first is a private act; the second is a public one.
Writing to think is a tool for reasoning, and you can't replace it with a model — or arguably with a computer at all. There are merits to writing by hand, and to being deliberately slow about it.
Writing, to me, is simply thinking through my fingers.
Writing to communicate is a tool for sharing, and something a model can help with — and, provided we have the right ideas, almost automate.
I write a lot under my own name — this blog, a newsletter (thepragmaticcto.com), two books (third one in the works), whatever ends up on LinkedIn and other social media networks. The thinking is the part I want to spend my time and energy on; even before getting to the first draft, building and shaping the core idea takes time, energy, and human creativity.
If the ideas are mine, does it matter who phrased them? It does. They say code is cheap now; if code is cheap, then prose is essentially free. But words and semi-coherent sentences don't convey voice. Voice is part of that thinking, not decoration on top of it. Putting your name on something is a promise about who did the thinking, and prose that reads like slop breaks the promise even if the piece is factually correct.
So how do you take advantage of a model's ability to generate prose while keeping the ideas and the voice intact?
Since February, I've been trying different techniques, approaches all from prompting to setting up rules, to trying different models, to building a system that can catch the tells and keep the voice intact. The goal is to have a model that can write in my voice, but without the slop that comes from the model's own fingerprint and without losing the ideas that are mine.
Five months later, and after a mix of success and failures, I'm getting close to a system that I consider to be close enough, and that can help me do a lot of the heavy lifting in writing while keeping the voice intact. The system is not perfect, but it is a step in the right direction.
What I have now is a combination of claude skills and references, Vale rules, and a correction log that together form a system that can catch the tells and keep the voice intact.
It starts with the outline
Ask a model for an article and it hands you an article: fluent, organized, correctly punctuated, and about nothing in particular. It has no stake in the topic, so it averages — it writes the piece sitting at the dead center of everything it knows on the subject. This makes for fairly uninteresting and mediocre articles.
Starting with the outline is not new and it's a good practice regardless if you are using an LLM or writing on your own. As part of most of my writing workflows I have a mechanical step that maps out arguments and key points. No writing yet just research and planning.
I have turned this into a Claude code workflow named /topic-outline which takes a topic as well as any context and rough framing I give it and returns a content outline.
export const meta = {
name: 'topic-outline',
phases: [
{ title: 'Research',
detail: 'parallel agents sweep the topic — APIs/code/pitfalls' },
{ title: 'Validate',
detail: 'adversarially verify findings; drop the unverifiable' },
{ title: 'Outline',
detail: 'synthesize into a CONTENT OUTLINE — thesis + skeletons' },
{ title: 'Review',
detail: 'archetype critic checks structure/depth; revise on fail' },
],
}
Four research agents run in parallel, each pointed at a different lens and approach to the topic. By design they don't see each other's work, I do this to prevent the "groupthink" effect where a model amplifies its own reasoning by seeing it reflected back. Each agent returns findings that are then merged into a single set of unique results.
Returning a bunch of research is not enough. Every finding goes to its own fact-checker, in its own context, and the checker is told to assume the finding is wrong:
agent(`You are a fact-checker. Adversarially verify this finding
against its source. Assume it is wrong until the source proves
otherwise.
CLAIM: ${f.claim}
API: ${f.api}
SOURCE URL: ${f.source_url}
QUOTED AS: "${f.exact_quote}"
Confirm the API/behavior is REAL and CURRENT: the function exists
with that arity and options, behaves as described, and is not
deprecated or renamed. Mark verified=true ONLY if confirmed.
Default to verified=false when uncertain.`,
{ schema: VERDICT_SCHEMA })
verified=false when uncertain is doing the real work in that prompt. An unreachable source or a half-remembered function signature drops out silently, and anything coming back low confidence drops with it. I call this the bullshit filter, and a good way to catch hallucinations early on.
The result is a content outline with a thesis line, then sections that each carry a structure skeleton drawn only from verified facts, the flow beats, and any caveats.
There is archetype review, and this tends to be the most flaky part of the workflow. This particular blog's "house style" is the code-first deep-dive, so technical is the default and opinion — the citation-driven analysis essay is the minority. My interests, and the focus technical vs opinion, do change so the archetype review is more a soft check to make sure the outline is not a melange of two different archetypes.
The model writes a rough draft, and nothing more
When I started working on this workflow, I would do a lot of manual editing to the outlines and more fine-tuning, now I find doing that a lot less, in part because the system keeps getting better and in part because it's easier to make changes later in the process than trying to get everything perfect in the outline. The outline is a guide, not a contract.
Perfect is the enemy of done.
With outline in hand, we can move to the draft stage. I let the model write a rough draft, and nothing more. The draft is not a finished article, it is a starting point, it is not meant to be perfect; and of course it is instructed to write in my voice, but it can only get so close.
/draft-article takes an outline and runs three phases: draft, audit, revise. The writing itself goes to macgregor-tech-writer, a subagent whose system prompt carries the archetype, and which loads three reference files before it writes a word:
const REFS = `Obey Allan MacGregor's voice guidelines — read them first:
- ${PRIV}/writing_meta_prompt.md // the mechanical process
- ${PRIV}/voice_dna_analysis.md // the voice fingerprint
- ${PRIV}/ai_writting_patterns_reference.md // forbidden slop patterns`
Voice cloning, in practice, is far less mystical than the phrase suggests. Nothing in those files says "write conversationally"; they're mechanical and countable, because countable is the only kind of instruction a model reliably follows. Sentence length varies from three words to thirty-plus, semicolons run about one per 150 words, and fragments are emphasis beats capped at two or three in a row. The meta prompt goes as far as asking for a rhythm map before any prose exists:
Section: "We've Confused Generation With Understanding"
Sent 1: 18 words - opening observation with semicolon
Sent 2: 4 words - fragment
Sent 3: 4 words - fragment
Sent 4: 3 words - puncher
Sent 5: 25 words w/ semicolon - contrast statement
Sent 6: 22 words - elaboration
Plan the rhythm, then write to it. It reads like overkill until you watch what happens without it: under load — a complex topic, plus data, plus an argument to hold together — the model regresses to its base training and every sentence lands between twelve and eighteen words. The prose goes flat in a way that's hard to name and impossible to miss.
The audit phase is a second model reading the draft cold, with a pass condition made of hard numbers:
passes: {
description: 'FAIL if antithesis_count > 3, OR any fragment
triplet, OR any "Here\'s/Let me" signposting, OR any classic
slop phrase, OR a clickbait title template, OR a fortune-cookie
opening sentence, OR (technical) code_block_count < 6.',
}
Two revision rounds, then it stops regardless of the verdict. What lands in priv/drafts/ after that is a rough draft that has passed a numeric floor. The ideas are mine, the structure is mine, the facts are checked, but the prose is still generated.
So we asked a model to generate a draft with 3,000 words, and that reads roughly fluently and follows the outline, and then asked it to review it and revise; still not enough. LLMs are pretty good at giving the illusion of done, structure, coverage, clean sentences, no typos: a model's first pass is strong in all the places a human's first pass is weak, which is what makes it feel further along than it is.
This is what I think a lot of people are getting wrong when working with LLMs for copywriting, writing, and content creation; they fell for the trap and end up publishing slop; LLMs cannot replace good judgment, good taste and your personality.
This is where editing comes in.
The edit loop is where the voice actually gets trained
Once I have a draft, I read it and annotate it. I've gone as far as to build a small VS Code extension to make my workflow easier. I don't just mark the sentences that are wrong;I mark the sentences that are:
- right but could be better,
- right but could be more like me.
- factually correct but too mechanical and not how I would say it.
The last one is important because, LLMs cannot replicate the little idiosyncrasies that make your writing, thinking and speaking unique. At least not as far as I have been able to get.
I place the annotations in the draft itself, and I have a skill that reads the annotations and applies them to the draft. The skill is called /apply-notes; it's fairly simple but lets me focus on the actual edits and not on the mechanics of applying them.
I'll do several rounds of this, and in some cases, I will completely rewrite a section or change the overall structure of the article. This is what I mean with the outline being a starting point and not a contract. Some of my articles end up being completely different from the original outline, and that's ok. This one is a good example of that; the outline was about Vale and tooling, and the final article is about the process of getting a model to write in my voice.
A quick overview of the annotation system
I built Memo, a small VS Code extension that allows me to select a span of text, add a note, and it stores the note as HTML comments so it survives git and copy-paste while staying invisible in the rendered page. An inline anchor marks the span; one JSON block at the end of the file holds the notes:
{
"id": "mem_20260727114647",
"status": "open",
"action": "rewrite",
"quote": "The deterministic layer is a real markup-aware prose…",
"request": "This is just a word salad, why are talking about…",
"instruction": "Apply this annotation per its action.",
"created_at": "2026-07-27T11:46:47.000Z"
}
The request field is the valuable part, and it's why the note lives in a structured block instead of a code comment: my instruction, in my own words, typos and all, attached to the exact text that provoked it. That particular annotation is why this article has a section called "Why you'd want a model in your voice at all" — the draft you're reading opened straight into linter configuration, and I'd written three thousand words of tooling for a problem I hadn't bothered to establish.
/apply-notes consumes the block. It executes each note by action — rewrite in my voice, replace with my literal text (spelling fixed first, since I type badly in a hurry), delete, or comment, which proposes and changes nothing. It never touches fenced code, the frontmatter, or [TODO: Allan] markers. Before it clears a completed note, it appends the correction to priv/voice-corrections.md:
### make-an-llm-write-in-your-voice — mem_20260727114647 (2026-07-27)
- **original:** "The deterministic layer is a real markup-aware
prose linter — Vale — rather than a grep hook, for a reason that
matters more than it sounds: it never sees your code."
- **request:** "This is just a word salad, why are talking about
code already we haven't talked of why you want LLMs to write
like you"
- **applied_text:** Added a `## Why you'd want a model in your voice
at all` section ahead of all implementation. The paragraph became:
"The first layer is deterministic, and it's Vale: a markup-aware
prose linter rather than a grep hook, because it never sees your
code."
- **lesson:** A how-to cannot open on the how — establish why the
reader would want the outcome before any config, or the tooling
reads as solving nobody's problem. "for a reason that matters more
than it sounds" is an importance-assertion; it instructs the reader
to care instead of earning it.
Each entry is a labeled example — the original, my verbatim request, what shipped, and one transferable lesson. The file opens with a distilled "Patterns so far" section, sixteen numbered patterns across twenty-three entries at this point, and macgregor-tech-writer is told to weight that section above the generic style guides whenever they disagree. Generic guidance describes a voice; the log demonstrates a practical one.
This correction log and the editing loop are what I'm hoping with time will train the model to write in my voice. The log is a record of what the model got wrong, and what I did to fix it. I'm not expecting this to ever be perfect, but with time it will get closer to my final edits.skills
The last line of defense
Editing catches most of it. It doesn't catch all of it, because by the fifth read you start developing blind spots, and mentally editing sentences that a fresh reader would flag.
So two things are run after the edit loop. One is deterministic, cheap, and names the tells that hold still long enough to be caught by a pattern. The other is a second model reading the draft cold, hunting the ones that move.
What a regex can name
Using Vale.sh as a linter, I can write rules that catch the patterns that are hard to prompt away.
The rules are written in YAML, and they can be as simple as a list of banned phrases, or as complex as a regex that matches a specific pattern. The rules are run against the draft, and any matches are flagged for review. Vale also does a lot of the heavy lifting for me, like ignoring fenced code blocks, inline code spans, and URLs, so I don't have to worry about false positives from those.md
The workhorse rule type is existence, which fires when any token matches. This is the one that catches signposting, the reflex of announcing a point you could have just made:
# styles/MacGregor/Signposting.yml
extends: existence
message: "Signposting: '%s' — show it, don't announce it."
level: warning
ignorecase: true
nonword: true # strip the implicit \b for multi-word phrases
tokens:
- here'?s the thing
- let me (?:walk you through|be clear|explain)
- the key takeaway is
Every entry in tokens folds into one case-insensitive alternation, so a flat list of banned phrases compiles to a single regex for free.existence Severity carries the judgment: utilize → use is unambiguous enough to swap unattended,substitution while words that are slop in prose and precise in code — optimize, robust, navigate — stay at suggestion and never gate anything. Which rules you turn on has to be settled against your own archive. Run the off-the-shelf AI-tells package against mine wholesale and it fires 30.7 alerts per post and 2,458 errors, led by EmDashUsage at 505 and SemicolonUsage at 442 — the exact cadence the ruleset is supposed to protect. Curated, the same corpus drops to 7.0 alerts and 0 errors.basedonpackage
[*.md]
BasedOnStyles = MacGregor # the authored voice style
# Curated subset — NOT `BasedOnStyles = ai-tells` wholesale.
# Signature-voice rules stay OFF: SemicolonUsage, EmDashUsage,
# ColonUsage, ShipOveruse.
ai-tells.ContrastiveFormulas = warning # "not X; it's Y" form
ai-tells.ContrastiveNegation = warning
ai-tells.ParallelStaccato = warning # fragment-triplet rhythm
ai-tells.MicDrop = suggestion # ~70–75% precision — advisory
A linter catches the tells like fixed phrase, banned words, the template with a stable surface, with some further customization to match my writing style. It is deterministic, and it is cheap. It is also blind to subtler patterns that are not stable enough to be caught by a regex.
Catching the slop
For the subtle slop we rely on a second pass. I think of it as the humanizer, though not in the SEO sense of laundering text past a detector; its job is to find what the pattern matcher structurally cannot.
Asking the writer agent to audit its own draft will always fail. I tried this plenty at the beginning and it failed every time. What I have now is a second model, in its own context, that can't see the writer's reasoning:
---
name: slop-auditor
description: "Independent adversarial reviewer that hunts AI-slop in prose
drafts. Use to audit a draft BEFORE publishing — it catches the
structural patterns the writer cannot detect on a self-read."
model: sonnet
---
You are an adversarial AI-slop auditor. You did not write the draft
and you have no stake in defending it. You assume slop is present
until the text proves otherwise.
Each subagent runs in its own context window with its own system prompt, so the auditor shares no generation context with the writer; it can't be talked into leniency, and is not trying to defend the draft.subagents One thing its prompt has to say explicitly: cede the lexical layer to the linter. Tell it a deterministic pass already ran and owns the fixed banned words — assume "delve" and "utilize" are handled — or it burns its entire context re-flagging vocabulary and never reaches the structural work it is supposed to do.
What I've found useful is giving the auditor a budget. A ban list doesn't help when a cliché keeps coming back in a slightly different form:
## What to hunt (in priority order)
1. Contrastive antithesis overuse — "It isn't X. It's Y." reversals.
One to three across a whole article is voice; eight-plus is a machine.
Count every instance and quote each. More than ~3 → fail.
2. Fragment triplets — "Not a. Not b. C." At most one in the entire piece.
6. Semicolon balance — ~1 per 150 words. Flag BOTH failure modes:
too few (choppy, mechanical) AND 3+ clauses stacked in one sentence.
A few reversals are just how I write; the thesis line usually has one, and a section punch might have another. The auditor keeps those and rewrites the rest as plain statements.
So far it works. On one of my drafts I had already cut every hard "Not X. It's Y." in an earlier pass, and the audit still came back with 12 in the body and 2 more hiding in the footnotes. They had grown back softer: X, not Y tails, instead of contrasts, aren't… They're… mirrors.
Overall, it's another safety net to catch and refine the subtle slop that the linter cannot.
Final thoughts
When I started this project, I wanted to see how far I could get a model to write in my own voice, not with the goal of bringing more slop into the world but to hand off the drafting so I could spend my energy on the thinking, and while not perfect today I have a process that is mostly working.
Over the past 5 months I've also generated some failures: there are posts now that are rough and read more mechanical than I would like, but the process is getting better and I am learning more about how to get better content out there.
Maybe in time I'll go back and rework those earlier articles and see how much better they can be with the process I have now, but for now I'm happy to have a system that can help me write in my own voice and keep the ideas intact.
I'm very curious to hear what you as a reader think about this process, and if you have any ideas or suggestions for how to improve it. Please reach out to me on LinkedIn, X or at the email below.