This page is for whoever builds step 1 of [issue 130](https://github.com/nordscope-fi/plain-english/issues/130). By the end of it, the plugin carries a new skill. It gives Claude the plain-English rules in Claude chat (web, desktop and phone) and in Cowork. The same skill runs the checker on substantial drafts there. Claude Code keeps working as it does today. The research behind it is in [the 9 October investigation](../../investigations/2026-10-09-claude-surfaces.md). The live test it depen...
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
SSkills DirectorySkills Directory
Is that skill safe to install?
SSkills Directory ProSkills Directory
Get any skill into Claude in one click.
Are you the author of Specs?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/nordscope-fi-specs)
# Design: a writing skill that checks drafts in Claude chat and Cowork
This page is for whoever builds step 1 of [issue 130](https://github.com/nordscope-fi/plain-english/issues/130).
By the end of it, the plugin carries a new skill. It gives Claude the
plain-English rules in Claude chat (web, desktop and phone) and in Cowork. The
same skill runs the checker on substantial drafts there. Claude Code keeps
working as it does today.
The research behind it is in
[the 9 October investigation](../../investigations/2026-10-09-claude-surfaces.md).
The live test it depends on is the
[10 October result](https://github.com/nordscope-fi/plain-english/issues/130#issuecomment-6095687083):
the checker ran unchanged in chat on the web and the phone, on `/opt/node22/bin/node`
`v22.22.0`, with nothing downloaded. No Anthropic page promises Node in chat;
the docs say only that a script runs in "what the code-execution environment
provides". The measurement covers one account. Other plans are untested, and
the `unavailable` status below is what they get if Node is missing.
## What the owner decided
Each of these was asked and answered during design on 10 October 2026.
| Question | Decision |
| --- | --- |
| Who is it for | The public Anthropic directory. Every file stays within the directory's checks. |
| One plugin or two | One. The skill is added to the existing plugin. |
| When Claude runs the checker | On substantial drafts: every document, email or report, and any reply over about 100 words. |
| How a draft reaches the checker | A new function that takes text, over faking a Claude Code hook event. |
| One copy of the checker or two | One, kept inside the skill. The mod loads it from there. |
The directory portal confirmed the one-plugin decision. Its "Listed on" section
for the existing plugin has Claude Code, Cowork and Claude apps (web, desktop,
mobile) all ticked. For the last two it notes only "Not used here: output
styles. The directory lists mods for Claude Code only."
## What goes into the plugin
The plugin gains one skill folder, `skills/plain-english/`, and the checker's
core moves into it from `hooks/core/`:
```text
skills/plain-english/
SKILL.md generated by `plain-english render`
scripts/check.mjs reads a draft on standard input, prints a JSON report
scripts/version.mjs the package version, written by build-plugin
scripts/core/*.mjs the checker's core, written by build-plugin
hooks/register.ts imports the core from ../skills/plain-english/scripts/core/
```
- **Why the core moves.** Anthropic's skills guide says "In chat on claude.ai,
the skill's whole folder, scripts included, is copied into the code
execution sandbox". Nothing says the rest of the plugin goes with it, so the
core has to sit inside the skill.
- **Why one copy.** The directory rescans every version: "The scan can raise
the same hold again on each new version." A second copy would put the same
code through the scan twice and need a test to keep the copies equal.
- **The move is tested.** A scratch copy of the plugin with the core moved and
the mod's two imports repointed passed `claude plugin validate --strict` on
Claude Code 2.1.296 ("✔ Validation passed"). It also passed
`claude plugin test`: "36 pass, 0 fail", the same as the unmoved plugin.
- **Size.** The plugin stays at about 37 files and 0.95 MB. The build splits
the core into pieces of at most 240 kibibytes (units of 1,024 bytes), below
the directory's 256-kibibyte read limit (`SPLIT_AT` in
`scripts/build-plugin.mjs`).
- **Folder depth.** The skill upload page refused a zip with a path more than
10 folders deep. No Anthropic page states this limit; it was found by
testing. The deepest path in this layout is `scripts/core/<file>`, three
segments below the skill root.
- **The existing `writing-a-document` skill stays as it is.** It covers
Markdown in a repository; the new skill covers replies and drafts in any app.
## The draft-checking function
A new module, `src/draft-check.ts`, exported from `src/plugin-core.ts`.
```ts
export type DraftKind = "reply" | "document";
export interface DraftFinding {
ruleId: string;
severity: "error" | "warn";
line: number;
column: number;
quote: string; // the matched passage
message: string; // what to do about it
mayStand?: true; // a count the person's request can justify
}
export interface DraftReport {
status: "checked" | "incomplete" | "invalid";
kind?: DraftKind;
findings: DraftFinding[];
review: string[]; // questions only a reader can answer
notes: string[]; // why a report is incomplete or invalid
}
export function checkDraft(request: unknown, io: CheckerIo, options?: { budgetMs?: number }): DraftReport;
```
- **Input.** `request` is `{ text, kind }`. Anything else is `invalid`, with a
note naming the field: empty or missing text, or a kind other than `reply`
or `document`.
- **Rules.** The bundled ruleset only, through `compile(loadDefault(io))`,
which reads it from `io.defaultRules()`. No project configuration is
discovered, because a chat request has no repository.
- A `reply` lints with `chatRuleSet` at the default level and the hook time
budget (`HOOK_BUDGET_MS`, 500 ms), as `decideChat` does.
- A `document` lints with the document rules, no reply limits, and the
command line's budget (`DEFAULT_BUDGET_MS`, 2,000 ms), as `lint` does.
- Inline suppression is off for both. A draft carries no waivers.
- **Status.** `checked` when every rule ran. `incomplete` when `lintText`
reports any `timedOut` rule, listing them in `notes`. The function never
reports `checked` for a partial run.
- **Counts the person can justify.** `reply-length`, `reader-load` and
`reply-pace` are the rules the reply judge may clear (`JUDGEABLE` in
`src/adapters/chat.ts`). Their findings carry `mayStand: true`, so Claude can
keep a long reply when the person asked for depth.
- **Review questions.** The checks a model makes in Claude Code come back as
questions. For a reply, one per `chat.judge` entry in `rules/default.yml`
(`unexplained-terms`, `no-let-up`, `answers-a-question-nobody-asked`). For a
document, the `flag` lines of `docs.guidance`, the same faults of shape the
document judge is given.
- **What it leaves out.** No file reads, no `io.state`, no acknowledgement
file, no hold-once logic and no model call. Each call is independent.
## The script the skill runs
`scripts/check.mjs` takes the kind as its only argument and the draft on
standard input:
```sh
node "${CLAUDE_SKILL_DIR}/scripts/check.mjs" reply < /tmp/plain-english-draft-<random>.txt
```
- **How the draft gets there.** Claude saves it with its file tool to a new
file in a temporary folder, never the person's own folder, and deletes it
after the check. The shell redirects that file into the script. A heredoc would end at the
first line matching its delimiter, so pasted prose carrying that line would
run the rest as shell commands. A security review of the first version of
the skill text flagged exactly that.
- **Where it is found.** The skills guide says "Claude Code and Cowork replace
`${CLAUDE_SKILL_DIR}` with the skill's folder when the skill loads". Chat
does not, so `SKILL.md` also names the path relative to itself,
`scripts/check.mjs`.
- **Reading the draft.** It reads standard input only when the kind is valid,
and gives up after 10 seconds if the input never closes. Without that limit
the script hangs on an open pipe; a test reproduced it on Node 20 and 26. A
read that fails or times out is reported as `invalid`, never as `checked`.
- **What it imports.** `./core/plugin-core.mjs`, `./core/default-rules.mjs`
and `./version.mjs`. It builds a `CheckerIo` whose `read`, `stat` and `list`
return nothing and whose `defaultRules` returns the bundled ruleset. It has
no `node:` import.
- **What it prints.** The `DraftReport` as JSON, plus a `version` field from
`version.mjs`. Exit 0 for `checked` with no errors, 1 for `checked` with
errors, 2 for anything else.
- **When the core will not load.** It prints
`{"status":"unavailable","notes":[...]}` and exits 2. This is the one status
the script adds; `checkDraft` cannot report it, because it is the code that
failed to load. A missing `node` is reported by the shell before the script
runs, and the skill's instructions cover that case.
## What the skill tells Claude
`SKILL.md` is generated by a new `renderWritingSkill` in `src/render.ts`, next
to `renderDocsSkill`, and written to
`integrations/claude-code/skills/plain-english/SKILL.md`. `build-plugin`
copies it into the plugin as it copies the other guidance files. The
instructions about running the script live in the generator; the ruleset's
format does not change.
- **Description.** Under 200 characters. An older help article caps it at
"200 characters maximum"; the current skills guide allows "Up to 1,024
characters". The shorter limit satisfies both. It names the tasks that
should load the skill: answering a question, drafting an email or report,
and reviewing prose.
- **Rules.** The reply rules from `styleBody` at the default level, shared with
the output style and the block `init` writes into a repository's agent
instructions file, so the three cannot drift.
- **When to check.** Every document, email or report, and any reply over about
100 words.
- **The loop.** Draft, check, fix, check once more, deliver. At most two runs
per draft.
- **What the reader sees.** Nothing extra after a clean check. One line saying
the draft was not fully checked after `incomplete`, `unavailable`, or a
missing `node`.
- **Supplied text.** On "check this", quote the findings and edit at the
strength the person asked for. A light edit fixes clear faults only.
- **In Claude Code.** Follow the rules, do not run the script. The mod already
checks there.
- **No automatic commands.** `SKILL.md` carries no line that runs a command when
the skill loads. The directory holds a skill line of that kind that runs a
file from a subfolder.
## How replies get the skill
The skills guide says a skill loads when a request matches its description.
It sends rules for every conversation elsewhere: "Instructions for every
conversation: put those in your personal preferences or project instructions
rather than a skill". An ordinary short question may not load
the skill at all.
So the plugin README offers one sentence a person adds to their profile
instructions, and presents it as the way to get replies checked, not as an
extra. A person can also pick the skill by typing `/` in the message box. The
plugin changes no settings itself.
## What the listing says
The directory policy requires that "descriptions must precisely match actual
functionality", and the listing is read from `plugin.json` and the README.
Today's `plugin.json` description promises save checks and reply holds, which
only Claude Code provides. It is reworded to say what each app gets, and the
README gains a short section per app.
## How it is tested
Tests are written first and seen to fail.
- **`checkDraft`** in `test/draft-check.test.ts`, one case each:
- a reply with a stock opener and a stock phrase, and a clean reply;
- a long reply, whose length finding carries `mayStand`;
- a document with a banned word and no length finding;
- a banned phrase inside a code block, and a suppression comment that must
not hide a finding;
- empty text and an unknown kind, both `invalid`;
- a forced timeout, reported as `incomplete`;
- an `io` whose `read`, `stat`, `list` and `state` throw, proving none is
called.
- **The plugin's directory checks** in `test/claude-code-plugin.test.ts` read
the core from its new folder: file size, line length, no program spawn, no
eval, no web address, no credential-shaped name, no accessor.
- **The skill folder:** no `node:` import in `check.mjs`; no path deeper than
three segments; no automatic-command line in `SKILL.md`; the version file
matches the package.
- **A chat-shaped run:** copy only `skills/plain-english/` into an empty
temporary directory and run `check.mjs` on sample drafts, checking JSON and
exit codes. Cases: findings, clean, a document, empty input, a missing or
misspelled kind, a very large draft, and a removed core file that must print
`unavailable`. The temporary folder is made and removed by the test's setup
and teardown.
- **Generated files:** `npm run render -- --check` covers the new `SKILL.md`,
and `build-plugin --check` covers the core and the version file.
- **Anthropic's tools, before pushing:** `claude plugin validate --strict` and
`claude plugin test` on the plugin folder, the same two CI runs.
## What must hold before release
The owner runs these by hand, with synthetic prose only.
1. Install the plugin on the account. Then, on the web, the phone app, the
desktop app's chat and a Cowork task:
- the skill loads for an email, a report and a long explanation;
- the script runs;
- "check this" on supplied text quotes the findings and keeps a light edit
light.
The desktop app's chat and Cowork in its own right were not part of the
10 October test, so these are their first runs.
2. Compare a small set of drafts written three ways: no skill, rules only, and
rules plus checking. Record whether the skill loaded, whether the script
ran, whether a rewrite happened, and the added time. Run the reply cases
once without and once with the profile sentence.
3. Timing. For the first version, run these before submitting. For later
versions, run them before merging: the portal's "Publish new versions
automatically" is on, so a version that passes the directory's checks can
go live without a manual step.
## Release
A minor version bump, carried in the pull request. A changelog entry under
`Added`. A new decision record, `docs/architecture/adr/009-checker-ships-in-a-skill.md`,
covering the core's move into the skill, the `checkDraft` contract and the
statuses.
## Not part of this step
These are the later steps of issue 130's recommended sequence, already tracked
there:
- Cowork hooks that refuse a write or hold a reply (step 2).
- A remote connector for surfaces without a code environment (step 3).