All posts
AI & ML

What Actually Makes an AI Resume Pass ATS: Notes From Building the Normalizer

"AI resume" usually means "stuff it with keywords. " The real work is duller and more interesting: a Unicode-to-ASCII pass that kills em-dashes and smart quotes before a parser mangles them, a banned-cliche list, and a step that mimics your own writing. Here is what I learned building all three into an open-source job-search tool.

Dhileep Kumar6 min read
What Actually Makes an AI Resume Pass ATS: Notes From Building the Normalizer

Somewhere along the way, "AI resume" became shorthand for "stuff it with keywords and beat the robot. " It is a persistent myth, and it survives because it sounds like a hack. Skip it. Modern applicant-tracking systems parse your resume; they do not reject it for insufficient buzzwords. The decision is still made by a person reading it afterward, and keyword soup reads exactly as desperately as it sounds.

I know this from the boring end of the problem. I have been working on career-ops, an open-source job-search tool that (among other things) generates tailored, ATS-optimized resumes as PDFs. It is not a mass-apply cannon — it is explicitly a filter, built to reject most jobs and only surface the few worth a real application. But the part that taught me the most about "AI resumes" was not the language model. It was the unglamorous plumbing that runs after the model is done: a Unicode normalizer, a banned-cliche list, and a step that tries to sound like you instead of like a model. This post is about that plumbing, and why it matters more than the prompt.

The real ATS problem is not keywords. It is characters.

Here is the thing nobody puts in the "beat the ATS" listicles: the most common way an AI-written resume gets quietly mangled is not the wrong words. It is the wrong characters. Language models love typographic Unicode. They emit em-dashes and en-dashes, curly smart quotes, fancy arrows, bullet glyphs, non-breaking spaces, and the occasional invisible zero-width character. On screen it looks polished. Fed through an older resume parser, it turns into mojibake — the little garbage sequences you have seen where an apostrophe becomes three symbols.

So the PDF generator in career-ops runs a normalizer before anything reaches the ATS. It flattens the fancy Unicode down to plain ASCII the parsers can actually read. The rules are mundane and that is the point:

  • Em-dashes and en-dashes become plain hyphens.
  • Smart quotes become straight quotes.
  • Arrows become the actual words "to" and "from" — because an arrow means nothing to a parser but everything to a reader.
  • Bullets and middots collapse to a pipe character.
  • Zero-width characters and non-breaking spaces get stripped entirely.

There is one non-obvious trap in doing this, and I learned it the annoying way. If you naively find-and-replace across the whole document, you also rewrite the characters inside your CSS, your JavaScript, and your URLs — and now your template is broken instead of your text. So the normalizer masks the style and script blocks first, then only rewrites the body text that a human will actually read. Corrupting a hyphen in a sentence is fine. Corrupting one in a stylesheet is a bug.

javascript
// generate-pdf.mjs — ATS-oriented Unicode normalizer (paraphrased)
// ATS parsers and legacy systems often fail on em-dashes, smart
// quotes, and zero-width characters. Convert body text to plain ASCII.
//   em/en dash        -> hyphen
//   smart quotes      -> straight quotes
//   arrows            -> the words " to " / " from "
//   bullets, middots  -> pipe |
//   zero-width chars, non-breaking spaces -> stripped
//
// Mask <style> and <script> blocks FIRST, then rewrite only the
// remaining body text — never CSS, JS, or URLs.
//
// EUR and GBP are spelled out. The yen glyph is deliberately NOT:
// U+00A5 maps to BOTH Japanese Yen (JPY) and Chinese Yuan (CNY),
// so any spelled-out code is wrong for half the userbase. Better to
// leave the glyph than emit confidently-wrong data.

The most interesting line in that whole routine is the one that does nothing. EUR and GBP get spelled out. The yen glyph, U+00A5, is deliberately left alone. It maps to both the Japanese Yen and the Chinese Yuan, so if you "helpfully" convert it to a currency code you will be wrong for roughly half the people who use the tool. The honest move is to leave the glyph in place rather than emit confidently-wrong data. That is a small decision, but it is the whole philosophy in miniature: a tool that guesses and gets it wrong is worse than a tool that admits it does not know.

