A free Claude Code skill that makes your agent write like a person

·16 min read

You can tell when a post was written by AI in about a second. So can everyone else, and they scroll past it.

I care about this a lot. My posts and replies have to sound like me, because the moment something reads like a chatbot people stop giving it the benefit of the doubt. So I wrote rules for my own agents, and anything an agent drafts for me gets checked against them before I use it.

Today I'm giving that away as a Claude Code skill called unslop-writing. It has two parts. A SKILL.md that tells the agent how to write, and a small checker, check.mjs, that the agent has to pass before it hands you a draft. No dependencies, no account, nothing to sign up for. Both files are in full further down.

Why AI writing gets skipped

The text is usually fine. The trouble is that every model reaches for the same moves, and readers have seen those moves thousands of times now.

A reply opens with "Great point!". A sentence swings on "it's not about X, it's about Y". Three adjectives line up in a neat row. A long dash sits in the middle of a thought. The last line sums up what you just read. None of these are bad on their own. Together they're a pattern, and people spot the pattern faster than they read the words.

Once someone spots one tell, they stop reading closely. That's the real cost. Your actual point, the specific thing you know, never gets read.

I stopped using dashes as punctuation completely for this reason. It makes it look like AI.

Three before and afters

These are examples I wrote to show the pattern. They aren't real posts or real emails.

A reply to someone asking which AI coding tool to use:

Before: Great question! It's not about the tools, it's about the workflow. Consistency, clarity, and focus are what truly unlock results.

After: the tool matters less than you think. pick one agent and use it every day for a month before you switch

A cold email opening:

Before: I hope this email finds you well! I wanted to reach out because I believe our product could seamlessly elevate your team's workflow. Don't hesitate to let me know if you'd like to learn more.

After: Saw your team ships a release every Friday. We built a tool that writes the changelog from your merged PRs. Want me to run it on last week's release and send you what it made?

A README intro:

Before: This robust CLI empowers developers to navigate complex workflows with ease. Whether you're a beginner or a seasoned pro, it's a game-changer.

After: Reruns only the tests your last change touched, so you get an answer before you switch tabs.

The after versions aren't clever. They answer the actual thing, they name something specific, and they stop.

The rules

The skill gives the agent seven rules for how to write (the seventh is about matching the channel), then a list of tells to avoid.

  1. Answer the actual thing. If there's a question, the first sentence is the answer.
  2. One idea per piece. A reply makes one point. An email asks for one thing.
  3. Be specific. Name the tool, the error, the step. Never invent specifics you don't have.
  4. Opinions come with a reason. "It depends on your use case" is not an opinion.
  5. Short sentences, plain words. "Use", not "utilize".
  6. Stop when the point is made. No recap, no moral at the end.

The tells fall into four groups. Punctuation: dashes used as punctuation and semicolons. Constructions: "it's not X, it's Y", "not just X but Y", "here's the thing", flattering openers and summary endings. Words: leverage, delve, seamless, robust, crucial, landscape and the rest of the usual list. Decoration: emoji chains and hashtag walls.

Why a checker and not just a prompt

You can tell a model "don't use em dashes" and it mostly listens. Then it writes " - " instead. Same rhythm, same tell, different character. Tell it to stop saying "leverage" and you get "utilize".

That's why the checker exists, and why the skill says to rewrite and never respell. The checker flags spaced hyphens and double hyphens too, not just the long dash. When the agent gets exit code 1, the fix is a new sentence, not a synonym.

A few choices I made on purpose:

  • Errors fail, warnings don't. Tidy lists of three are only a warning, because sometimes there really are three things.
  • Code is ignored. Code blocks, inline code and URLs are skipped, so a README full of commands doesn't light up.
  • Hyphenated words are fine. "well-known" and "2-3 days" pass. Only dashes used as punctuation get flagged.
  • Normal phrases have exceptions. "Navigate to settings" and "unlock your phone" pass. "Navigate the chaos" doesn't.

It runs with plain Node, no packages. You can pass it text, a file or stdin, and it prints every problem with the snippet it matched.

The full skill

Save this as SKILL.md:

---
name: unslop-writing
description: Write text that reads like a person wrote it, with no AI tells. Use whenever you write or edit words someone will read under a real name, such as social posts, replies, comments, DMs, emails, blog posts, READMEs, docs, release notes and PR descriptions. Every draft must pass check.mjs before you return it.
---

# unslop-writing

People skim and ignore text that sounds machine written. The tells are small and repeat everywhere, so readers spot them in a second. This skill removes them. Follow the rules while you write, then run the checker and rewrite until it passes.

## How to write

