ResumeJSON

Blind resume screening: how to anonymize CVs, by hand or in code

Blind resume screening: hide the person, keep the evidence

Blind resume screening means reviewers judge an application without seeing the details that identify the candidate or hint at their background: name, photo, contact details, address, age clues and often the names of schools. The point is to make the first pass about what a person has done and can do. You can do it by hand with a redaction tool and a checklist, or you can build it into your hiring software by turning every CV into structured fields and showing reviewers only the fields that carry evidence.

This article covers both. It is written for the developer adding blind review to a job board, an applicant tracking system or an internal hiring tool, and for the recruiter who wants to try it on the next role before anyone writes code. We build ResumeJSON, a parsing API, so read the part about parsing as written by an interested party. The rules about what to hide are yours whichever tool you use.


What blind resume screening hides, and why

A CV mixes two kinds of information. Some of it is evidence about the job: roles held, how long, what the person did, skills, certifications, languages. The rest identifies the person or signals their background, and a reviewer can react to it without meaning to.

Here is the usual split. Decide your own list before the role opens, and write it down.

Detail on the CVHide it?Why
Full nameYesSignals gender and ethnicity
PhotoYesSignals age, gender, ethnicity and appearance
Email and phoneYesIdentify the person; an email often contains the name
Street addressYesCan signal neighbourhood and income
City or countryUsuallyKeep only if location is a real requirement
Date of birth, ageYesSignals age directly
Graduation yearsOftenA graduation year is an age estimate
School or university namesOftenPrestige bias; keep the degree and subject
Personal links (LinkedIn, social)YesOne click shows the photo and name
Portfolio or code linksDependsNeeded for some roles, but they usually name the person
Job titles and datesNoThis is the evidence
Employer namesSometimesA famous employer works like a famous school
Skills, certifications, languagesNoThis is the evidence

If a detail cannot change whether someone can do the job, it should not be in front of the first reviewer. That is the whole test, and it settles the edge cases. A driving role needs a licence, so the licence stays. A remote role does not need a city, so the city goes.

Blind screening only covers the first pass. The interview is not blind, and it should not be. What blind review buys you is a shortlist built on the record rather than on a first impression of a name.

Blind resume screening by hand

For a single role with a manageable pile of applications, the manual way works and needs nothing new.

  1. Pick one person to anonymize and a different person to review. The anonymizer sees everything, so they cannot also be the reviewer.
  2. Give every application an id. A spreadsheet with the id, the candidate's name and the original file is the only place the two are linked. Keep it away from reviewers.
  3. Redact each CV. Open the PDF in a tool with a real redaction feature (not a black highlighter, which leaves the text underneath selectable) and remove everything on your hide list. Save it under the id.
  4. Check the file metadata. A PDF's author field and a DOCX's properties often carry the candidate's name. Re-export or clear them.
  5. Hand reviewers the redacted files only. They score or shortlist by id.
  6. Unblind after the shortlist is fixed. Only then does anyone map ids back to names.

This is slow, and it leaks in predictable places. A name appears in the email address in the header, then again in a footer, then again in a sentence like "Sarah led the migration". Two-column layouts hide details in a side panel. A cover letter undoes all of it. Redacting a document means finding every place a detail appears; redacting a record means dropping one field. That difference is why most teams that keep doing blind review move to the second approach.

Blind resume screening in code: parse, then show only the evidence

If your software already stores applications, blind screening becomes a view over structured data. You stop editing documents and start choosing which fields a reviewer's screen renders.

Step 1: turn every CV into the same record

You need each CV as fields rather than as a page. The options are the same as for any parsing job: ask candidates to type their history into a form, parse the files yourself, or call a parsing API. We compared the do-it-yourself routes in Python and Node.js, and the API route in our CV parser API guide.

ResumeJSON returns a record shaped like this for every PDF or DOCX:

{
  "basics": {
    "full_name": "Sarah Okonkwo",
    "email": "sarah.okonkwo@example.com",
    "phone": "+44 20 7946 0000",
    "location": "Leeds, UK",
    "headline": "Backend engineer",
    "links": ["https://linkedin.com/in/example"]
  },
  "work": [
    {
      "company": "Northwind Logistics",
      "title": "Senior Backend Engineer",
      "start_date": "2021-03",
      "end_date": null,
      "is_current": true,
      "location": "Leeds, UK",
      "highlights": ["Moved order routing from a monolith to three services"]
    }
  ],
  "education": [
    {
      "institution": "University of Leeds",
      "degree": "BSc",
      "field_of_study": "Computer Science",
      "start_date": "2012",
      "end_date": "2015"
    }
  ],
  "skills": ["Go", "PostgreSQL", "Kafka"],
  "certifications": [],
  "languages": [{ "language": "English", "proficiency": "Native" }],
  "total_years_experience": 9.5
}

Everything identifying sits in basics. The evidence sits in work, skills, certifications, languages and the computed total_years_experience. That separation is what makes the blind view cheap to build.

Step 2: build the blind view as an allowlist

Write a function that builds the reviewer's record by naming the fields it keeps. Do not start from the full record and delete fields: the day the parser adds a field, a delete list lets it through.

type BlindRecord = {
  id: string;
  headline: string | null;
  work: { title: string | null; start: string | null; end: string | null; current: boolean; highlights: string[] }[];
  education: { degree: string | null; field: string | null }[];
  skills: string[];
  certifications: { name: string | null; issuer: string | null }[];
  languages: { language: string; proficiency: string | null }[];
  years: number | null;
};

function blind(id: string, r: Resume): BlindRecord {
  return {
    id,
    headline: r.basics.headline,
    work: r.work.map(w => ({
      title: w.title,
      start: w.start_date,
      end: w.end_date,
      current: w.is_current,
      highlights: w.highlights,
    })),
    education: r.education.map(e => ({ degree: e.degree, field: e.field_of_study })),
    skills: r.skills,
    certifications: r.certifications.map(c => ({ name: c.name, issuer: c.issuer })),
    languages: r.languages,
    years: r.total_years_experience,
  };
}

What this drops, on purpose:

Store the full record and render the blind one. The reviewer's screen, the export and any API a reviewer can call should all read from blind(), never from the stored Resume. Put that in one function and one test, so it stays true.

Step 3: scrub the free text

Fields remove most of the risk. The rest hides in free text: a highlight that reads "Sarah rebuilt the billing service", or a headline that includes a name. Two cheap checks catch most of it:

  1. Replace the candidate's own name tokens in every string you render. You already have full_name; split it and replace each part, case-insensitively, with "the candidate".
  2. Flag, do not silently fix, anything else that looks personal: an email pattern, a phone pattern, a URL. Show it to the anonymizer rather than guessing.
function scrub(text: string, fullName: string | null): string {
  if (!fullName) return text;
  let out = text;
  for (const part of fullName.split(/\s+/).filter(p => p.length > 1)) {
    const escaped = part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    out = out.replace(new RegExp(`\\b${escaped}\\b`, 'gi'), 'the candidate');
  }
  return out;
}

Run scrub over headline and every highlights entry inside blind().

Step 4: unblind at a fixed point

Decide in advance when reviewers see names: usually once the shortlist is saved. Make that a state change on the application with a timestamp, so you can later show which decisions were made blind and which were not.

Manual redaction and structured blind review, side by side

Redacting documents by handBlind view over parsed fields
Work per applicationMinutes per CV, every timeSet up once, then none
Name in a footer or sentenceEasy to missDropped with the field, or caught by the scrub
Two-column and designed CVsHard to redact cleanlySame record as any other CV
File metadataMust be cleared by handNever shown
Changing what is hiddenRe-redact every fileChange one function
Works without new softwareYesNo

Where blind screening falls short

Blind review is a useful tool with real limits. Be honest about them with the people using it.

When the manual way is enough

You do not need code for blind resume screening when:

If the CVs arrive as files and you want to see what the parsed record looks like before building anything, our free parser handles one CV in the browser with no signup.

Where ResumeJSON fits

ResumeJSON does the parsing step: a PDF or DOCX in, the record above out, with identifying details kept together in basics so the blind view is a matter of leaving them out. It does not score, rank or anonymize for you, and the hide list stays in your code, where you can read, test and change it.

If blind review is one part of a wider screen, our guide to automated resume screening covers writing the rules that run over the same record, and bulk resume parsing covers getting a backlog of files into it. When you are ready to wire it in, the API docs show the request and every field.

All articles