A resume tool that guesses your currency and gets it wrong is worse than one that leaves the character alone. Confidently-wrong is the failure mode to design against.

The second job: stopping the model from sounding like a model

Once the characters are safe, you hit the harder problem, and it is not technical. Left to itself, a language model regresses to the mean. It reaches for the same handful of phrases on every resume it has ever written: "passionate about," "results-oriented," "proven track record," "leveraged. " None of those words are illegal. The problem is that they are the linguistic fingerprint of machine-generated text, and a recruiter who reads fifty resumes a day has learned to see straight through them.

So career-ops carries a short, blunt banned-cliche list as a first-class feature, not an afterthought. The generator is told, plainly, not to reach for these phrases. It is the writing equivalent of the Unicode pass: a deterministic guardrail that catches the model when it defaults to the average.

javascript
// Professional Writing & ATS Compatibility — banned phrases
const CLICHES = [
  'passionate about',
  'results-oriented',
  'proven track record',
  'leveraged',
];
// The point is not that these words are illegal. It is that a model,
// left alone, reaches for them on every resume it has ever written.

But a banned list only removes the worst tells. It does not make the writing sound like you — and that gap is where most AI resumes die. Two candidates with the same tool produce two resumes in the same faintly-corporate voice, and neither reads like a person. So there is a second, more ambitious step: a writing-style calibration pass. Before generating a cover letter, the tool reads samples of the candidate's own past writing, extracts signatures — punctuation habits, sentence rhythm, vocabulary — and caches them, so the generated text is nudged toward the candidate's real voice instead of the model's default one.

I want to be careful here, because this is the point where AI-resume writing gets oversold. Style calibration does not make the model honest. It makes it sound more like you. Those are different things, and conflating them is how people end up with a fluent, confident resume for a career they did not have.

Who does which half

This is the framing that actually predicts a good AI resume, and it is not "human versus AI. " A resume is two jobs stitched together. One is mechanical: structure, phrasing, formatting, matching the language of the role, and — as it turns out — surviving a Unicode parser. The other is judgment: deciding what your career means, what to lead with, and what to leave out. A model is genuinely good at the first job. It is reliably terrible at the second, and the failure is confident, which is the dangerous part.

Everything I built lives on the mechanical side, and I built it precisely because that is the side you can make deterministic. You cannot write a function that decides your side project matters more than your job title. You absolutely can write a function that guarantees no em-dash reaches the ATS, that no resume ships the word "leveraged," and that the voice leans toward yours. The division of labor that works looks like this:

  1. You supply the raw truth — every role, project, and result, messy and unedited. Only you know it.
  2. The model drafts and structures it into clean, active, tailored bullets.
  3. The deterministic layer does the mechanical safety pass: strip the dangerous Unicode, catch the cliches, calibrate toward your voice.
  4. You edit for truth and emphasis — cut anything invented, promote what matters, restore the specific detail that makes it yours.
  5. You read it one last time as the person who has to defend every word of it in a room.

One honest caveat about the tool itself: it can hit the same failure it is designed to prevent. Ask a model to make you sound impressive and it will happily manufacture a metric you never earned. A banned-cliche list does not stop that. A Unicode normalizer does not stop that. Only step four does — the human reading for truth. The engineering makes the output clean and parseable and yours-sounding. It cannot make it true. That part is not automatable, and I would be suspicious of any tool that claimed otherwise.

So, can AI build a better resume than a human?

For the draft, the formatting, the tailoring, and — the part I keep coming back to — the thousand tiny mechanical correctness checks that decide whether a machine can even read your resume: yes, easily, and faster than a person would ever do them. A human does not think to strip a zero-width character or refuse to convert a yen sign. Code does that reliably, every time.

For deciding what your career means and whether every line is true: no, and it is not close. The best resume out of this whole process is not human or AI. It is a human bringing the judgment and the truth, a model bringing the speed and the draft, and a boring deterministic layer in between making sure the polished, honest result actually survives contact with the parser on the other side.

Share

Enjoyed this?

Get the next deep dive in your inbox. No spam — just the stories worth reading.

Subscribe to the newsletter

Comments