Write in the owner's voice
Use this skill when an agent drafts outward messages that go out under a firm owner's name (email, direct messages, posts, comments, texts) and the owner keeps catching wording they would never write. Drafts come from the owner's real sent writing: the agent changes the nouns of the nearest sent message, checks the draft against hard rules, never-written phrases, usual length and past corrections, and sends only on a pass from the last hour. Every night, drafts matched to sent mail become swaps, and a held-out test scores whether a judge can tell a draft from the real thing. Also use for "write it like me", "draft this in my voice", "reply for me", "that doesn't sound like me", "why does this read like a robot".
Applies whenever you draft a message that will go out under the owner's name, to a client, a peer, a vendor, a prospect or the public. Produces one checked draft built from the owner's own sent writing, and a pass that lets it be sent within the hour.
The play
- Keep a local corpus of what the owner wrote: one record per sent message, the owner's words only. Quoted replies, forwarded blocks, the signature and tracking footers are cut. It is built from mail and chat exports, or from messages you read through a connector. The corpus holds client mail and personal messages, so it stays on the owner's machine. Record shapes are in
references/json-shapes.md. - Before you write a word, pull the nearest examples and the register:
Examples rank by the same recipient first, then the same email domain, then the same channel and kind of recipient, then a similar length, then word overlap with the facts you give. A message that breaks a hard rule is never offered, so you cannot copy a habit the owner has since dropped.examples --to ADDRESS_OR_NAME --channel email --n 8 examples --to ADDRESS_OR_NAME --channel email --stats--statsgives the median length, the median sentence length, and the owner's usual openers and closers. - Write the draft by changing the nouns of the nearest example. Keep its opener, its length and its close. The rules are the check, never the source.
- Put the exact text to be sent in a file, with no subject line, and check it:
The check returns BLOCK, FLAG or PASS. What each catches is incheck FILE --to ADDRESS_OR_NAME --channel email --save-draftreferences/the-check.md. - Act on the result:
- BLOCK: rewrite each listed line as a new sentence. Changing its punctuation is not a rewrite. Run the check again.
- FLAG: read each flagged word, phrase or length note. Change whatever the owner would not write. Keep what they would, such as a name or a term from the thread.
- PASS or FLAG writes a pass for that exact text and saves the draft for the nightly loop.
- Send that same text within 60 minutes. One changed word after the check needs a new check. A hook in front of every send tool refuses a send whose text has no pass from the last hour. A refusal is never worked around. The draft goes back to step 2.
- When the owner corrects your wording, record the correction the same turn as a swap: your phrase, their phrase. From then on the check catches it.
- Let the nightly loop run without the owner: yesterday's sent mail goes into the corpus, saved drafts are matched to what was sent, the changes become swaps, and a held-out test scores the drafting. The order is in
references/the-loop.md.
What good looks like
- First thing I check: whether the draft started from a sent example or from a description of my style. A draft written from rules reads like a draft written from rules. One note carried a technical phrase I had to catch myself, and that is why the system exists.
- The mediocre version is a long style guide and a reminder to follow it. It drifts within a week. The check and the gate hold because they are mechanical.
- I know it is working when I send drafts unchanged. Each one is recorded as a pass, and the swap list stops growing.
- I know the drafting is good when the held-out judge sits near 0.5. That means it cannot tell the draft from what I really sent.
- Red flag: a swap list full of the agent's own rewrites. Several drafts of one note once got written up as my corrections. Only the newest draft of a note is matched to what I sent now. The earlier ones are marked superseded and never become swaps.
Rules
- MUST pull examples and the register before drafting, for every outward message in the owner's name.
- MUST check the exact text to be sent, and check again after any change.
- MUST rewrite every BLOCK line as a new sentence.
- MUST send within 60 minutes of the pass, or check again.
- MUST record each wording correction from the owner as a swap in the same turn.
- MUST keep the corpus local, on the owner's machine.
- NEVER upload, quote, post or link the corpus or any message in it to a connector, a shared page, a task tool or any remote host.
- NEVER draft from the style rules alone.
- NEVER work around a gate refusal. Go back to the examples.
- NEVER let the drafting side of the nightly test see the real message it is drafting.
- NEVER expect a script to do a model's job. The scripts call no model. The agent does that part.
references/json-shapes.md
Record shapes
All files sit in the corpus folder on the owner's machine, except the pass files, which sit in the agent's own settings folder. Addresses below are placeholders.
Corpus record
One per line in corpus.jsonl:
{"id": "gmail:3f9a0c1d2e4b5a6c", "source": "gmail", "channel": "email",
"date": "2026-10-26T10:15:00-04:00", "to": ["client@example.com"], "to_kind": "client",
"subject": "Re: Plan", "thread_before": "the previous message, up to 600 characters",
"text": "the owner's words only", "words": 54, "ingested": "2026-11-01T15:52:00+00:00"}
source is gmail, slack, linkedin or text. channel is email, dm, channel, post, comment or text. to holds addresses for email, names for direct messages, #name for a chat channel, and public for posts and comments.
Add list
The input to ingest --add, a JSON list. Only source, channel, date and text are required. The full body may be pasted; it is cleaned the same way.
[{"source": "gmail", "channel": "email", "date": "2026-10-31T14:02:00-04:00",
"to": ["client@example.com"], "subject": "Re: plan", "thread_before": "their message",
"text": "the full body as sent"}]
Recipients
recipients.json, a flat map from an address, an @domain or a name to a kind: client, peer, vendor, friend, prospect, public or unknown. Keys starting with _ are comments.
{"client@example.com": "client", "@example.com": "client", "Sam": "vendor"}
Pass file
voice-pass/<sha256>.json, the hash taken over the normalised text. latest.json adds head, the first 200 characters.
{"ts": "2026-11-01T15:52:00+00:00", "epoch": 1793548320.0, "sha": "...", "tier": "FLAG",
"to": ["client@example.com"], "channel": "email", "kind": null}
Saved draft
drafts/<YYYY-MM-DD>_<sha8>.json:
{"text": "...", "to": ["client@example.com"], "channel": "email", "kind": "client",
"ts": "2026-11-01T15:52:00+00:00"}
Pair
One per line in pairs.jsonl. result is unchanged, changed or superseded.
{"draft_id": "2026-11-01_3f9a0c1d", "sent_id": "gmail:...", "to": ["client@example.com"],
"date": "2026-11-01T16:20:00-04:00", "draft": "...", "sent": "...", "result": "changed",
"overlap": 0.82, "changes": [["<your phrase>", "<their phrase>"]]}
Swap
One per line in swaps.jsonl:
{"my_phrase": "<your phrase>", "owners_phrase": "<their phrase>", "count": 2,
"first_seen": "2026-10-31", "last_seen": "2026-11-02"}
Held-out test
Items, answers and results, in eval/:
{"date": "2026-11-02", "items": [{"item_id": "e1", "to": ["client@example.com"],
"channel": "email", "kind": "client", "thread_before": "..."}]}
{"date": "2026-11-02", "answers": {"e1": {"record_id": "gmail:...", "date": "...",
"text": "...", "words": 87}}}
{"date": "2026-11-02", "items": [{"item_id": "e1", "facts": ["..."], "draft": "...",
"real": "...", "real_was": "A", "judge_pick": "B", "judge_correct": false}]}
In results, item_id, draft, real and judge_correct are required. judge_correct is true when the judge picked the owner's real text.
Metrics
One line per scored date in metrics.jsonl:
{"date": "2026-11-02", "n": 5, "judge_accuracy": 0.6, "length_ratio": 1.12,
"drafts_blocked": 0, "block_hits": 0, "corpus_records": 1243, "swaps_applied": 3,
"scored_at": "..."}
references/the-check.md
The check
The check reads one file holding exactly the text that will be sent: the email body or the chat message, with no subject line. It returns one of three tiers and an exit code, and lists every hit with its line.
check FILE --to ADDRESS_OR_NAME --channel email --save-draft
check - --to ADDRESS_OR_NAME --channel dm (reads the draft from standard input)
BLOCK (exit 2)
Any hit on the firm's hard rules. No pass is written. Hard rules are the things the owner has ruled out for good. A typical set:
- punctuation the owner never uses, such as a long dash;
- banned words and stock phrases, and filler transitions;
- announcement phrases of the "we're excited to" kind;
- internal labels left in a draft, such as a revision number;
- contrast shapes that define a thing by what it is not, in any punctuation: "It's not X. It's Y.", "a X, not a Y", "Not X. Y."
Each hit is listed with its line. Rewrite the sentence. Swapping the punctuation keeps the shape, and the shape is what was banned.
FLAG (exit 1)
Things the owner might write, but has not yet:
- Words and two-word phrases the owner has never written. Names, web addresses, numbers and a short stoplist of common words are ignored.
- Length. A draft more than 1.5 times the median length of the owner's messages to this recipient. With too few messages to that person, the median for that kind of recipient is used, then the channel.
- Swaps. Any phrase the owner has replaced twice or more, shown with the owner's replacement.
- Filler intensifiers, allowed only in a warm social line.
- A spaced short dash.
A FLAG still writes a pass. Read each line and change what is not the owner's. A term the other person used in the thread can stay.
PASS (exit 0)
None of the above.
The pass file
On PASS or FLAG the check writes a pass named by a hash of the normalised text: markup stripped, curly quotes straightened, whitespace collapsed, letters lowercased. So a change in spacing or quote style keeps the pass, and a changed word loses it. Passes older than seven days are removed.
--save-draft also keeps the draft for the nightly match against what was sent.
The gate
A hook runs before every send tool call. It refuses:
- a mail send, reply, forward or draft, or a chat send or scheduled send, whose exact text has no pass from the last 60 minutes;
- a browser click whose summary says send, post, publish, reply, submit or share, when no draft at all has passed in the last 30 minutes.
Messages only to the owner pass. Nothing else is stopped. Any error inside the hook lets the call through, so the gate can never break a turn. A refusal means the draft goes back to the examples.
references/the-loop.md
The loop
The system improves every night from two signals that need nothing from the owner: everything they send, and the difference between your draft and what they actually sent. It runs inside a scheduled early-morning run on weekdays.
The nightly order
- Read yesterday's sent mail and chat messages through the connectors. Write them to a dated file in the corpus inbox, in the list shape in json-shapes.md.
- Add them to the corpus:
The same script also reads a mail export (ingest --add INBOX/<date>_sent.json--mbox), a chat export folder (--slack-json) or a social network data export (--linkedin-dir). - Match drafts to sent messages:
reconcile - Pick the held-out items:
evaluate sample --n 5 - Do the agent's part of the test, below.
- Score it:
evaluate score <date> - Mondays only, the one-line summary:
evaluate report
Reconcile and swaps
Reconcile takes each saved draft from the last seven days and looks for the message the owner sent: same recipient, sent within 72 hours of the draft, sharing at least 5 distinct words that make up at least half of the shorter text's distinct words.
- A draft sent unchanged is recorded as a pass.
- A changed draft yields swaps. Each short stretch the owner replaced or cut becomes a swap: your phrase, their phrase. A swap is counted every time it recurs, and the check applies any swap seen twice or more.
- Several saved drafts of one note (same recipient, within 72 hours of each other, overlapping above the same floor) count as one note. Only the newest is matched. The earlier ones are recorded as superseded and never become swaps. Without this rule, your own successive rewrites get written up as the owner's corrections.
A correction the owner gives you directly goes into the swaps file the same turn, so it does not wait for the nightly match.
The held-out test
Sample picks five messages the owner sent in the last 90 days. The same date always gives the same pick. A message qualifies when it has 15 to 400 words, passes the hard rules, was not used in an earlier test, and has at least two other messages to the same recipient (or of the same kind and channel), so the drafting side has examples to work from. It writes two files: the items the drafting side may see, and the answers, for the facts step and the scorer only.
Your part, for each item:
- A facts step reads the answer and writes the bare facts as a short list: who, what, dates, numbers, the ask. Never the wording.
- A drafting step that never sees the answer runs
examples --to ... --channel ... --exclude-eval <date>and drafts from the facts and those examples.--exclude-evalleaves the held-out messages out, so a draft never sees its own answer. - A separate model, the judge, gets the draft and the real text as A and B in random order and guesses which one the owner wrote.
Write the results file, then score. The score gives the judge's accuracy, the length ratio and the hard-rule blocks, and keeps the trend. A judge accuracy of 0.5 means it cannot tell the two apart. That is the goal.
Tests
Every script takes --corpus DIR to work on another folder. Test every change on a scratch folder, never on the real corpus.

