Writing a CV that survives
Best practices for engineering CVs, and how each one is checked against your real CV. Many of these conventions come from the excellent r/EngineeringResumes wiki, which is worth reading in full.
How most "AI resume scores" work
The resume tools that charge monthly for "AI optimization" mostly do one thing: parse your CV to text, parse the job description to text, count the overlapping keywords, and sell the overlap back to you as a score. A "95% match" means your CV contains most of the posting's words. Nothing about whether your experience is real, relevant, or readable. Worse, the advice that falls out of that number is always the same: stuff more keywords in. Recruiter-side screening keeps getting better at flagging exactly that.
Cvlinter deliberately doesn't work that way, and the differences are checkable:
- the parse score measures the one thing you can measure: whether an ATS can extract your CV. Every lost point is itemized, and the whole method is spelled out below.
- content is never scored. Writing quality is a judgment, so you get a findings tally, "1 thing to fix, 2 to consider", where every finding points at a specific bullet. No fictional "your content is 62/100".
- XYZ: accomplished X, measured by Y, by doing Z. "Cut p95 API latency 40%, measured in New Relic, by adding Redis caching and tightening the worst queries."
- STAR: situation, task, action, result. Better for interviews than bullets, but the R is non-negotiable either way.
- CAR: challenge, action, result. "Facing 3× traffic growth, redesigned the pipeline on Kafka. 2M events a day, zero downtime."
The lint checks the failure modes of that discipline:
- Weak openers. "Responsible for", "worked on", "helped". Duty phrasing, not impact. Start with what you built, led, cut, shipped.checked: weak-opener
- Responsibility beyond the first word. A bullet can open strong and still describe a job description. Judged per bullet, by substance.checked: responsibility-phrasing
- Roles with no numbers at all. Scale, speed, money, users, hours saved. Anything countable makes the same claim credible.checked: no-metric
- Claims without evidence. "Significantly improved performance" asserts an achievement and proves nothing.checked: vague-claim
- Over-long or fragmentary bullets. Past ~45 words a bullet stops being skimmed; a four-word fragment spends a line saying nothing.checked: long-bullet
- First person and tense drift. Implied first person, past tense for finished work, present for current. Mixing them reads as unedited.checked: first-person
- End bullets with periods, consistently. Either convention is fine; mixing them isn't.convention
When a bullet is flagged, the fix flow asks what you actually did and tightens your answer. Suggestions use only your own material, and any invented number is rejected automatically before you see it. We don't write bullets for you; a generated achievement is a lie with good grammar.
The summary (if you have one)
Convention says skip the summary unless you're changing careers or past ~10 years of experience. Your bullets should carry the story. If you keep one: two or three tight sentences of verifiable fact (years, stack, scale, domain), no adjectives a recruiter can't check. "Passionate, results-driven team player" spends your most expensive line saying nothing.checked: summary-buzzwordchecked: summary-length
Skills that hold up
- Every listed skill should be demonstrated somewhere. A skills section is receipts for the bullets. A recruiter can't verify a skill no experience shows.checked: skill-unevidenced
- And the reverse: tech you actually used belongs in the list. Keyword filters usually search the skills section; tools that live only in prose can be missed.checked: skill-unlisted
- Plain comma-separated lists under simple category labels parse best. We verified against the live engine that decorative separators and inline category labels create phantom terms.convention
- Know that parsers rewrite your skills. Normalization can replace what you wrote with something else entirely. We've watched "Vercel" become "Linux Kernel". The preview shows every replacement in red, because a recruiter searching your actual term gets zero hits on the replaced one.
How the parse score is calculated
The parse score measures one thing: how well your CV survives being read by an applicant tracking system. It is not a judgment of your experience, your writing, or your fit for any role. A brilliant CV in a layout that parsers mangle scores low, and that's the point. We run your file through the same commercial engine real ATSs use, then inspect the result for likely mis-parses. Each finding does a measured amount of damage to the section it belongs to; the score is nothing but a roll-up of the findings you can already see.
| severity | base damage | meaning |
|---|---|---|
| error | −55% | the parser got this factually wrong (a misread skill, a swapped field) |
| warning | −22% | worth double-checking (missing dates, a low-confidence read) |
| info | −5% | a quiet note (minor normalization oddities) |
- coverage scaling: a problem on one item out of many hurts less than one across a whole section, so fixing four of five undated roles visibly moves the score. The divisor caps at 10, because keyword-search damage is absolute: a recruiter searching the one skill that was misread gets zero hits either way.
- diminishing returns: each finding removes its share of what remains, so a section scores 100 × ∏(1 − damage). It degrades toward a floor instead of slamming to zero, which keeps the number discriminating between bad and worse.
- empty sections: if a section's heading is in your raw text but nothing parsed out of it, that's the worst failure we can detect and the section loses 95%. If your CV genuinely doesn't have the section, it's excluded from the roll-up and noted instead. Projects counts here too: a projects heading with nothing under it is a parse failure, and no projects heading at all is simply not scored.
- your skills, not inferred ones: whether you have a skills section is judged by the section you wrote, not by the keywords the parser infers from your prose. A CV with no skills section gets no skills score, because the alternative is scoring you on a list you never wrote. It's the same rule the export follows when it refuses to invent a skills section.
- no letter grades: plain numbers out of 100, overall and per section. Letters added a second scale to learn without adding information.
Section weights, rescaled over the sections your CV actually has: experience 40, skills 25, contact 20, education 15, projects 10. Experience weighs most because it's what recruiters search; contact matters because a mis-parsed email means no reply at all. The severity numbers are chosen judgment calls, not yet derived from data, and we're calibrating them against real CVs. Every score carries its method version, and when the weights or rules change, the version changes and this page changes with it.
What we deliberately don't do
- No quality grade. Content findings are itemized; they never roll into a "your CV is X/100" judgment, and they never touch the parse score.
- No keyword advice. No rule here tells you to insert a term to satisfy a filter. Matching a CV against a specific posting is a feature we're still building, and when it lands a requirement will only count as evidenced if one of your bullets shows you doing it.
- No rewriting in your name. Suggestions compose only from your own words and answers, a fact guard rejects any number you didn't state, and nothing applies without your approval.
- No invented sections. The ATS-safe export renders your parsed content plus the fixes you approved: one classical template, your skills section reproduced from your CV's own text, and no skills section at all if your CV didn't have one. It targets your CV's own page count and never drops content to get there.
- No storage. Your CV is parsed in memory, discarded, and never used to train anything. We log which checks fired and what the sections scored: counts only, never your CV's content. The privacy page lists every service that touches your file.
Every check, in fix order
All 30 of them, generated from the same catalog the report runs, so this list can't drift from the code. The order is the order the report tells you to fix things in: a field the ATS read wrong outranks a bullet that reads a little long. parse means the machine couldn't read it and it costs score; content means it reads fine but says less than it could, and never touches the score.
- parseCV is a scanned imagecvlinter(scanned-document)
OCR extraction is far less reliable than reading real text, so every other check on this CV is less trustworthy too.
after Export a PDF from the source document instead of scanning or screenshotting it.
- parseNo email extractedcvlinter(missing-email)
Without an email the ATS has no way to contact you, and some systems reject the application outright.
after One plain-text email on its own line near your name: jordan.reyes@example.com
- parseNo name extractedcvlinter(missing-name)
The ATS files your application under your name. Without one your record can be unsearchable.
after Put your name alone on the first line, in normal body text, not inside a header, text box, or image.
- parseEmployer and title crossed overcvlinter(employer-title-swap)
Recruiters search by job title. A company name in the title field means their search never reaches you.
before Nova Analytics / Senior Software Engineer (on separate lines, or in a two-column layout)
after Keep them on one line, title first, separated by a comma: Senior Software Engineer, Nova Analytics
- parseRole has no bulletscvlinter(empty-role)
Everything you did in this role is invisible to keyword matching.
after Two to five bullets per role, each starting with a past-tense verb.
- parseBullets broke apartcvlinter(fragmented-bullets)
Fragmented bullets lose the keywords they were meant to carry.
after One bullet per line with a real bullet character, no line breaks inside a sentence, no tables.
- parseRole has no datescvlinter(missing-role-dates)
ATS ranking weights recency. An undated role can be read as no experience at all.
after Right-align a plain range on the same line as the role: Mar 2022 – Present
- contentLayout looks like two columnscvlinter(two-column)
Multi-column layouts are the most common cause of scrambled extraction on other ATSs, even when ours reads it.
after One column, top to bottom.
- parseSkill renamed by the parsercvlinter(skill-misread)
Keyword matching runs on the normalized skill, not your words. A recruiter searching what you actually wrote gets no hit.
before GCP (read as Good Clinical Practices)
after Write the tool's full canonical name so normalization has nothing to guess: Google Cloud Platform (GCP), not GCP alone.
- parseTwo projects read as onecvlinter(projects-merged)
The second project's heading is gone, so its work is filed under the wrong name or lost from the CV entirely.
before Meshpad github.com/you/meshpad Visprobe github.com/you/visprobe (run together, so the second name is read as part of the first entry)
after Put the project name on its own line, then its link, then the bullets: Meshpad / github.com/you/meshpad
- parseEntry parsed twicecvlinter(duplicate-entry)
Duplicated entries make the CV look corrupted to a human reader.
after Usually caused by a layout the parser reads twice. Keep one column and no text boxes.
- contentRole shows no measurable outcomecvlinter(no-metric)
Numbers are what make a claim checkable: scale, speed, money, users, anything countable.
before Improved dashboard performance for our users.
after Reduced p95 dashboard load from 2.7s to 1.5s for 900 weekly users.
- parseProject has no namecvlinter(project-missing-name)
Nothing ties these lines to a project, so a reader can't tell what they describe and a keyword search can't attribute them.
after Name the project on its own line above its bullets.
- contentBullet opens with duty phrasingcvlinter(weak-opener)
Recruiters skim for outcomes. A duty opener buries yours behind filler.
before Responsible for the ingestion pipeline and on-call rotation.
after Cut the ingestion pipeline's failure rate 40% by adding retries and dead-letter queues.
- contentDescribes responsibility, not impactcvlinter(responsibility-phrasing)
What you were assigned says less than what changed because you were there.
before Worked on the checkout flow with the payments team.
after Rebuilt the checkout flow, lifting conversion 12%.
- contentClaim without evidencecvlinter(vague-claim)
Unevidenced claims read as filler, and inflated ones are exactly what recruiter-side screening now looks for.
before Improved performance by 300%.
after Cut p95 API latency 38%, from 210ms to 130ms, measured in New Relic.
- contentSummary leans on buzzwordscvlinter(summary-buzzword)
Self-describing adjectives are unverifiable, so recruiters skip them and read the bullets instead.
before Passionate, results-driven engineer with a proven track record.
after Senior full-stack engineer, 6 years on consumer products, TypeScript and Go, real-time systems at 250M-user scale.
- parseNo phone number extractedcvlinter(missing-phone)
Recruiters expect one, and some ATS filters treat a missing phone as an incomplete profile.
after Include the country code: +49 151 234 5678
- parseDegree didn't parse cleanlycvlinter(unparsed-degree)
Degree filters are common in ATS screens; a mangled degree field fails them.
after Spell it out on one line: BSc Computer Science, TU Berlin
- parseEducation has no datescvlinter(missing-education-dates)
Some ATS treat undated education as incomplete and rank it lower.
after Oct 2015 – May 2019, on the same line as the institution.
- contentSkill shown but not listedcvlinter(skill-unlisted)
Keyword filters usually run on the skills section, so tech that only appears in prose can be missed.
after Add it to the skills section under the right category.
- contentSkill listed but never showncvlinter(skill-unevidenced)
A recruiter can't verify a skill no experience demonstrates; the strongest skills sections are receipts for the bullets.
after Either show the skill in a bullet, or drop it from the list.
- parseNo location extractedcvlinter(missing-location)
Recruiter searches filter by location. Without one you're missing from those results.
after City and country is enough: Berlin, Germany
- contentCV runs longcvlinter(page-count)
Reviewers skim page one; extra pages dilute your strongest material rather than adding to it.
after One page under about 8 years of experience, two above it.
- parseParser wasn't surecvlinter(low-confidence-field)
Low-confidence fields are the ones ATSs most often get wrong.
after Simplify the line the parser struggled with: plain text, one fact per line, no decorative separators.
- contentBullet is a fragmentcvlinter(fragment-bullet)
A fragment spends a line without making a claim.
before Various backend work.
after Make it a full statement with a verb and an outcome.
- contentBullet is too long to skimcvlinter(long-bullet)
Bullets over two lines stop being skimmed, which means they stop being read.
after Split it: one claim per bullet, under about 30 words.
- contentBullet uses first personcvlinter(first-person)
CV convention is implied first person; a literal "I" reads as unedited.
before I designed the checkout A/B testing framework.
after Drop the pronoun: "Designed the checkout test framework", not "I designed the checkout test framework".
- contentTense is inconsistentcvlinter(tense-inconsistent)
Mixed tense inside one role reads as unedited text.
after Past tense throughout for past roles; present tense only for the role you're in now.
- contentSummary is too longcvlinter(summary-length)
A summary competes with your first role for attention and loses if it runs long.
after Two or three lines: what you are, how long, which stack, which domain.
Using a chatbot instead
You can get useful CV feedback for free by pasting your CV into any capable chatbot with a good prompt. Try it. Two things it can't show you, because it never sees them: what an ATS actually extracts from your specific file (layout survives or it doesn't. That's a parser question, not a language question), and whether its rewriting drifted from what you can defend in an interview. That second one matters: generic AI rewrites are exactly what recruiter-side screening is learning to flag. Everything here checks your real file against a real parsing engine, and never puts a claim in your mouth.
Parse your CV to see all of this against your own file. Nothing is stored. If a check misfires or the parser mangles something, tell us: that is how the detection gets better.