Turn a job description into a fully tailored resume + cover letter, in DOCX and PDF, with a skills-gap report and a 0–100 match score — while keeping your clients, employers, locations and work durations locked so they can never drift.
Every run rewrites the professional summary, the ordering of your skills, and the project context + bullets of every engagement so the technologies and responsibilities the job asks for sit front and centre — then force-reverts the client names and start/end dates from your master profile.
- CLI + local web app with an application tracker (every run saved, status, notes, match/coverage view, re-render).
- Every run also emits a defense sheet — the claims your profile doesn't back, each paired with the real work to pivot to in a screen — plus plain-text and Markdown copies of the resume.
- When one reaches a screen, one command builds the interview prep pack: the questions they will ask, STAR stories from your real material, and every stretch claim paired with a pivot.
- Any AI provider — Claude Code subscription, Anthropic / OpenAI / Gemini / OpenRouter API, or a local model (Ollama, LM Studio, llama.cpp, …). Pick it on the Settings tab.
- Multiple master profiles (e.g. Backend vs Data vs Platform); "Auto" routes a JD to the best-fitting one by required years + role.
- Resume templates —
standard(~2 pages),compact(tighter type + spacing, fewer bullets, aims for one page) ordetailed— picked per run or re-rendered later with no new AI call. - CSV export of the application tracker, optional Gmail status suggestions (read-only — you confirm every status change), and a daily submissions chart (open vs. closed) on the tracker.
- Optional one-box Proxmox LXC + Tailscale self-host —
deploy/DEPLOY.md.
pip install -e . # from a clone; or: ./setup.sh
resume-tailor init # scaffolds profile/ + settings.yaml
# try it right now, no API key, no network:
resume-tailor tailor --engine mock --jd examples/sample_job.txt
$EDITOR profile/profiles/default/master_profile.yaml # your real resume, once
resume-tailor web # http://127.0.0.1:8000 → Settings tab picks the providerPython 3.11+. LibreOffice is optional (pixel-faithful PDF from the DOCX); without it, a built-in renderer mirrors the layout.
docker compose up --build # http://127.0.0.1:8000 , data in ./rt-dataThe container can't use the claude-cli provider — set an API key in .env
(see .env.example) or open the Settings tab and pick one.
| provider | key env | notes |
|---|---|---|
claude-cli |
— | local claude CLI / Claude Code subscription — no API bill (default) |
anthropic |
ANTHROPIC_API_KEY |
native Claude API — pip install '.[api]' |
openai |
OPENAI_API_KEY |
ChatGPT API |
gemini |
GEMINI_API_KEY |
Google Gemini (via its OpenAI-compatible endpoint) |
openrouter |
OPENROUTER_API_KEY |
one key, hundreds of models |
ollama |
— | local models — ollama serve + ollama pull … |
custom |
optional | any OpenAI-compatible URL: LM Studio, vLLM, llama.cpp, a gateway |
mock |
— | offline, deterministic, canned output — for a demo or CI |
Everything except claude-cli and anthropic goes through one small stdlib
OpenAI-compatible client — no extra packages. Keys live in profile/secrets.yaml
(chmod 600, git-ignored) or an env var (which wins), never in settings.yaml.
resume-tailor engines shows what's configured.
resume-tailor tailor --jd job.txt --company "Acme" --role "Senior Java Engineer"
pbpaste | resume-tailor tailor # JD on stdin
resume-tailor tailor --jd job.txt --engine openai --model gpt-4o
resume-tailor gap --jd job.txt # gap report only, no documents
resume-tailor prep # interview prep for the last run
resume-tailor web # dashboard + tracker
resume-tailor engines / models / profiles # what's configuredUnder the current directory (or $RESUME_TAILOR_HOME): profile/ (your master
profiles + settings.yaml), data/applications.db (the tracker), outputs/
(one folder per run). None of it is committed.
Your JD and the relevant parts of your master profile are sent to whichever
provider you pick. claude-cli and ollama/custom (a local model) keep that on
your machine / subscription; the hosted APIs send it to that vendor. mock sends
nothing.
Keep one profile per target role/seniority. Manage them in the web UI (Profiles → create / clone / edit — a structured form, no YAML), or from the CLI:
resume-tailor profile new ai-engineer --from default
resume-tailor profile set ai-engineer --years 7 --role-family "ai engineer,llm,ml" --label "Senior AI Engineer"
resume-tailor profiles # list them
resume-tailor tailor -p auto --jd job.txt # routes by the JD's required years + roleEach profile carries its own declared years of experience, role family, and section
layout (which sections, order, headings, per-section max items, plus custom: sections that
print verbatim). On the dashboard, Auto (the default) reads the JD's min_years + role and
picks the profile that fits — a 5-year JD lands on your 6-year profile, a 7-year JD on your
senior one. A profile left on the default five sections behaves exactly as before.
When an application actually reaches a screen, build the prep pack for it — one AI call, on demand, so run time stays where it is and you only spend it on the jobs that got a reply:
resume-tailor prep # the most recent run
resume-tailor prep app_2d56f497c6 # or a specific one
resume-tailor prep --no-llm # deterministic scaffold, no AI callinterview_prep.md gives you likely technical questions per must-have (weight-3
first, each tagged screening or interview.
Every tailor run is recorded in data/applications.db (SQLite).
resume-tailor apps list [--status applied] [--profile ai-engineer]
resume-tailor apps show <id>
resume-tailor apps set-status <id> interview # draft|applied|screening|interview|offer|rejected|archived
resume-tailor apps note <id> "recruiter call 9/12"
resume-tailor apps export [--out FILE] # CSV — same as the web "Export CSV" button
resume-tailor apps gmail-scan # optional, see "Gmail status suggestions" below
resume-tailor apps stats [--days 30] # daily submissions, open vs. closedThe Stats link on the Applications page charts daily submissions (tracked
runs no longer left in draft) over the last 7/14/30/60/90 days, split into
open (applied, screening, interview, offer) and closed (rejected or
archived) — plus an all-time "still open" headline count. resume-tailor apps stats prints the same breakdown as a terminal bar chart. No JS or chart
library — the web chart is server-rendered inline SVG with native hover
tooltips and a plain-HTML table fallback.
The web app (resume-tailor web) is the friendlier view: submit a JD, watch the
job run live, download the files, read the match score + coverage map, set status,
keep notes, re-render (optionally with a different template).
Scan Gmail for updates on the Applications page (or resume-tailor apps gmail-scan) searches Gmail for replies about applications still applied /
screening / interview, and suggests interview / screening / offer / rejected
from the subject + snippet — a plain keyword match, no AI call. Nothing changes
until you confirm the suggestion. One-time setup: pip install 'resume-tailor[gmail]', enable the Gmail API + create a Desktop app OAuth
client in Google Cloud Console, save its JSON as profile/gmail_client_secret.json,
then resume-tailor gmail-auth (opens a browser). Full walkthrough in
deploy/DEPLOY.md § Gmail.
extract requirements → gap analysis → draft resume+cover letter → self-critique & revise ×N → deterministic coverage + match score → render. The requirement
extraction ranks each must-have by prominence, pulls verbatim ATS keywords,
hard gates (exact years, clearance, cert) and a role archetype. Every
tailored bullet is tagged with the master material it came from — or stretch if
aggressive mode added it. review_rounds (default 2, --review-rounds /
dashboard override, 0 = classic single draft) is how many critique/revise passes
run. Variants (--variants aggressive,conservative,ic,lead or the dashboard
checkboxes) produce each angle in its own subfolder plus a comparison.md and a
recommendation; the app can "promote" the one you pick.
Each run creates outputs/<company>_<role>_<date>/:
| file | what |
|---|---|
resume.docx / resume.pdf |
tailored resume, standard layout |
resume.txt / resume.md |
same resume as plain text (ATS paste / email body) and Markdown |
cover_letter.docx / cover_letter.pdf |
"why I fit this requirement" write-up |
defense.md |
every claim your master profile doesn't fully back — each stretch bullet paired with the real work to pivot to, plus gates / gaps / a pre-submit checklist |
interview_prep.md |
only after resume-tailor prep — likely questions per must-have, STAR stories, behavioural, screening logistics, landmines |
match.md / match.json |
0–100 fit score, component breakdown, concrete "what's weak" list |
coverage.md / coverage.json |
per must-have: is it in the summary / skills / a bullet; hard-gate verdicts; every stretch bullet |
gap_report.md |
matched / partial / missing skills + required-vs-available years |
comparison.md |
only with --variants: the angles side by side + a recommendation |
tailored_profile.yaml |
the structured tailored content (bullets carry their source) — hand-edit and re-render |
requirements.json |
what the AI extracted from the JD (ranked skills, ATS keywords, gates, archetype) |
job.txt |
the JD you fed in |
warnings.txt |
anything the invariant-lock had to correct |
<angle>/… |
with --variants: a full set per angle |
Edit tailored_profile.yaml and regenerate the documents without another AI call:
resume-tailor render outputs/acme_senior-java-engineer_2026-09-08/tailored_profile.yamlprofile/profiles/<name>/master_profile.yaml is your single source of truth
(a legacy single profile/master_profile.yaml is auto-migrated to
profiles/default/ on init).
- Locked every run: each experience entry's
client,employer,location,start,end. - Rewritten every run:
summary,skill_groups(and their order), each entry'srolewording,summaryline andbullets. - Put everything you have ever worked with into
skill_groups, each entry'senvironment, andbullet_library. The richer it is, the better the result.
- default (aggressive): projects are rewritten to match the JD; skills the JD
wants that aren't in your profile are woven into plausible points in your real
timeline. Review
gap_report.md— themissinglist tells you what was added beyond what your profile documented. --conservative: only rephrases and reorders experience you actually listed; missing skills are left out.
Edit them on the web Settings tab, or in profile/settings.yaml (every key is
documented in settings.example.yaml):
active provider, per-provider model + base URL, make_pdf, page_size
(letter/a4), accent_color, default resume_template, default review_rounds,
match-score weights. Env overrides: RESUME_TAILOR_ENGINE, RESUME_TAILOR_MODEL,
RESUME_TAILOR_HOME.
API keys entered here are written to profile/secrets.yaml (chmod 600,
git-ignored) — never to settings.yaml; an env var always overrides. Save &
test does a one-token call against the provider; Refresh models lists what a
local server (Ollama, LM Studio, …) actually has pulled.
For claude-cli the aliases opus / sonnet (default) / haiku trade quality
for speed/cost and map to the right id per provider; any other value (a full model
id, sonnet[1m], …) passes straight through. resume-tailor models lists them.
Override one run with --model opus or the dashboard's Model dropdown (your
pick is remembered). For the other providers, set the model on the Settings tab.
For most people docker compose up or pip install + resume-tailor web behind
your own reverse proxy is enough. deploy/DEPLOY.md is the
author's specific setup — a Proxmox LXC reachable only on a Tailscale tailnet,
systemd service, HTTPS via tailscale serve, headless claude token so there's
still no API bill. It's one example, not a requirement.
pip install -e ".[api,dev]"
pytest # fully offline via the `mock` engine
ruff check .The DOCX is built to one fixed structure: centered name + title + contact line,
then Professional Summary, Technical Skills, Professional Experience
(client — vendor, role | location, date range, bullets), Education, Certifications.
Tune fonts/spacing/heading rules in src/resume_tailor/render_docx.py; the PDF
fallback in render_pdf.py mirrors the same layout. If you have a required
house template, share it and it can be wired in as the renderer.
Three named templates (RESUME_TEMPLATES in render_docx.py) trade density
for page count without touching that structure — standard (default),
compact (smaller type, tighter spacing, caps bullets/skills per role — aims
for one page), detailed (roomier spacing, no cap). Pick one per run
(--template / dashboard dropdown), set a default (resume_template: in
settings), or change an existing run's later with resume-tailor render --template … / the app page's Re-render form.

