A desktop app for building CVs that survive Applicant Tracking System parsers. Several people share the app; each keeps one master profile holding everything they have ever done, and derives as many tailored CVs from it as they need — one per role, field, or language.
Exports to PDF (with a real text layer) and Word .docx, and scores every CV against 25 ATS rules as you type.
+------------+--------------------------+------------------+
| People | Section editor | Live preview |
| omer | (forms generated from | (the same HTML |
| berna | the section registry) | that becomes |
| nazan | | the PDF) |
| | - add/remove sections +------------------+
| CVs | - reorder them | ATS panel |
| Embedded | - tick which entries | score, what to |
| General | appear on this CV | fix, job match |
+------------+--------------------------+------------------+
python -m venv .venv && .venv/Scripts/pip install -e ".[dev]".venv/Scripts/python -m cvmakerOn macOS or Linux the paths are .venv/bin/ instead of .venv/Scripts/.
The thing that makes tailoring practical is that a CV is a view over a profile, not a copy of one.
profile.jsonholds every job, degree, project, and skill the person has ever had, each with a stable id.- Each CV file stores references to those ids, plus its own ordering, its own summary, and any per-CV field overrides.
So fixing a job title once fixes it on all three of your CVs. Unticking a role removes it from this CV while leaving it in your profile. Deleting it from the profile tells you which CVs were using it first.
In %APPDATA%/CVMaker/ on Windows — outside this repository, so
nothing personal can be committed by accident. .gitignore blocks
profiles/ as a second line of defence. The only profile in the repo is
the anonymised examples/sample_profile.json.
Writes are atomic (temp file, fsync, replace) and the last 10 versions of
every file are kept in backups/, so a crash mid-save cannot cost you a
profile.
A new CV opens with the five sections recruiters and parsers expect:
Contact Information · Professional Summary · Work Experience · Education · Skills
You can add any of: Projects · Certifications · Languages · Publications · Volunteering · Awards and Honours · Courses and Training · References.
You can reorder sections, hide them, and rename a header — but only to one of that section's recognised synonyms ("Work Experience" → "Professional Experience" → "Employment History"). Free-form headers are deliberately not allowed: parsers use the header to decide what the content underneath means, so inventing one defeats the point of the app.
The score is a heuristic built from published ATS vendor guidance and long-standing recruiter advice. It checks 25 rules across five weighted categories:
| Category | Weight | Checks |
|---|---|---|
| Contact details | 20 | email and phone present and well-formed, location, contact block in the document body |
| Structure | 25 | core sections present and non-empty, recognised headers, reverse-chronological order, no duplicates |
| Content quality | 25 | bullets open with action verbs, at least one measured result per role, bullet length, no first-person pronouns |
| Formatting | 15 | consistent MM/YYYY dates, no smart quotes or emoji, no tables or images, length under two pages |
| Keyword match | 15 | coverage against a pasted job description |
Two behaviours worth knowing:
- Any error-severity finding caps the score at 69. A CV with no email address can never show green, however good the rest of it is.
- Rules with nothing to judge are excluded, not passed. An empty CV scores 7, not 69 — it earns no credit for mistakes it is too empty to make.
It is not a simulation of any particular employer's ATS, and no offline tool can be — vendors do not publish their parsers. A high score means nothing in the CV is known to break parsers. It is not a prediction that you will get an interview.
The keyword panel lists terms from a posting that your CV is missing. It will never insert them for you: keyword stuffing is itself an ATS risk, and a CV you cannot defend in an interview is worse than a lower number.
Both templates (Classic and Compact — they differ in spacing only) guarantee:
- Single column. No tables, text boxes, or floats for content.
- No images, icons, logos, or photographs.
- No header or footer regions; everything is in the document body.
- Calibri / Arial / Georgia at 10–12pt.
- Standard section headers, plain bullets,
MM/YYYYdates throughout. - Ligatures disabled. Left on, QtWebEngine renders "firmware" with an
fi ligature that text extraction returns as
U+FB01, silently breaking keyword matching. This was caught by the test below and is exactly the class of bug the app exists to prevent.
The .docx is generated independently from the same document model, using
Word's built-in Heading 1 and List Bullet styles rather than manual
formatting, so it carries a real outline for parsers to walk.
The preview pane renders the actual PDF and shows it as real A4 pages, using Qt's PDF viewer. It is not an HTML approximation: it is the exported file, so it cannot disagree with what you send. Page breaks, margins and the page count are all real, and the pane says how many pages you are on.
Above it, a layout bar controls page density: margin, body size, line height, section gap, entry gap, heading size, name size, and whether a page break may split an entry in half. Reset returns to the template's defaults.
Settings are stored per CV and only the dials you actually move are saved,
so switching between Classic and Compact still changes everything you left
alone. The same values are applied to the .docx, so the Word file matches
the pages you laid out.
There is deliberately no free placement. Dragging blocks to arbitrary positions requires absolute positioning or text boxes, and ATS parsers cannot read either in reading order. An app that exists to produce parser-safe CVs should not ship the one feature guaranteed to break them, so placement is controlled through order and density instead. A test asserts no layout control is positional.
The app can check GitHub for a newer release: Help -> Check for updates. If one exists it shows the release notes and offers to download the installer and run it, which upgrades in place. Your profiles and CVs are never touched by an update.
The check is opt-in and off by default. It is the only time this app touches the network, and the request carries nothing but a User-Agent - GitHub sees an IP address and the app version. There is a test that walks the module's syntax tree asserting it cannot even reference profile data, so that guarantee cannot rot silently.
Publishing a release is two commands:
python tools/release.py buildpython tools/release.py tag --pushThe tag triggers .github/workflows/release.yml, which builds the
installer on a Windows runner and attaches it to a GitHub release. That
release is what the update check reads.
The workflow refuses to publish if the tag and VERSION disagree - a
release tagged v1.1.0 containing a 1.0.9 build would offer users an
"update" that installs an older app. release.py tag derives the tag from
VERSION so the two cannot drift apart by hand.
Set the repository the app checks in cvmaker/core/updates.py
(DEFAULT_REPO). Note that the installer is unsigned, so each update will
show the SmartScreen warning; the dialog says so rather than letting it
come as a surprise.
python tools/release.py buildThat bumps the patch version, packages the app with PyInstaller, and
compiles a Windows installer with Inno Setup, leaving
dist/CVMaker-Setup-<version>.exe.
The installer gives the user a wizard with a choose install location page, a Start Menu entry, an optional desktop shortcut, and a proper uninstaller registered in Add/Remove Programs. It installs per-user by default, so no administrator prompt — an all-users install is offered on the privileges page for anyone who wants it.
Uninstalling never deletes your CVs. The program goes in the folder you
chose; your data lives in %APPDATA%\CVMaker, and the uninstaller does not
touch it.
Build prerequisites, both one-time:
winget install -e --id JRSoftware.InnoSetuppython -m pip install -e ".[dev]"If Inno Setup is somewhere unusual, point INNO_SETUP_ISCC at ISCC.exe.
To build just the application folder without an installer, use
--exe-only.
There is no application icon yet. Drop a .ico at installer/cvmaker.ico
and both the executable and the installer pick it up on the next build.
VERSION at the repository root is the single source of truth. The package
metadata, the window title, Help → About, python -m cvmaker --version,
and the installer's Add/Remove Programs entry all read from it.
One rule: a build increments the patch of whatever is currently there.
python tools/release.py version # what is it nowpython tools/release.py set 1.1.0 # take manual controlpython tools/release.py bump --minor # 1.0.203 -> 1.1.0, without buildingSo automatic builds walk 1.0.1, 1.0.2 … 1.0.203. When you decide something
big has changed, set 1.1.0. The next automatic build is 1.1.1, and
counting continues from there. No special cases, and no separate "release
mode" to remember.
Two guarantees worth knowing:
- A failed build rolls the version back. A version number should mean "an artifact with this label exists", not "a build was attempted".
AppIdin the installer script never changes. That is what makes installing 1.0.4 over 1.0.3 an upgrade rather than a second entry in Add/Remove Programs.
.venv/Scripts/python -m pytest400 tests. cvmaker/core/ never imports Qt — enforced by a test — so all
the interesting logic runs headlessly.
The one that matters most is the extraction test: it renders a full CV
to PDF, pulls the text back out with pdfminer.six, and asserts every
field is present and in display order. That is the closest local proxy
for "will an ATS read this", and it is what protects every future template
change.
cvmaker/
core/ models, section registry, storage, resolve, i18n, view, ats/, keywords
render/ html.py (Jinja2), pdf.py (QtWebEngine), docx_writer.py
ui/ PySide6 panes; thin, holds no CV logic
templates/ Jinja2 HTML + Classic/Compact CSS
One rule holds it together: core/ never imports Qt. Data flows one way —
Profile + CV → ResolvedCV → {HTML, PDF, DOCX, ATS findings} — and the
analyzer reads the resolved document, never the widget state, so the score
always describes exactly what will be exported.
Design and plan documents live in docs/superpowers/.
MIT.