Freelance proposal lint
Goal: a short bid that the client reads to the end and answers, on whatever marketplace the job is on, 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, no network). It is
the same file as in the cover-letter-lint skill of this repo, shipped here so this skill installs on its own.
Call it by its full path, node ${CLAUDE_SKILL_DIR}/scripts/lint-cover.mjs in Claude Code, or the skill's
folder in other agents, e.g. node .claude/skills/freelance-proposal-lint/scripts/lint-cover.mjs.
1. Facts first
Look for facts.md, me/facts.md or a .cover-lint.json with a "facts" path. Claims come only from there.
None found: offer to fill in templates/facts.md from this skill's folder with the user (skills, roles, track
record exactly as their profile shows it, proof links, phrases never to use). Don't invent any of it. If the
user declines, lint without --facts and tell them claims were not checked.
2. Fit the format to the site
Ask which marketplace if the user didn't say. Then:
- Upwork: a cover letter of 50-90 words plus any screening answers. Answer each screening question directly, in the order asked.
- Freelancer.com, PeoplePerHour, Guru: bids are compared side by side with a price and a delivery time. Put the concrete approach in the first line, and make sure the price and days in the text match the numbers the user enters in the bid form.
- Fiverr brief or custom offer: the buyer chose a category, so open with their exact deliverable, then what the offer includes and how many revisions.
- Contra or a direct inquiry: treat it as a first message; lint with
--mode message.
Mirror the client: three casual lines get three casual lines back, another language gets that language.
3. Write
Plain sentences, no bullets or headings:
- A greeting in the client's style, then one concrete detail of THEIR post 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 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 a rate.
- One question, the one whose answer changes the scope or the price.
- The user's first name on its own line.
4. Lint and fix
node ${CLAUDE_SKILL_DIR}/scripts/lint-cover.mjs --facts facts.md --job job.md bid.md
- reads stdin, --mode message drops the word range for replies, --json gives machine output. Fix every
error and rerun until it exits 0. Fix warnings too unless there's a reason, and say the reason.
What fails a bid (errors): template phrases ("I hope this finds you well", "seamless", "I'm excited to"),
"not just X, but Y" contrasts, em dashes, placeholders like [Your Name], emails, phone numbers or "message
me on WhatsApp" (most marketplaces ban moving contact off the site before a contract), links and "N years"
the facts file doesn't back, phrases under "Never say", and more than 90 words in cover mode. Warnings cover
tech names that are in neither the facts nor the post, a missing or second question, lists, "Dear Sir", "!!",
emoji and very long sentences. Per-project tweaks go in .cover-lint.json:
{"facts": "facts.md", "maxWords": 90, "minWords": 50, "extraPhrases": [], "allowPhrases": [], "disable": []}.
Never edit the facts file to make a claim pass without the user saying it's true.
5. Hand it over
Show the bid, the price and delivery time it states, and the linter's last line. 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.