Cover letter lint
Goal: a cover letter of 50-90 words that a busy client reads to the end and answers, with every claim backed by the user's own facts file.
The linter is scripts/lint-cover.mjs in this skill's folder (Node 20+, no dependencies). Call it by its full
path: node ${CLAUDE_SKILL_DIR}/scripts/lint-cover.mjs in Claude Code (also for a plugin install). Other
agents don't fill in that variable, so use the folder, e.g. node .claude/skills/cover-letter-lint/scripts/lint-cover.mjs
for a project install or node ~/.claude/skills/cover-letter-lint/scripts/lint-cover.mjs for a global one.
1. Find the facts file
Look for facts.md, me/facts.md or a .cover-lint.json with a "facts" path in the current project.
- Found: read it. Claims come only from there.
- Not found: offer to create
facts.mdfromtemplates/facts.mdin this skill's folder, and fill it in with the user (skills, roles, track record, proof links, things never to say). Don't invent any of it. If the user declines, lint without--factsand tell them claims were not checked.
2. Write (skip if the user brought their own draft)
Save the job post to a file if the user pasted one (e.g. job.md). Then write, as plain sentences with no
bullets or headings:
- A greeting in the client's style, then one concrete detail of THEIR post (the bug, the stack, the deadline) and what likely causes it or how you'd approach it. Never restate the post.
- One proof line from the facts file that really matches (a similar job, a role, a demo link). A personal project is called a personal project. No honest match: leave proof out.
- The plan in one or two sentences, with hours and a price if the user gave you rates.
- One question, the one whose answer changes the scope or the price.
- The user's first name on its own line.
Mirror the post: three casual lines get three casual lines back, another language gets that language.
3. Lint and fix
node ${CLAUDE_SKILL_DIR}/scripts/lint-cover.mjs --facts facts.md --job job.md proposal.md
(- reads stdin; --mode message for client replies, which have no word range; --json for machine output.)
Fix every error and rerun until it exits 0. Warnings: fix them unless there is a reason, and say the reason.
For unlisted-skill, either remove the claim or ask the user whether to add the skill to the facts file.
Never edit the facts file to make a claim pass without the user saying it's true.
| Rule | Level | What it catches |
|---|---|---|
| ai-phrase | error | ~75 template phrases: "I hope this finds you well", "seamless", "I'm excited to", "the key is"… |
| not-x-but-y | error | "This is not just X, but Y" style contrasts |
| dash | error | em dashes and dashes used as them |
| placeholder | error | [Your Name], {{name}}, <client>, TODO, lorem ipsum |
| off-platform | error | emails, phone numbers, "message me on WhatsApp", payment outside the marketplace |
| unbacked-claim | error | links and "N years" that are not in the facts file |
| never-say | error | phrases listed under "Never say" in the facts file |
| word-count | error / warn | over 90 words (error), under 50 (warning) in cover mode |
| unlisted-skill | warn | a framework or service the letter names that's in neither the facts file nor the job post |
| question | warn | no question, or more than one |
| list, greeting, exclamation, emoji, long-sentence, i-start | warn | lists, "Dear Sir", "!!", emoji, 35+ word sentences, too many sentences starting with "I" |
Per-project tweaks go in .cover-lint.json (all keys optional):
{"facts": "facts.md", "maxWords": 90, "minWords": 50, "extraPhrases": [], "allowPhrases": [], "disable": []}.
4. Hand it over
Show the letter and the linter's last line. Name the price and the question you chose so the user can change them. Then read it once more against the facts file yourself: the linter checks links, years and tech names, not every sentence. The user sends it; never submit anything for them.