File: README.md
AI Job Search
The job search that runs on your machine.
An AI-powered job application framework built on Claude Code. Fork it, fill in your profile, and let Claude evaluate job postings, tailor your CV, write cover letters, and prepare you for interviews.
Note: This is an independent open-source project and is not affiliated with, endorsed by, sponsored by, or maintained by Anthropic. Anthropic and Claude Code are referenced only to describe the toolchain this workflow uses.
This project has no affiliated cryptocurrency, token, or paid sponsorship program. Anything claiming otherwise is unauthorized and should be treated as a scam. The only ways to support the project are the Ko-fi link below and contributing on GitHub.
Does it actually work?
I'm a geophysicist by training. When my position was cut in late 2025, I built this framework to run my own job search - the same /scrape, /apply, and /interview workflow in this repo, used weekly, on my own career. I was upfront about it with every employer I spoke to, and instead of counting against me, it usually sparked a genuine technical conversation.
Sixty-nine tailored applications, twenty first interviews, and one signed contract later, I started as an AI engineer in June 2026. People kept asking whether this actually works. It got me hired. Now it's yours.
The longer version, including the full application funnel, is on LinkedIn.
What this is
A structured workflow that turns Claude Code into a full-stack job application assistant. The core workflow (self-profiling, fit evaluation, and the drafter-reviewer application pipeline) is language- and country-agnostic. The job portal search skills are built for the Danish market (Jobindex, Jobnet, Akademikernes Jobbank, etc.), but the pattern is designed to be swapped for your local job boards.
/setup /scrape /apply
| | |
v v v
Fill in Search job Evaluate fit
your profile portals Score & recommend
| | |
v v v
Profile Present matches Draft CV + Cover Letter
files ready with fit ratings (LaTeX, tailored)
| |
v v
Pick a match Reviewer agent critiques
-> /apply -> Revise -> Final outputThe framework encodes career guidance best practices, including structured evaluation criteria, forward-looking cover letter framing, and optional salary benchmarking.
Prerequisites
- Claude Code (CLI). Using a different agent tool (Codex, Antigravity, Gemini CLI)? Start at
AGENTS.md- the portal search skills work there out of the box, and community forks adapt the full workflow. - Python 3.10+
- Bun (for job search CLI tools)
- LaTeX distribution with
lualatexandxelatex: TeX Live, MacTeX, TinyTeX, or MiKTeX. The CV compiles withlualatex(pdflatex often fails on modern MiKTeX installs withfontawesome5font-expansion errors); the cover letter compiles withxelatexbecausecover.clsrequiresfontspec. If using a minimal TeX install such as TinyTeX or BasicTeX, install the extra packages listed in SETUP.md. - Optional:
pdftotextfrom poppler (macOS:brew install poppler, Debian/Ubuntu:apt install poppler-utils, Windows:choco install poppler) β used by/apply's ATS parseability check on the compiled CV. If missing, the check degrades gracefully to a visual keyword review.
Quick start
π₯ Prefer to see it in action first? The Next New Thing did a hands-on walkthrough of how the workflow is actually used, from setup to a finished application (recorded August 2026 - commands may have evolved since).
1. Fork and clone
gh repo fork MadsLorentzen/ai-job-search --clone
cd ai-job-search[!IMPORTANT]
A fork of this repo is always public β GitHub does not allow private forks of
public repositories β and/setup(step 3 below) writes your personal data (name,
contact details, employment history, salary expectations) into tracked files.
If this copy is for your own job search rather than for contributing changes back,
use a private repository with this repo asupstreaminstead β the two-minute
recipe is in SETUP.md section 8,
and every update workflow works identically. Fork only to contribute.
2. Install job search tools
PowerShell:
$tools = @("jobbank-search", "jobdanmark-search", "jobindex-search", "jobnet-search", "linkedin-search", "freehire-search")
foreach ($tool in $tools) {
Push-Location ".agents/skills/$tool/cli"
bun install
Pop-Location
}Bash / zsh / Git Bash:
for tool in jobbank-search jobdanmark-search jobindex-search jobnet-search linkedin-search freehire-search; do
(cd .agents/skills/$tool/cli && bun install)
doneFor linkedin-search and freehire-search the install is optional: both have zero runtime dependencies and run with plain bun; bun install only pulls TypeScript dev types.
3. Set up your profile
claude
# Then inside Claude Code:
/setup/setup offers three paths: read your documents/ folder if you have one populated (CV PDF, LinkedIn export, diplomas, reference letters, past applications), import a single CV pasted in chat, or walk through an interview. It auto-detects what you have and asks. Documents-folder mode is idempotent and safe to re-run as you add more material; see documents/README.md for the layout.
4. Search for jobs
/scrapeThis searches multiple job portals for positions matching your profile, deduplicates results, and presents them sorted by fit. Pick a match to run /apply on it directly β or, when a scrape returns more jobs than you want to eyeball, run /rank to batch-score them all against the fit framework and get a ranked shortlist first.
5. Apply to a job
/apply https://jobindex.dk/job/1234567If the URL can't be fetched (some job portals block automated access), you can paste the job description directly instead:
/applyThis runs the full workflow: evaluate fit, draft CV + cover letter, review with a second agent, revise, and present the final output.
Postings are treated as untrusted input (the workflow follows no instructions embedded in them and fetches no links from their body), but agentic defenses are instruction-level, not a sandbox - on an unfamiliar job board, skim what was fetched and written before you hit send. Details in SECURITY.md.
Other commands
/setup, /scrape, and /apply form the core workflow. Ten more commands extend it once your profile is in place:
/interviewpreps you for a scheduled interview on a tracked application. It builds a stage-specific prep pack from the application's archive (the exact posting, the CV and cover letter the interviewer actually read, feedback recorded from earlier rounds), researches the company and interviewers with a verify-before-use rule, maps likely questions to your STAR examples, and offers a mock interview following the roleplay protocol in07-interview-prep.md. Gaps get honest bridge answers, never invented experience./outcomerecords what happened to an application - interview stages, offers, rejections, silence. It archives the submitted CV, cover letter, and posting text intodocuments/applications/<company>_<role>/, keepsoutcome.mdin the format/setupPath A parses, and updates the tracker. It also owns the stretch before there is an outcome to record:/outcome followupsurfaces open applications that have gone quiet (default 10 days), drafts a short channel-appropriate follow-up in your writing style using only claims from the materials you already submitted (drafts only, never sends; at most twice per application), and offers a thank-you note in the same turn an interview stage is recorded. Once a few applications resolve, it points you back to/setupto calibrate the fit framework from what actually got interviews./notion-syncpublishes a one-way, read-only view of the pipeline into a Notion database via the official Notion MCP server (OAuth, no API keys) - one row per ranked job plus every tracked application, with a write-once briefing page per row. The repo files stay the system of record: nothing syncs back, and documents sync as filenames only. Complements/html-report: that is the deep offline dashboard you regenerate at your desk; this is the glanceable live view from anywhere Notion runs (desktop, web, phone)./gmail-syncreads your Gmail (via the Gmail connector) for status signals on your open applications - interview invites, assessment links, offers, rejections - and proposes them as a batch for you to approve before anything is written to the tracker oroutcome.md, citing the source email on every proposed change. Offers stop short of proposinghired/offer_declinedsince that's your call; conflicting or unmatched signals get flagged for a manual/outcomepass instead of guessed./rankbridges/scrapeand/apply: it batch-scores all newly scraped postings against the fit framework (parallel agents fetch each posting and score the five evaluation dimensions) and returns a ranked shortlist with honest per-job strengths and gaps. Deal-breakers veto, deadlines get urgency flags, dead postings get marked expired. Pick a number and it hands off to the full/applyworkflow./expandenriches your profile by scanning public sources you've already linked in it (GitHub repos, portfolio site, Kaggle, Google Scholar) and looking up syllabi for named courses and certifications. Discovered competencies are added to your profile with a source tag. Useful right after/setupto surface skills that documents alone don't make explicit./upskillanalyzes the gap between your profile, your tracked job postings, and your ranked-but-untracked postings (/rank's recorded gaps inseen_jobs.json) β or a single posting via/upskill. Produces a prioritized heatmap of skill gaps and a learning plan with web-searched study resources and time estimates. Useful for career planning between applications./html-reportgenerates a self-contained HTML dashboard fromjob_search_tracker.csvand the application archives β stat cards, status/sector/channel/funnel charts (inline SVG, no external dependencies), and a filterable applications table. Opens directly in a browser, fully offline. Re-run it any time after/applyor/outcomeadds new entries./add-templateregisters your own CV or cover letter template (LaTeX, Typst, or another toolchain) in place of the stock ones. It captures the template's instructions (source extension, compile command, fonts, style rules, page limit), runs a mandatory test compile, and wires the template into/apply. See Custom templates below./add-portalgenerates a job-portal search skill for a job board in your market. It investigates the portal (search URL pattern, result structure, access rules), scaffolds the CLI skill from the same structure as the shipped ones, and test-runs a live query before registering. See Job search tools below.
/reset is also available, see Starting over below.
File structure
/* Detailed source-code truncated for AI context efficiency. */How `/apply` works
The /apply command runs a drafter-reviewer workflow with mandatory PDF compilation:
- Parse the job posting (URL or text)
- Evaluate fit against your profile (skills, experience, culture, location, career alignment)
- Draft a tailored CV and cover letter in LaTeX
- Spawn a reviewer agent that researches the company and critiques the drafts
- Revise based on the reviewer's feedback
- Compile and inspect both PDFs: lualatex for the CV, xelatex for the cover letter. Claude reads the rendered pages and iterates on the LaTeX until the CV is exactly 2 pages with no orphaned entry titles, and the cover letter is exactly 1 page with the signature visible and fonts consistent.
- ATS-check the CV: extract the PDF's text layer (
pdftotext, optional dependency) and verify it the way an ATS parser sees it β contact details present as literal text, no garbled glyphs, sane reading order β then score the posting's keyword coverage against the extraction. Keywords the profile genuinely supports get added; genuine gaps stay visible, never stuffed. - Present the final output with a verification checklist
All claims in the CV and cover letter are verified against your actual profile. The system never fabricates skills or experience.
What makes this workflow different
- PDF verification loop. Most LaTeX-resume templates produce "looks fine in the .tex" output that breaks in the PDF: job titles orphan to the next page, cover letters spill onto page 2, bullet fonts silently fall back to the body font. The
/applycommand compiles and visually inspects every PDF and applies targeted fixes (\needspace,\enlargethispage, font-matching wrappers for list items) until the layout is clean. This runs automatically on every application. - ATS verification on the PDF text layer. An ATS reads the PDF's embedded text, not the rendered page β and LaTeX can silently produce PDFs whose text extracts as garbage (icon glyphs where the email should be, interleaved lines from multi-column layouts).
/applyextracts the compiled CV's text layer withpdftotextand verifies contact details, reading order, and the posting's keyword coverage against what a parser actually sees. Honesty rule enforced: a keyword the profile doesn't support is acknowledged as a gap, never stuffed in. - Relevance-weighted CV cutting. When a CV overflows 2 pages, the workflow does not cut mechanically from the "oldest" section. It scores each candidate line by (a) relevance to the target posting, (b) uniqueness in the document, and (c) whether the cover letter depends on it, and cuts the lowest-total-score line first. An older-role bullet that hits posting keywords survives ahead of a recent-role bullet that does not.
- Drafter-reviewer separation. The drafter writes; a second Claude agent, spawned with a fresh context, researches the company and critiques the drafts. The drafter then revises. This catches missed keywords, weak framing, and generic language that a single pass often leaves in.
- Token-efficient reviewer dispatch. The reviewer agent receives drafts inline rather than re-reading them, and the verification checklist runs once at the end of the workflow rather than being duplicated by both agents. Note: the new compile-and-inspect step in Step 5 spends some of those savings on PDF rendering and layout iteration β the workflow trades some end-to-end token cost for a real reduction in broken PDFs reaching the user.
Customization
Which files to edit manually
If you prefer editing files directly instead of using /setup:
| File | What to change |
|---|---|
CLAUDE.md |
Your full profile (name, education, experience, skills, goals) |
01-candidate-profile.md |
Structured version of your CV data |
02-behavioral-profile.md |
Your behavioral assessment or self-assessment |
04-job-evaluation.md |
Skill match areas, career goals, motivation filters |
05-cv-templates.md |
Profile statement templates for different role types |
07-interview-prep.md |
Your STAR examples from actual experience |
search-queries.md |
Job search queries for your skills and location |
Updating your search queries
As your priorities evolve, you can reconfigure just the job search without re-running the full profile setup:
/setup --section searchThis re-runs the search configuration interview: which roles to target, which skills to search for, which locations, and which portals. It also suggests role types you may not have considered based on your profile.
Custom templates
The CV uses moderncv (banking style). The cover letter uses a custom cover.cls with Lato/Raleway fonts. Both are LaTeX β the reference engine this repo ships and maintains.
To use your own template instead β LaTeX, Typst, or any other toolchain that compiles to PDF from the command line β run:
/add-templatePoint it at your source file (a .tex file plus any .cls/.sty files or bundled fonts; a .typ file plus any local packages; or an equivalent for another toolchain). The command interviews you for the template's instructions β source extension, compile command, fonts and where they live, style rules to preserve, hard page limit β stores everything under templates/, runs a mandatory test compile, and activates the template so /apply drafts and compiles from it. Templates are stored with [PLACEHOLDER] tokens instead of personal data, so they're safe to commit and share.
/add-template --listshows registered templates/add-template --use <name>switches between them/add-template --use defaultreverts to the stock moderncv / cover.cls templates
If you prefer doing it by hand, the manual route still works: update the guidance in 05-cv-templates.md and 06-cover-letter-templates.md.
Job search tools
The four Danish CLI tools in .agents/skills/ (Jobbank, Jobdanmark, Jobindex, Jobnet) demonstrate the pattern for building a job-portal integration for a specific market. If you're in a different country, run:
/add-portalGive it your local job board's URL. The command investigates the portal (search-URL pattern, result-page structure, robots.txt/access rules), scaffolds a CLI skill with the same structure, commands, and output contract as the shipped ones, and test-runs a live query before registering anything. Auth-walled portals are declined, and portals with restrictive terms get a prominent personal-use-only warning in the generated skill. The generated skill is market-specific and lives in your fork; the generator itself is the universal part.
Maintaining a fork adapted to your market or language? Add it to the Community forks & adaptations thread so others can find it.
For country-agnostic starting points outside Denmark, the repo ships two portal skills alongside the Danish demos:
linkedin-searchβ built on LinkedIn's public, unauthenticatedjobs-guestendpoints. Field-agnostic, zero runtime dependencies (runs with justbun), and takes the search location as an explicit flag, so it works for any market out of the box (-l "Berlin, Germany",-l "Mumbai, Maharashtra, India",-l "Remote", β¦). Intended for personal use only β automated access is against LinkedIn's Terms of Service, so keep volume low. See.agents/skills/linkedin-search/SKILL.md.freehire-searchβ queries the freehire.me aggregator's public REST API (JSON, no API key). Tech-focused (software, data, engineering, DevOps, remote), multi-market via facet flags (--region,--country,--remote), and zero runtime dependencies. Unlike the HTML-scraping Danish portals, results come back structured (skills, seniority, category). The backend is MIT-licensed and self-hostable β pointFREEHIRE_API_URLat your own instance if you prefer. See.agents/skills/freehire-search/SKILL.md.
Extending the framework: portals, templates, criteria - and borrowing from other forks
Everything above adds up to an extension model, so here it is stated plainly. The framework has three extension points, and none of them require touching upstream:
- Portal skills - the module system for job boards. Every
*-searchskill is a self-contained folder under.agents/skills/with the same contract (asearch/detailCLI,--format json|table|plainoutput, anenabled:flag in itsSKILL.md, its own tests)./scrapeauto-discovers any installed skill that follows the contract - nothing to register, nothing to wire up./add-portalgenerates new ones; the community portal index catalogs the ones other forks have built. - Document templates -
/add-templateregisters any CV or cover-letter toolchain that compiles to PDF from the command line, LaTeX or otherwise. - Evaluation criteria - deal-breakers and preferences in your profile are free-form, and the evaluation rubric scores against whatever you put there. "Strong parental-leave terms", "minimum salary X per my union's scale", "no on-call" - each is one profile line, no code, and it carries real weight in
/rankand/applyfit evaluations. Language is the one deal-breaker type with dedicated, structured handling:/setupcaptures every language you work in and your level (asked directly, or inferred from your CV/LinkedIn export) into aLanguagestable, and the Language Gate (04-job-evaluation.md) hard-rejects a posting that requires a language you haven't declared at all, while flagging - not auto-rejecting - one that asks for a higher level than you declared in a language you do work in, so a borderline case (a strict "fluent" bar against your own B1/B2, say) gets your judgment instead of a silent drop.
Borrowing a portal skill from another fork is the intended way to get a board that upstream doesn't ship: find it in the portal index, open that fork, and copy the one folder into your own .agents/skills/. Before you run it:
- Read the code. All of it - these CLIs run pre-approved on your machine (
.claude/settings.jsonallowlists them) against your career data. Check that the only network calls go to the job board it claims to search, thatpackage.jsonhas nodependenciesand no lifecycle scripts (postinstalletc.), and that nothing reads or writes outside its own folder. - Run its tests offline (
bun testin the skill'scli/directory) - a well-built skill's tests pass with no network access. - Check the
enabled:flag and the skill's own ToS notes.
The copy step is manual on purpose. Your settings already allow installed portal skills to run without asking each time - so an installer that fetched them from third-party repos for you would skip the one check that matters: you, reading the code first. There isn't one, and that's a security decision rather than a missing feature.
Market-specific data sources (a national salary database, local award-rate tables) follow the same pattern as portals: they belong in a market fork, shared via #78, not upstream.
Salary benchmarking
The salary tool works with any salary data you provide (union statistics, Glassdoor exports, personal research, etc.). See tools/README_SALARY_TOOL.md for the expected format and setup. If you don't have salary data, the salary step is simply skipped.
Starting over
To wipe your profile data and start fresh:
/reset profile # clears skill files, preserves framework rules
/reset documents # deletes files from documents/ folder
/reset all # both/reset shows exactly what will be deleted and requires you to type RESET to confirm. Nothing is deleted until you do.
Staying up to date
Upstream moves fast. Rather than pulling raw master and hoping, update your fork to a tagged release - a vetted checkpoint described in CHANGELOG.md. python3 tools/check_upstream_updates.py previews exactly which of your personalized files an update touches before you merge, and python3 tools/upstream_triage.py sorts the commits you're behind into "worth reviewing" vs "probably skip" (a weekly workflow can post this to a rolling issue). Full walkthrough in SETUP.md, section 8.
Tips for better results
Profile depth matters
The single biggest factor in output quality is how much detail you put into your profile. A thin profile produces generic applications; a detailed one enables genuinely tailored results.
- Role descriptions: Don't just list job titles. Describe what you actually did in each position: specific projects, tools used, responsibilities, and measurable achievements. The more material you provide, the more precisely the system can reframe your experience for different roles.
- Skills in context: Instead of listing "Python" or "project management," describe how and where you applied them. "Built ML pipelines for customer churn prediction in Python using scikit-learn" gives the system far more to work with than "Python, machine learning."
- All onboarding paths work: Whether you point
/setupat yourdocuments/folder, paste a single CV, or walk through the interview, the principle is the same: richer input produces sharper output.
Career path discovery
The framework supports two distinct modes of job searching:
- Explicit targeting: You know which roles or sectors you want. The system helps refine and prioritize based on fit.
- Latent opportunity discovery: By analyzing your full history (not just job titles, but the actual work you did), the system can surface career paths you haven't considered. Transferable skills that map to unexpected industries, patterns in what you enjoyed or excelled at, or emerging roles that combine your domain expertise with new technology.
To get the most from this, invest time during /setup in describing not just your experience, but what energized you, what drained you, and what you'd want more of. This context directly shapes how the system evaluates fit and which roles it surfaces during /scrape.
Contributing
Thinking about a PR? Read CONTRIBUTING.md first - it explains what gets merged, what lives in forks, and why.
Acknowledgements
- Mikkel Krogholm (skills repo) for the job search CLI skills
- Built with Claude Code by Anthropic
License
MIT
File: documents/README.md
Documents Folder
This folder holds your actual career documents. The /setup command reads everything here and uses it to populate the candidate skill files under .claude/skills/job-application-assistant/. It is safe to re-run /setup as you add new documents β it merges intelligently and will never overwrite existing content without asking you first.
Folder Structure
documents/
βββ cv/ # Your CV files (PDF or LaTeX)
βββ linkedin/ # LinkedIn profile export (PDF)
βββ diplomas/ # Degree certificates and transcripts
βββ references/ # Reference letters
βββ postings/ # Raw job posting text, pasted manually for pages Claude can't fetch
β βββ <Company> - <Job Title>.txt # Filename = company + job title, content = full posting text
βββ applications/ # Past job applications
β βββ <company>_<role>/
β βββ job_posting.md # The original job posting (written by /apply, or pasted)
β βββ cover_letter.tex # The cover letter you submitted
β βββ cv_draft.tex # The CV variant you submitted
β βββ outcome.md # Result + notes (fill in after hearing back)
βββ README.md # This filecv/
Your master CV β the most complete, unedited version of your professional record.
Supported formats: .pdf, .tex
What /setup extracts:
- Work experience (titles, companies, dates, bullet points)
- Education (degrees, institutions, dates, thesis topics)
- Technical skills
- Awards and publications
- Contact information
Naming: Any filename works. If multiple files are present, /setup reads all of them and cross-references for consistency.
Tip: Keep your most comprehensive CV here (not a tailored variant). The skill files are the canonical source β tailored CVs are generated per application by /apply.
linkedin/
Your LinkedIn profile exported as a PDF.
How to export: On LinkedIn, go to your profile β More β Save to PDF. This exports a structured summary of your profile.
Supported formats: .pdf
What /setup extracts:
- Work experience and dates (cross-referenced against your CV)
- Skills and endorsements
- Education
- Certifications and licenses
- Volunteer work
- Publications
- About/summary section (used to infer behavioral profile additions)
- Recommendations received (may enrich reference context)
Naming: Any filename works. Only one LinkedIn export is expected; if multiple are present, /setup uses the most recently modified one.
diplomas/
Degree certificates, transcripts, and any official qualifications.
Supported formats: .pdf
What /setup extracts:
- Degree titles and official names (used to verify education entries)
- Graduation dates
- Grades or distinctions (if visible)
- Institution names (official spelling)
Naming: Use descriptive names, e.g. msc_physics_ucph_2025.pdf, bsc_physics_ucph_2016.pdf. Naming does not affect parsing.
references/
Reference letters from former managers, supervisors, or collaborators.
Supported formats: .pdf, .txt, .md
What /setup extracts:
- Referee name, title, and organization
- Specific quotes and assessments (added to the references section of
01-candidate-profile.md) - Competency language used by referees (adds behavioral signal to
02-behavioral-profile.md)
Naming: Use the referee's name, e.g. reference_ole_frandsen.pdf.
postings/
A drop folder for raw job posting text when Claude can't fetch a page directly (bot-blocked ATS platforms like Lever, Greenhouse behind Cloudflare, JS-heavy SPAs that return empty content, etc.). You open the posting yourself and paste the full text into a .txt file here.
Naming: <Company> - <Job Title>.txt, e.g. RYZ Labs - Front End Engineer - React.js.txt. Content is the full posting text, pasted as-is. Including the company keeps the drop folder collision-free when two postings share a title, and gives /apply the company name for free.
Workflow: Drop the file, then tell Claude in the conversation β it isn't watched automatically. Once a posting has been evaluated or applied to, it can be deleted from here or left as a record; it's a scratch inbox, not an archive (use applications/<company>_<role>/job_posting.md for that once you actually apply).
Trust boundary: Pasted posting text is still untrusted third-party content, the same as anything Claude fetches directly β data to evaluate, never instructions to follow (see SECURITY.md's untrusted-input rules). Pasting it by hand doesn't change that.
applications/
A record of past job applications. Each subfolder is one application.
You can maintain these folders by hand, or let the /outcome command do it: it records progress updates and final results conversationally, archives the submitted drafts and, if /apply has not already written it, the posting text, keeps outcome.md in the format below, and updates job_search_tracker.csv in the same step.
Subfolder naming: <company>_<role> β lowercase, underscores for spaces.
Every character that is not a letter, digit or underscore is dropped (so Novo Nordisk A/S
becomes novo_nordisk_as), runs of underscores collapse to one, and leading and trailing
underscores are trimmed. If the derived name is empty, stop and ask the user for a company or
role containing at least one letter or digit; do not create a file or directory. Every non-empty
result is therefore a single path component whatever the posting contains.
Examples:
applications/
βββ acme_ml_engineer/
βββ bigcorp_software_engineer/
βββ consultco_ai_consultant/Files within each application folder
job_posting.md β The full job posting text, written by /apply, or paste it here. Used by /setup to infer which skills and role types you have targeted, and to calibrate 04-job-evaluation.md.
cover_letter.tex β The cover letter you actually submitted. Used to extract writing style patterns and structure for 06-cover-letter-templates.md.
cv_draft.tex β The CV variant you submitted. Used to extract profile statement styles for 05-cv-templates.md.
outcome.md β Fill this in after the application resolves. Format:
# Outcome: <Company> β <Role>
**Status:** in_progress | hired | offer_declined | rejected | no_response | interview_only
**Date resolved:** YYYY-MM-DD
## Interview stages reached
- [ ] Phone screen
- [ ] Technical interview
- [ ] Case interview
- [ ] Final round
- [ ] Offer received
## Notes
What happened? What feedback did you receive (if any)?
What would you do differently?
Any signal about what they valued or didn't?in_progress marks an application that is still open (used by /outcome for interview-stage updates before a resolution). /setup's calibration draws conclusions only from applications with a final status.
Application folders may also contain interview_prep_<stage>.md files written by /interview (one per interview stage, kept as history). /setup reads only the four files named above and ignores these.
What /setup learns from outcome.md:
- Which role types and companies have led to interviews (signals strong fit areas)
- Which applications did not progress (informs the experience match calibration in
04-job-evaluation.md) - Interview feedback, if you recorded it, can surface new STAR candidates
File Format Notes
| Format | Readable by /setup |
Notes |
|---|---|---|
.pdf |
Yes | Parsed directly with the Read tool |
.tex |
Yes | LaTeX source β structure and content both readable |
.md |
Yes | Plain text |
.txt |
Yes | Plain text |
.docx |
No | Convert to PDF before placing here |
.png / .jpg |
No | Scanned documents won't be parsed β use text PDFs |
Re-running `/setup`
The command is designed to be re-run as your document collection grows. Each run:
- Reads the current state of all skill files
- Compares extracted document content against what's already there
- Only proposes changes for content that is genuinely new or conflicting
- Never silently overwrites β conflicts are shown explicitly for your decision
When to re-run:
- After adding a new LinkedIn export
- After adding reference letters
- After recording outcomes for completed applications
- After updating your master CV
File: tools/README_SALARY_TOOL.md
Salary Benchmark Tool
What is this?
The salary lookup tool (salary_lookup.py) lets you benchmark company salaries against a baseline from your own data. It's used during the /apply workflow to show how a company's compensation compares to market rates.
This tool is optional. If you don't have salary data, the salary step is simply skipped during /apply.
How it works
The tool reads a salary_data.json file in the repo root containing company salary benchmarks. It uses fuzzy matching to find companies by name, handling Danish/Nordic characters, legal suffixes (A/S, ApS), and common spelling variations.
The data format supports any index-based or absolute salary data. For example:
- Index 100 = median salary, higher is better
- Absolute salary values in your currency
- Any custom metric you want to track
Data format
The tool expects salary_data.json with this structure:
{
"metadata": {
"source": "My Union Statistics 2025",
"index_baseline": 100,
"index_label": "Index",
"baseline_description": "Index 100 = median salary for private sector"
},
"companies": [
{
"company": "Novo Nordisk A/S",
"city": "Bagsværd",
"categories": {
"all_employees": { "count": 500, "index": 108.5 },
"engineering": { "count": 120, "index": 112.3 }
}
},
{
"company": "Γrsted A/S",
"city": "Fredericia",
"categories": {
"all_employees": { "count": 200, "index": 105.2 }
}
}
]
}Fields
- metadata.source: Where the data comes from (for reference)
- metadata.index_baseline: The baseline value (e.g., 100 for index-based data)
- metadata.index_label: Label for the index column in output
- metadata.baseline_description: Human-readable explanation of the baseline
- companies[].company: Company name (required)
- companies[].city: City/location (optional, used for filtering)
- companies[].categories: Named salary categories, each with
countand/orindex
Setup options
Option A: Create salary_data.json manually
Create the file by hand with data from any source: union statistics, Glassdoor, salary surveys, networking, or personal research.
Option B: Convert from Excel
If you have salary data in an Excel file:
pip install openpyxl
python3 tools/convert_salary_excel.py path/to/salary-data.xlsx \
--source "My Salary Data 2025" \
--baseline 100 \
--baseline-desc "Index 100 = median salary"On Windows, use py if that is how Python is exposed on your PATH. If your system uses python instead of python3, substitute that in the examples.
The converter auto-detects the Excel layout:
- Looks for a "Company"/"Firma" column and an optional "City"/"By" column
- Treats remaining columns as salary data (auto-pairs count/index columns)
Option C: Build from research
Start with an empty template and add companies as you research them:
{
"metadata": {
"source": "Personal research",
"index_baseline": 0,
"index_label": "Monthly salary (DKK)",
"baseline_description": "Approximate monthly salary before tax"
},
"companies": [
{
"company": "Example Corp",
"city": "Copenhagen",
"categories": {
"entry_level": { "index": 42000 },
"senior": { "index": 55000 }
}
}
]
}Usage
python3 salary_lookup.py "Novo Nordisk"
python3 salary_lookup.py "Γrsted" --city "Fredericia"
python3 salary_lookup.py "COWI" --json
python3 salary_lookup.py --list-all
python3 salary_lookup.py --validate # pre-flight check your salary_data.jsonImportant notes
- The data file (
salary_data.json) is excluded from git (see.gitignore). Your salary data may be proprietary or confidential. - If the data file is missing,
salary_lookup.pyexits with a helpful error message and the/applyworkflow skips the salary benchmark step. - The fuzzy matcher handles Danish company name variations: legal suffixes, Nordic characters, anglicized spellings, and partial matches.
--validatechecks your data file for malformed category values and duplicate company names and prints a report, without performing a lookup.