Blind resume screening: how to anonymize CVs, by hand or in code
Published
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 CV | Hide it? | Why |
|---|---|---|
| Full name | Yes | Signals gender and ethnicity |
| Photo | Yes | Signals age, gender, ethnicity and appearance |
| Email and phone | Yes | Identify the person; an email often contains the name |
| Street address | Yes | Can signal neighbourhood and income |
| City or country | Usually | Keep only if location is a real requirement |
| Date of birth, age | Yes | Signals age directly |
| Graduation years | Often | A graduation year is an age estimate |
| School or university names | Often | Prestige bias; keep the degree and subject |
| Personal links (LinkedIn, social) | Yes | One click shows the photo and name |
| Portfolio or code links | Depends | Needed for some roles, but they usually name the person |
| Job titles and dates | No | This is the evidence |
| Employer names | Sometimes | A famous employer works like a famous school |
| Skills, certifications, languages | No | This 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.
- Pick one person to anonymize and a different person to review. The anonymizer sees everything, so they cannot also be the reviewer.
- 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.
- 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.
- 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.
- Hand reviewers the redacted files only. They score or shortlist by id.
- 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:
- All of
basicsexcept the headline: name, email, phone, location and links. - Employer names. Keep them if your reviewers need them; many teams hide them in the first pass because a famous employer works the same way a famous school does.
- School names and education dates, keeping the degree and the subject.
- Per-role locations.
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:
- 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". - 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 hand | Blind view over parsed fields | |
|---|---|---|
| Work per application | Minutes per CV, every time | Set up once, then none |
| Name in a footer or sentence | Easy to miss | Dropped with the field, or caught by the scrub |
| Two-column and designed CVs | Hard to redact cleanly | Same record as any other CV |
| File metadata | Must be cleared by hand | Never shown |
| Changing what is hidden | Re-redact every file | Change one function |
| Works without new software | Yes | No |
Where blind screening falls short
Blind review is a useful tool with real limits. Be honest about them with the people using it.
- The evidence itself carries signals. Employment gaps, the years someone worked, a language listed as native and certain employers can all hint at age, caring duties or origin. Hiding the name does not hide those.
- It only covers the first pass. Bias can come back at the interview. Structured interviews with the same questions and a written scoring guide are the natural next step.
- It can hide context you need. Some roles require a location, a work permit or a portfolio. Keep those, and say why in the hide list.
- It is not a legal shield. Rules on hiring and on automated decisions differ by country. Check what applies where you hire.
When the manual way is enough
You do not need code for blind resume screening when:
- You are hiring for one role with a few dozen applicants. Redacting thirty PDFs is an afternoon. Building a blind view is not worth it for one role.
- You want to test the idea first. Run one role blind by hand, see whether reviewers find it workable, then decide.
- Applications already arrive as form fields. If candidates type their history into your form, you already have a record. Build the allowlist over that and skip parsing entirely.
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.