1. **Answer the actual thing.** If the prompt or post asks a question, the first sentence is the answer. "32gb." beats a paragraph about tradeoffs. Never restate the question back.
2. **One idea per piece.** A reply makes one point. A post makes one point. An email asks for one thing. Cut the second idea or save it for another piece.
3. **Be specific.** Name the tool, the number, the file, the error, the step. "the build failed on the Expo 52 upgrade" beats "we ran into some challenges". Never invent specifics you don't have. If you need a fact you don't have, ask or leave it out.
4. **Opinions come with a reason.** "skip the ORM here, raw SQL is 40 lines and you can read it" is an opinion. "it depends on your use case" is not.
5. **Short sentences, plain words.** Say "use", not "utilize". Say "help", not "empower". If a sentence needs a comma splice or a semicolon, make it two sentences.
6. **Stop when the point is made.** No recap, no moral, no "hope this helps". The last line should be a fact, a next step or a real question.
7. **Match the channel.** Replies: 5 to 25 words, casual is fine. Posts: a hook line, then the one idea. Emails: the ask in the first two lines, one ask only, a sign-off without filler. Docs: steps in order, commands in code blocks, no marketing words.

## The AI tells (never use)

**Punctuation**
- Em dashes, en dashes, a spaced hyphen or a double hyphen used as a dash. Hyphenated words like "well-known" are fine. Use a full stop, a comma or a colon, or rewrite the sentence.
- Semicolons. Use two sentences.

**Constructions**
- "It's not X, it's Y" and "This isn't about X. It's about Y." Just say Y.
- "Not just X but Y" and "isn't just a tool". Say what it is.
- "Here's the thing", "here's why:", "the real X is Y", "let that sink in", "read that again".
- "No X. No Y. Just Z."
- Tidy lists of three ("fast, cheap, and reliable"). Keep the one that matters. Real lists of steps are fine.
- "Whether you're a beginner or a pro".
- Restating the prompt or the post before answering.
- Openers that flatter: "Great question", "Love this", "So true", "100%", "Spot on", "Couldn't agree more", "Absolutely!".
- Summary or moral endings: "In conclusion", "Overall,", "Ultimately,", "The bottom line", "The takeaway", "Remember:".
- Generic closing questions like "What do you think?". Ask something only this reader can answer.

**Words and phrases**
leverage, delve, seamless, robust, crucial, landscape, journey, navigate (except "navigate to"), unlock, empower, game-changer, testament, elevate, revolutionize, supercharge, tapestry, realm, "in today's world", "in a world where", "at the end of the day", "it's worth noting", "let's dive in", "plays a key role", "harness the power", "I hope this email finds you well", "don't hesitate to", "I hope this helps".

Softer signals (the checker warns): pivotal, vital, foster, streamline, utilize, showcase, embark, myriad, plethora, transformative, cutting-edge, fast-paced, holistic, "dive into", sentences that open with Furthermore, Moreover or Additionally.

**Decoration**
- 3 or more emoji, or emoji chains. One at most, usually none.
- 2 or more hashtags. One at most, usually none.
- Bold words scattered through a short post, headers on a 3-line message, bullet walls where a sentence works.

## Self-check (required)

Before you return any draft:

1. Save the draft to a temp file and run the checker that ships with this skill:
   ```bash
   node ~/.claude/skills/unslop-writing/check.mjs --file draft.txt
   ```
   For a project-level install the path is `.claude/skills/unslop-writing/check.mjs`. Short text can go inline: `node check.mjs "the text"`. Exit code 1 means it found errors.
2. For every error, **rewrite the sentence**. Do not respell the tell. Swapping an em dash for " - ", "leverage" for "utilize" or a semicolon for a comma splice keeps the same machine rhythm and is not a fix.
3. Run the checker again. Repeat until it exits 0.
4. Read the warnings. Fix the ones that are real. A triplet that is a true list of three things can stay.
5. Read the draft once more as the reader. The checker catches tells, not bad ideas. Is it specific? Does it answer the actual thing? Would a person type this? If not, rewrite it even though it passed.

Code blocks, inline code and URLs are ignored by the checker, so commands and links in docs don't trip it.

## Quick rewrites

| Tell | Rewrite |
| --- | --- |
| "It's not about speed, it's about trust." | "people stay for trust, speed just gets them in the door" |
| "This tool will seamlessly elevate your workflow." | "it runs your tests after every edit, so you stop finding breaks a day later" |
| "Great question! There are a few factors to consider." | "32gb. the simulator alone eats half of 16" |
| "In conclusion, consistency is key." | (delete the line) |

Save this next to it as check.mjs:

