Skip to main content
Glama
README.md
# honest-cv

[![CI](https://github.com/JesusCabreraReveles/honest-cv/actions/workflows/ci.yml/badge.svg)](https://github.com/JesusCabreraReveles/honest-cv/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/JesusCabreraReveles/honest-cv/blob/master/LICENSE)

<!-- mcp-name: io.github.JesusCabreraReveles/honest-cv -->

**Honest, ATS-safe CVs tailored to each role, generated by your AI agent.** honest-cv is an MCP
server, a CLI and a Claude Code skill. It keeps one source of truth about your experience and
turns it into a PDF and DOCX for each type of role. It never claims a skill you don't have.

[Leer en español](https://github.com/JesusCabreraReveles/honest-cv/blob/master/README.es.md)

![Demo: skill checklists including your own, scoring a job post, a verified build, a cover letter that refuses a skill the person lacks, LinkedIn texts and logging the application](https://raw.githubusercontent.com/JesusCabreraReveles/honest-cv/master/docs/assets/demo.gif)

## Why

Many CVs that get no callbacks don't fail on design. They fail on targeting, and on details that
quietly break text extraction. AI makes the opposite problem easy: a model will happily stuff a CV
with keywords you can't defend in an interview. honest-cv is built around three ideas:

- **The code enforces honesty, not a prompt.** Every skill has a level. A skill at familiarity
  level is always labeled as such, and a skill you don't have is never printed. Raising a level
  takes written evidence and *your* confirmation, which the server asks you for directly. The
  model can't confirm on your behalf.
- **Gaps are measured against your own inventory.** For each keyword a job post asks for,
  honest-cv tells you which of three cases it is:
  - you have it, but this CV doesn't show it;
  - you only know it at familiarity level;
  - you don't have it.
- **ATS safety is checked on every build.** Every PDF goes through two text extractors (pypdf and
  pdfminer.six). The build checks that the text is one column, has no ligatures, keeps each date on
  its role's line, follows the right reading order and includes every bullet.

The server is deterministic and needs no API key. Your MCP client's model runs the conversation;
honest-cv validates, builds, verifies and measures. It keeps your profile in a folder on your
machine and never sends it anywhere: see [Privacy](#privacy) for what your AI client does with it.

<p align="center"><img src="https://raw.githubusercontent.com/JesusCabreraReveles/honest-cv/master/docs/assets/ada-platform-en.png" width="560" alt="A generated one-page CV for the fictional Ada Example"></p>

## Quick start

honest-cv needs [uv](https://docs.astral.sh/uv/). Typst and the fonts ship inside the package,
so there is nothing else to install.
It runs on Linux (glibc) on x86_64 and ARM64, macOS on Intel and Apple
Silicon, and Windows on x64. Alpine Linux and Windows on ARM aren't supported, because the `typst`
package doesn't publish wheels for them.

### Claude Code

```bash
claude mcp add honest-cv -- uvx honest-cv serve --profile ~/cv-profile
```

To install the skill as well, which guides the interview and the tailoring:

```bash
mkdir -p ~/.claude/skills/honest-cv
curl -fsSL https://raw.githubusercontent.com/JesusCabreraReveles/honest-cv/master/skills/honest-cv/SKILL.md \
  -o ~/.claude/skills/honest-cv/SKILL.md
```

Then follow [Your first CV, step by step](#your-first-cv-step-by-step).

### Cursor

Add this to `~/.cursor/mcp.json`, or to `.cursor/mcp.json` in a project:

```json
{
  "mcpServers": {
    "honest-cv": {
      "command": "uvx",
      "args": ["honest-cv", "serve", "--profile", "/absolute/path/to/cv-profile"]
    }
  }
}
```

### Claude Desktop

Edit `claude_desktop_config.json`:
- on macOS, `~/Library/Application Support/Claude/`;
- on Windows, `%APPDATA%\Claude\`.

Add the same `mcpServers` block as in the Cursor example and restart the app. If the app can't
find `uvx`, use its absolute path (`which uvx`).

### How each client confirms a skill upgrade

Raising a skill's level needs your own confirmation, which the server asks for through MCP
elicitation. Not every client supports it yet:

| Client | Where you confirm |
|---|---|
| Claude Code | In the chat: a form with one "no / sí" field per upgrade, up to 10 at a time. Only the ones you answer "sí" are saved; Decline saves nothing. |
| Claude Desktop | In a terminal. The server refuses the change and gives you the command to run: `uvx honest-cv skill <profile> "<name>" <level> --evidence "..."`. |
| Other clients | In the chat if they support elicitation, otherwise in a terminal as above. |

## Your first CV, step by step

This walkthrough uses Claude Code; any MCP client works the same way, only the setup differs.

**1. Install uv and add honest-cv.** One time only. On macOS or Linux:

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh        # Windows: see docs.astral.sh/uv
claude mcp add honest-cv -- uvx honest-cv serve --profile ~/cv-profile
```

`~/cv-profile` is the folder where your profile will live. It doesn't have to exist yet, and it should
not be inside a public repository. Install the skill too ([Claude Code](#claude-code) above).

**2. Start the interview.** Open Claude Code in any folder (`claude`) and write, in your own
language, *"Interview me for my CV"*, or run `/mcp__honest-cv__interview`. The assistant keeps
your language for the whole conversation. You can answer by voice with your system's dictation
(on macOS: System Settings → Keyboard → Dictation): it reads names and numbers back to you before
saving them, since dictation mishears them. It goes like this:

> **You:** Interview me for my CV.
>
> **Assistant:** The folder `~/cv-profile` doesn't exist yet. Shall I create it? … The whole interview
> can take a few hours, but everything is saved as we go. A first session with your contact
> details, your two most recent roles and the skills checklist is enough for a first CV. Do you
> have a LinkedIn data export or a JSON Resume to start from?

- **Import, if you can** (optional). Download LinkedIn's data export (see
  [Import](#import-what-you-already-have)) and give the assistant the path to the ZIP. Your roles,
  education and achievements come in, and skills arrive without a level for you to mark.
- **Contact, roles and education.** It asks a few at a time and reads names and numbers back
  before saving. Then it asks once about certifications, courses and anything you wrote.
- **Achievements, role by role.** For each role: what the team did, what *you* did, and what
  changed, with numbers. It drafts a bullet and saves it only after you approve the wording.
- **Skills.** It offers a checklist, e.g. `backend-jvm` + `engineering-lead`, or your own for any
  profession. Open `~/cv-profile/checklist.md` in your editor, put P, L, F or X on each line, add where
  you used it after " — ", save, and tell the assistant *"done"*. It asks for the evidence it is
  missing, in groups. Then a form asks you to confirm the upgrades: only what you answer
  "sí" is saved.
- **Your biggest achievements.** One achievement usually covers several skills. It then asks
  about your main P skills that no achievement shows yet; the rest can wait for a job post.

**3. Build your first CV.** The assistant proposes one *variant* per kind of role you apply to
(for example "platform" or "engineering manager"): title, summary and which bullets to show. Once
you approve it:

> **You:** Build my platform CV in English and Spanish.

The PDF and DOCX land in `~/cv-profile/out/`, already checked for ATS safety. Without an AI client,
the same is `uvx honest-cv build ~/cv-profile -v platform -l en`.

**4. Tailor it to a job post.** Paste the post, or run `/mcp__honest-cv__tailor_for_job`:

> **You:** Here is a job post: *(paste it)*. How well do I fit, and what should I change?

You get the coverage of each variant, the missing keywords split into *you have it but this CV
doesn't show it*, *familiarity only* and *you don't have it*, plus the ones you marked in your
checklist but haven't backed with evidence yet (it asks where you used them), a tailored variant to approve, and
the build. Then it can draft a cover letter, which `check_claims` checks before it is built, and
log the application with the post's text and what to prepare for the interview. When you get the
interview, *"help me prepare the interview"* drafts likely questions with answers from your own
achievements.

**5. Come back any day.** Say *"let's continue my CV interview"*: it starts from what is missing.
When you need to update your LinkedIn profile, say *"update my LinkedIn"*.

## Your profile

A profile is a folder with four YAML files. Create one with `honest-cv init ~/cv-profile`, or let
the `interview` prompt fill it in with you. Keep it out of public repositories.

| File | What it holds |
|---|---|
| `profile.yaml` | Contact details, roles, education, certifications, publications, courses and languages. |
| `bullets.yaml` | A pool of achievements, each with an `id`, a `role` and `tags`. |
| `skills.yaml` | Your skill inventory, with levels and evidence. |
| `variants.yaml` | One CV plan per type of role. A variant can inherit from another with `extends:`. |

Any text can be a single string or one string per language: `{en: ..., es: ...}`. CVs can be
built in English and in Spanish.

| Level | Meaning | On the CV |
|---|---|---|
| `P` | Used in production at a job | Shown |
| `L` | Your own project or lab | Shown |
| `F` | Familiarity | Only on the line labeled "Familiarity" |
| `X` | You don't have it | Never shown, but recorded so gap reports are honest |

### Skill checklists

Recording dozens of skills one question at a time is slow. A checklist is a Markdown file you mark
in your editor, one letter per line:

```markdown
## Messaging and streaming
evidence: Payments platform at Acme, 2020–2026
- [P] Kafka — reconciliation flow, 2M events a day · years: 6 · last year: 2026
- [F] RabbitMQ — a workshop and the official tutorials
- [X] NATS
```

Text after " — " is the evidence, and an `evidence:` line under a heading covers that section.
Lines without evidence are never saved: they come back as pending, and your assistant asks you
about them in groups. Upgrades are confirmed once for the whole file.

Checklists come in English and Spanish (`--lang es`): the Spanish names are synonyms of the
English ones, so a skill you mark in Spanish still matches a job post in English.

```bash
honest-cv checklist list                                     # backend-jvm, platform-sre, ...
honest-cv checklist new ~/cv-profile backend-jvm engineering-lead
honest-cv checklist apply ~/cv-profile ~/cv-profile/checklist.md
```

**Any profession works.** The built-in checklists cover software (backend, frontend, mobile, data,
platform/SRE, QA, security, ML/AI, embedded, game development, architecture, engineering
leadership) and data analysis/BI, product and project management, UX design, marketing, sales,
customer support, finance and accounting, human resources, supply chain, nursing and teaching. A template is just a
Markdown list in this format: save your own (or one someone shared) in `~/cv-profile/checklists/`
and it shows up in `checklist list` and combines with the others in `checklist new`, which also
takes a path. A template can declare synonyms, such as `same: Work Breakdown Structure = WBS`,
and the job score, the gaps and the letter check use them, along with its skill names. Start from
[`examples/checklists/event-planning.md`](https://github.com/JesusCabreraReveles/honest-cv/blob/master/examples/checklists/event-planning.md). You can also apply a file you marked yourself, in English or Spanish (`años`,
`último uso`). When you're done, honest-cv names the P and L skills that no achievement shows, and
your assistant asks you for one real example of each.

```bash
honest-cv checklist list ~/cv-profile                        # yours too, e.g. accounting
honest-cv checklist new ~/cv-profile accounting engineering-lead
```

New checklists for other kinds of roles are welcome: see [CONTRIBUTING.md](https://github.com/JesusCabreraReveles/honest-cv/blob/master/CONTRIBUTING.md).

See [`examples/ada/`](https://github.com/JesusCabreraReveles/honest-cv/tree/master/examples/ada) for a complete, fictional profile with three variants, a job
post and its keywords.

## Privacy

- **honest-cv itself makes no network calls.** No accounts, no telemetry, no API keys. Your
  profile is a folder of YAML files on your machine, and the PDF engine and fonts run locally. You
  can check it: every command works with the network turned off.
- **Your AI client is a different matter.** Whatever the client's model reads through the tools
  goes to that model's provider, under the provider's terms, like anything else you share in a
  chat. That includes your skills, bullets and the text of your CVs.
- **Some clients copy files to the cloud.** To edit files directly, Claude Desktop may ask for
  access to the profile folder and say that the files leave your device. honest-cv's editing tools
  make that unnecessary: decline the request and ask the assistant to use the tools.
- **Running it on a hosted service takes it off your machine.** Directories such as Glama offer to
  deploy honest-cv on their servers or try it in the browser. It works, but then your profile and
  your CVs live on that service, under its terms. For your real data, run it on your own computer
  with `uvx honest-cv`.
- **If you want nothing to leave your machine,** use only the CLI (`build`, `verify`, `score`,
  `skill`) and edit the YAML files yourself.

## Import what you already have

`honest-cv import` reads a [JSON Resume](https://jsonresume.org) file or LinkedIn's data export
(Settings → Data privacy → Download your data, a ZIP of CSVs, or its folder if your browser
unzipped it). Ask for the full archive: the basic one, offered when you pick files, has only your
headline and summary, without roles. Contact, roles, education,
certifications, languages and each role's achievements go into your profile; with a headline and
a summary it also creates a first variant, so `build` works right away.

- It only adds what your profile lacks and never overwrites. Try `--dry-run` first.
- Imported achievements are tagged `imported`: review their wording.
- **Skills arrive without a level**, because neither format says whether you used a skill in
  production. They go to `imported-skills.md`, a checklist for you to mark.

LinkedIn's "Save to PDF" profile changes layout without notice, so it isn't parsed: with an MCP
client, the `import_linkedin_pdf` prompt reads its text and proposes each section for you to
approve.

## Track your applications

`honest-cv apps` keeps a log in `applications.yaml`, inside your profile folder: which variant and
files went to which company, and each status with its date (`draft`, `applied`, `interview`,
`offer`, `rejected`, `withdrawn`). Logging the same company and role again updates it. With an MCP
client, your assistant offers to log an application after building the CV or letter, and
`list_applications` points out the ones with no news for two weeks. It keeps a copy of each file
you sent in `sent/`, since the next build overwrites `out/`, and the job post's text in `posts/`,
since links expire, and what to prepare for the interview: ask your assistant
*"what did I need to prepare for Acme?"*.

When you get the interview, say *"help me prepare the Acme interview"* (or run the
`interview_prep` prompt). For each keyword of the saved post, honest-cv lays out your level,
your evidence and the bullets that back it, starting with the ones in the CV you sent. Your
assistant drafts the likely questions with STAR answers taken only from those bullets, honest
answers for what you only know at familiarity level or don't have, and questions to ask them. The
notes you approve are saved in `prep/`. Without an AI client: `honest-cv apps prep ~/cv-profile
acme_sre -k "Kafka,Scala"`.

## LinkedIn

`honest-cv linkedin` turns a variant into LinkedIn's texts, each within its limit: the headline
(your title and up to five production skills), About (the summary and impact bullets), one
description per position from its bullets, and the skills list. It writes them to a Markdown file
to copy from. The skills are P and L only: LinkedIn can't label familiarity, so F skills stay out,
and X skills never appear. With an MCP client, the `linkedin_profile` prompt polishes the wording
and checks it with `check_claims`.

## Export

To go the other way, `honest-cv export` writes a variant as JSON Resume for other CV tools and
sites, with the same rules as the CV: X skills never appear, and F skills only in a "Familiarity"
group with that level.

## Cover letters

The `cover_letter` prompt drafts a one-page letter for a job post from your own bullets. Before it
is built, `check_claims` reads the text against your profile:

- **Problems block the build:** a skill you marked X, or a familiarity skill presented as
  experience ("I use Rust in production" when Rust is F; "I am familiar with Rust" is fine).
- **Warnings are for you to review:** a technology neither your inventory nor your bullets show,
  or a number that appears nowhere in your profile. If it is true, record it first; if not, it
  comes out of the letter.

The letter uses the CV's fonts and passes the same ATS checks. Without an AI client, write it in a
text file (paragraphs separated by blank lines) and use `honest-cv letter check` and `letter build`.

## CLI

Run any command with `uvx honest-cv ...`, or install it once with `uv tool install honest-cv` to
type just `honest-cv`.

```bash
honest-cv init ~/cv-profile                        # create the profile templates
honest-cv build ~/cv-profile -v backend -l en -l es # PDF + DOCX, verified on every build
honest-cv verify any-cv.pdf --text                  # check any PDF, even one made elsewhere
honest-cv score ~/cv-profile -k "Kubernetes,Go" -p "Rust"   # coverage per variant, with gaps
honest-cv skill ~/cv-profile "Rust" L --evidence "CLI tool on GitHub"  # asks you to confirm
honest-cv import ~/cv-profile linkedin-export.zip --dry-run         # or resume.json
honest-cv export ~/cv-profile -v backend -l en                       # JSON Resume
honest-cv apps log ~/cv-profile --company Acme --role "SRE" -s applied -v platform
honest-cv apps list ~/cv-profile                                     # latest first, with counts
honest-cv apps prep ~/cv-profile acme_sre -k "Kafka,Scala"           # what backs each keyword, for the interview
honest-cv linkedin ~/cv-profile -v platform -l en                    # texts to paste into LinkedIn
honest-cv remove skill ~/cv-profile "Kafka"                          # asks you to confirm
honest-cv letter check ~/cv-profile letter.txt -k "Kubernetes,Go"     # claims your profile doesn't support
honest-cv letter build ~/cv-profile letter.txt --company Acme --role "Platform Engineer" -l en
honest-cv serve --profile ~/cv-profile              # the MCP server, over stdio
```

`verify` works on any PDF. Use `--text` to see exactly what a parser extracts. Its
`single_column` check also catches a common defect: a date aligned to the right, which parsers
split away from its role.

## MCP tools and prompts

| Name | What it does |
|---|---|
| `init_profile` | Creates a profile folder with commented templates. |
| `get_inventory` | Returns your skills, grouped by level, with years and evidence. |
| `set_skill` | Adds or changes a skill. To raise its level it needs evidence and asks you to confirm. |
| `set_skills` | Adds or changes several skills at once. Upgrades are confirmed with one question that lists each change. |
| `checklist_list` | Lists the built-in checklists. |
| `checklist_new` | Merges built-in checklists into one file, prefilled from your inventory. |
| `checklist_apply` | Saves the skills marked in a checklist and returns the lines that still need evidence. |
| `add_bullet` | Adds an achievement to the pool. |
| `remove_skill`, `remove_bullet`, `remove_role`, `remove_publication` | Delete for good, and remove every use in the variants. A role with bullets needs `with_bullets`. Clients ask before running them. |
| `set_contact` | Changes contact details: name, email, phone, location, links. |
| `set_profile_details` | Replaces the certifications list, the courses list and the spoken-languages line. |
| `upsert_role` | Adds a role in date order, or changes fields of an existing one. |
| `upsert_education` | Adds or changes an education entry. |
| `upsert_publication` | Adds or changes a book, article or paper the person wrote. Each variant shows all of them, or the ones it lists. |
| `save_variant` | Adds or replaces a variant. Every reference is checked before saving. |
| `list_variants` | Lists the variants after inheritance, with their bullet texts and the bullets no variant uses. |
| `build_cv` | Renders a PDF and/or DOCX and verifies it. |
| `verify_pdf` | Runs the ATS checks on any PDF and returns its extracted text. |
| `score_job` | Measures the keyword coverage of each variant. The client extracts the keywords. |
| `gap_report` | Groups missing keywords by what you can honestly do about each one. |
| prompt `interview` | Fills your inventory and bullet pool, one question at a time. |
| `check_claims` | Checks any text against your profile: blocks X skills and familiarity sold as experience, flags what the profile doesn't show. |
| `build_letter` | Renders a cover letter to PDF and DOCX, one page, with the CV's ATS checks. Refuses while `check_claims` finds problems. |
| prompt `tailor_for_job` | Scores the variants, explains the gaps, adapts a variant and rebuilds. |
| prompt `cover_letter` | Drafts a letter for a job post from your bullets, checks its claims and builds it. |
| `import_profile` | Imports JSON Resume or a LinkedIn data export, adding only what is missing. For a LinkedIn PDF it returns the text. |
| `export_json_resume` | Exports a variant as JSON Resume; X skills never appear and F skills keep their label. |
| `log_application` | Records an application (company, role, CV variant, files sent, the post's text, what to prepare) or a new status, with its date. |
| `linkedin_profile` | LinkedIn headline, About, position descriptions and skills (P and L only) from a variant, within LinkedIn's limits. |
| `list_applications` | Lists applications, latest first, with a count per status and the ones with no news for 14+ days. |
| `interview_prep` | For an application: the saved post and, per keyword, your level, evidence and the bullets that back it. Saves the prep notes. |
| prompt `import_linkedin_pdf` | Turns the PDF from LinkedIn's "Save to PDF" into a profile, section by section, with your approval. |
| prompt `linkedin_profile` | Polishes the LinkedIn texts of a variant without adding claims, checked with `check_claims`. |
| prompt `interview_prep` | Prepares an interview: likely questions, STAR answers from your own bullets, honest answers for the gaps. |

## ATS guarantees

The template enforces these. `verify` checks them on every build, and the test suite runs in CI on
Linux, macOS and Windows:

- One column. No layout tables, images, icons, headers or footers.
- Ligatures and kerning off, no hyphenation, and terms like "CI/CD", URLs and emails never split
  across lines.
- Reading order equals source order. Dates sit on the same line as their role.
- Text is extractable by both pypdf and pdfminer.six, and the two agree.
- The fonts (Carlito, metric-compatible with Calibri) are embedded and identical on every machine.

## Development

```bash
uv sync
uv run pytest
uv run honest-cv build examples/ada -l en -l es
```

The repository's `.mcp.json` registers `honest-cv-dev` with the fictional profile, so Claude Code
in this folder can use your working copy. See [CONTRIBUTING.md](https://github.com/JesusCabreraReveles/honest-cv/blob/master/CONTRIBUTING.md).

## License

MIT. The bundled Carlito font is under the SIL Open Font License; see
`src/honest_cv/render/fonts/OFL.txt`.

TDQS

A4.1/5.0

Scored across 31 tools

Disambiguation4/5

Despite 31 tools, the descriptions carefully staked out distinct boundaries (singular set_skill vs plural set_skills vs checklist_apply; build_cv vs build_letter vs export_json_resume vs linkedin_profile; score_job vs gap_report vs check_claims). A few clusters are dense—upsert_role/remove_role, remove_bullet/remove_role, and the several output builders—but the cross-references ('use X instead') keep them distinguishable.

Naming Consistency4/5

Predominantly consistent snake_case verb_noun (get_inventory, set_skill, upsert_role, add_bullet, build_cv, score_job). Minor deviations: a few noun-first names (checklist_list, checklist_new, checklist_apply, linkedin_profile, interview_prep, gap_report) break the otherwise uniform verb-first pattern.

Tool Count2/5

31 tools is well above the 16-25 heavy threshold and feels over-decomposed for a CV/profile domain. Removing a role/bullet/skill/publication are each their own tool, and multiple near-parallel setters (set_skill/set_skills/set_contact/set_profile_details) inflate the surface beyond what the task needs.

Completeness4/5

Coverage is broad: profile lifecycle, import, skills, roles, bullets, education, publications, contact, variants, CV/letter rendering, PDF verification, applications, interview prep, job scoring and gap analysis. Minor gaps exist (no explicit remove_variant or remove_education), but core workflows have no dead ends.

Maintenance

ActivityMaintained
ResponsivenessResponsive