cvlinter_

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 lint checks the failure modes of that discipline:

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

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.

severitybase damagemeaning
error55%the parser got this factually wrong (a misread skill, a swapped field)
warning22%worth double-checking (missing dates, a low-confidence read)
info5%a quiet note (minor normalization oddities)

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

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.

  1. 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.

  2. 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

  3. 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.

  4. 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

  5. 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.

  6. 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.

  7. 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

  8. 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.

  9. 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.

  10. 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

  11. 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.

  12. 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.

  13. 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.

  14. 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.

  15. 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%.

  16. 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.

  17. 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.

  18. 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

  19. 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

  20. 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.

  21. 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.

  22. 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.

  23. 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

  24. 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.

  25. 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.

  26. 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.

  27. 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.

  28. 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".

  29. 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.

  30. 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.