#!/usr/bin/env node
// unslop-writing checker: flags the common tells of AI-written text.
// Zero dependencies. Node 18+.
//
//   node check.mjs "some text"
//   node check.mjs --file draft.txt
//   cat draft.txt | node check.mjs
//
// Prints every problem with the matched snippet. Exit code 1 if there is
// any error, 0 if clean (warnings are printed but do not fail the check).
// Code blocks, inline code and URLs are ignored.

import { readFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';

const E = 'error';
const W = 'warn';

// Words and phrases that mark text as machine written.
const PHRASES = [
  [/\bdelv(e|es|ed|ing)\b/gi, 'delve'],
  [/\bleverag(e|es|ed|ing)\b/gi, 'leverage'],
  [/\bseamless(ly)?\b/gi, 'seamless'],
  [/\brobust(ly|ness)?\b/gi, 'robust'],
  [/\bcrucial(ly)?\b/gi, 'crucial'],
  [/\blandscapes?\b(?!\s+(mode|orientation|photos?|format)\b)/gi, 'landscape'],
  [/(?<!\b(user|customer|buyer)\s)\bjourneys?\b/gi, 'journey'],
  [/\bnavigat(e|es|ed|ing)\b(?!\s+(to|back|away|into)\b)/gi, 'navigate'],
  [/\bunlock(s|ed|ing)?\b(?!\s+(the|your|my|a|his|her)\s+(phone|door|screen|device|account|car|laptop)\b)/gi, 'unlock'],
  [/\bempower(s|ed|ing|ment)?\b/gi, 'empower'],
  [/\bgame[- ]?chang(er|ers|ing)\b/gi, 'game-changer'],
  [/\b(a|is a|stands as a) testament\b/gi, 'testament'],
  [/\belevat(e|es|ed|ing)\b(?!\s+(privileges|permissions|access|rights|shell|prompt|terminal)\b)/gi, 'elevate'],
  [/\brevolutioni[sz](e|es|ed|ing)\b/gi, 'revolutionize'],
  [/\bsupercharg(e|es|ed|ing)\b/gi, 'supercharge'],
  [/\btapestry\b/gi, 'tapestry'],
  [/\bin the realm of\b/gi, 'in the realm of'],
  [/\bin today'?s\s+(\w+[- ])?(world|age|era|landscape)\b/gi, "in today's world"],
  [/\bin a world where\b/gi, 'in a world where'],
  [/\bat the end of the day\b/gi, 'at the end of the day'],
  [/\bit'?s (worth noting|important to note)\b|\bit is (worth noting|important to note)\b/gi, "it's worth noting"],
  [/\blet'?s dive in\b/gi, "let's dive in"],
  [/\bplays? an? (crucial|vital|pivotal|key|important) role\b/gi, 'plays a key role'],
  [/\bharness(es|ed|ing)? the power\b/gi, 'harness the power'],
  [/\bbuckle up\b|\blet that sink in\b|\bread that again\b/gi, 'hype line'],
  [/\bi hope this (email |message |note )?finds you well\b/gi, 'hope this finds you well'],
  [/\b(please )?don'?t hesitate to\b/gi, "don't hesitate to"],
  [/\bi hope this helps\b/gi, 'I hope this helps'],
  [/\bas an ai\b/gi, 'as an AI'],
  [/\bwhether you'?re an? [^.!?\n]{1,40}\bor\b/gi, "whether you're a X or a Y"],
];

// Weaker signals: printed, but they don't fail the check.
const SOFT = [
  [/\b(pivotal|vital|foster(s|ed|ing)?|streamlin(e|es|ed|ing)|utiliz(e|es|ed|ing)|showcas(e|es|ed|ing)|boasts?|embark(s|ed|ing)?|myriad|plethora|unparalleled|meticulous(ly)?|invaluable|transformative|ever-evolving|fast-paced|cutting-edge|holistic|synergy)\b/gi, 'stock AI word'],
  [/\bdive (deep )?into\b|\bdeep dive\b/gi, '"dive into"'],
  [/(^|[.!?]\s+)(furthermore|moreover|additionally|notably),/gim, 'formal connector'],
  [/\bthe real (question|problem|issue|magic|secret|story|win|lesson|unlock) (is|isn'?t)\b/gi, '"the real X is Y"'],
  [/\bi'?d love to hear your thoughts\b|\bwhat do you think\?/gi, 'generic closing question'],
];

// Sentence shapes that read as AI.
const SHAPES = [
  // "It's not X, it's Y." / "This isn't about X. It's about Y."
  [/(?:\b(?:is|are|was|were) not|\b(?:isn|aren|wasn|weren)'?t|'(?:s|re) not)\s+[^.!?\n]{1,60}?(?:[,.:;\u2014]|\s-\s)\s*(?:it|this|that|they|we|you)(?:'s|'re| is| are| was)\b/gi, '"it\'s not X, it\'s Y"'],
  // "not just X, but Y" / "not only X but also Y"
  [/\bnot (just|only|merely|simply)\b[^.!?\n]{1,60}?\bbut\b/gi, '"not just X but Y"'],
  // "This isn't just a tool."
  [/\b(isn'?t|is not|aren'?t|are not|wasn'?t|was not) (just|only|merely|simply) (a|an|the|about)\b/gi, '"isn\'t just X"'],
  [/\bhere'?s the (thing|deal|kicker|catch|truth|secret|twist)\b/gi, '"here\'s the thing"'],
  [/\bhere'?s (why|what|how)\s*[:.\n]/gi, '"here\'s why:" setup'],
  [/\bno [^.!?\n]{1,30}\. no [^.!?\n]{1,30}\. just\b/gi, '"No X. No Y. Just Z."'],
];

// Openers that restate or flatter instead of answering.
const OPENERS = /^\s*((great (point|question|post|take|thread|insight)|love this|so true|couldn'?t agree more|spot on|underrated take|you'?re absolutely right|what a (great|fantastic|brilliant) \w+)\b|100%(?!\s*of\b)|(absolutely|certainly|this|exactly)!)/i;

// Summary or moral lines that close a draft.
const CLOSERS = /^\s*(in conclusion|in summary|to sum up|to summarize|overall,|ultimately,|all in all|in short,|the bottom line|the takeaway|the lesson here|remember:|at the end of the day)/i;

function stripCode(text) {
  return text
    .replace(/[\u2018\u2019]/g, "'")
    .replace(/^(```|~~~)[^\n]*\n[\s\S]*?^\1[^\n]*$/gm, (m) => m.replace(/[^\n]/g, ' '))
    .replace(/`[^`\n]*`/g, (m) => ' '.repeat(m.length))
    .replace(/https?:\/\/\S+/g, (m) => ' '.repeat(m.length));
}

function snippet(text, index, length) {
  const start = Math.max(0, index - 20);
  const end = Math.min(text.length, index + length + 20);
  return (start > 0 ? '...' : '') + text.slice(start, end).replace(/\s+/g, ' ').trim() + (end < text.length ? '...' : '');
}

function lineOf(text, index) {
  return text.slice(0, index).split('\n').length;
}

export function check(input) {
  const raw = String(input ?? '');
  const text = stripCode(raw);
  const problems = [];
  const spans = [];
  const add = (level, rule, index, length, message) => {
    // One report per spot: skip a hit that overlaps an earlier hit of the same rule.
    if (spans.some((s) => s.rule === rule && index < s.end && index + length > s.start)) return;
    spans.push({ rule, start: index, end: index + length });
    problems.push({ level, rule, line: lineOf(text, index), snippet: snippet(raw, index, length), message });
  };
  const scan = (re, level, rule, message) => {
    for (const m of text.matchAll(re)) add(level, rule, m.index, m[0].length, message(m));
  };

  // Dashes as punctuation. Hyphenated words (well-known, 2-3) are fine.
  scan(/\u2014/g, E, 'dash', () => 'em dash: use a full stop, comma or colon, or rewrite the sentence');
  scan(/\u2013/g, E, 'dash', () => 'en dash: use "to" for ranges, otherwise rewrite');
  scan(/(?<=\S)[ \t]+-[ \t]+(?=\S)/g, E, 'dash', () => 'spaced hyphen used as a dash: rewrite the sentence');
  scan(/\w--\w|\s--\s/g, E, 'dash', () => '"--" used as a dash: rewrite the sentence');

  scan(/;(?!\))/g, E, 'semicolon', () => 'semicolon: split into two sentences');

  for (const [re, name] of PHRASES) scan(re, E, 'phrase', () => `AI phrase (${name}): say the plain thing`);
  for (const [re, name] of SOFT) scan(re, W, 'soft', () => `${name}: usually a plainer word works`);
  for (const [re, name] of SHAPES) scan(re, E, 'shape', () => `${name} construction: just say Y`);

  const opener = text.match(OPENERS);
  if (opener) add(E, 'opener', opener.index, opener[0].length, 'flattering or restating opener: answer the actual thing first');

  const emoji = [...text.matchAll(/\p{Extended_Pictographic}/gu)].filter((m) => !/[\u00a9\u00ae\u2122]/.test(m[0]));
  if (emoji.length >= 3) add(E, 'emoji', emoji[0].index, 2, `${emoji.length} emoji: use one at most, or none`);

  const tags = [...text.matchAll(/(?<=^|\s)#[A-Za-z][\w-]*/g)].filter((m) => !/^#[0-9a-f]{3,8}$/i.test(m[0]));
  if (tags.length >= 2) add(E, 'hashtags', tags[0].index, tags[0][0].length, `${tags.length} hashtags: use one at most, or none`);

  const lines = text.split('\n');
  for (let i = lines.length - 1; i >= 0; i--) {
    if (!lines[i].trim()) continue;
    const last = lines[i].replace(/^[\s>*_#-]+/, '');
    const m = last.match(CLOSERS);
    if (m) {
      const index = lines.slice(0, i).join('\n').length + (i ? 1 : 0) + lines[i].indexOf(last);
      add(E, 'closer', index, m[0].length, 'summary or moral ending: cut the last line, the point was already made');
    }
    break;
  }

  // Tidy triplet: "fast, cheap, and reliable". A list of four or more is skipped.
  scan(/(?<!\w,\s)\b([a-z]+), ([a-z]+),? (and|or) ([a-z]+)\b(?!,)/gi, W, 'triplet', () => 'tidy list of three: keep the one that matters, or give a real list');

  const order = { error: 0, warn: 1 };
  return problems.sort((a, b) => order[a.level] - order[b.level] || a.line - b.line);
}

function readInput(args) {
  const i = args.indexOf('--file');
  if (i >= 0) {
    if (!args[i + 1]) throw new Error('--file needs a path');
    return readFileSync(args[i + 1], 'utf8');
  }
  if (args.length) return args.join(' ');
  if (!process.stdin.isTTY) return readFileSync(0, 'utf8');
  return null;
}

function main() {
  const args = process.argv.slice(2);
  if (args.includes('--help') || args.includes('-h')) {
    console.log('usage: node check.mjs "text" | --file draft.txt | (stdin)');
    return 0;
  }
  const input = readInput(args);
  if (input === null) {
    console.error('usage: node check.mjs "text" | --file draft.txt | (stdin)');
    return 2;
  }
  const problems = check(input);
  for (const p of problems) console.log(`${p.level.padEnd(5)} line ${p.line}  ${p.rule}: ${p.message}\n      > ${p.snippet}`);
  const errors = problems.filter((p) => p.level === 'error').length;
  const warns = problems.length - errors;
  console.log(errors ? `\n${errors} error(s), ${warns} warning(s). Rewrite the sentences, don't just swap words.` : `clean${warns ? ` (${warns} warning(s) worth a look)` : ''}`);
  return errors ? 1 : 0;
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  try {
    process.exitCode = main();
  } catch (e) {
    console.error(e.message);
    process.exitCode = 2;
  }
}

How to install

Put both files in a folder called unslop-writing inside your Claude Code skills folder:

mkdir -p ~/.claude/skills/unslop-writing
cp SKILL.md check.mjs ~/.claude/skills/unslop-writing/

Claude Code loads skills from that folder, and the description in the frontmatter tells it when to use this one: any time it writes a post, reply, email, doc or README. You can also ask for it by name. If you only want it in one repo, use .claude/skills/unslop-writing/ inside that repo instead.

To try the checker on its own:

node check.mjs "Great point! This isn't just a tool, it's a game-changer."
node check.mjs --file draft.txt

The first one fails with three errors. Exit code 1 means it found something, 0 means clean.

Caveats

It catches tells, not bad ideas. A draft can pass with zero errors and still be boring or wrong. The skill tells the agent to reread the draft as the reader after it passes, and you should still read it yourself.

Regex has false positives. "We're not just friends, we're cofounders" gets flagged, even though a person might say it. I kept the rules tight and pushed the fuzzy ones to warnings, but if a flag is wrong for your text, ignore it or delete the rule.

It's English only. The phrase list and the sentence shapes won't do anything for other languages.

The list goes stale. Models pick up new habits, and people learn to spot them. The word lists sit at the top of check.mjs as plain arrays, so adding your own takes one line.

It's not a disguise. The point is writing people actually read. If your readers expect to know a tool helped, tell them.

What to do this week

  1. Install it. Two files, one folder.
  2. Run check.mjs on the last few posts or emails you sent with an agent's help. See which tells show up most.
  3. Add the tells you personally can't stand to the lists at the top of the file.
  4. Next time your agent drafts something, check that it ran the checker before handing it back. The skill makes that step required, but it's worth seeing it happen once.

Which AI tell makes you stop reading the fastest?