- 批量筛岗:一次抓取多个招聘门户并去重打分,比人工刷十几个站点再逐条判断匹配度省时间,代价是抓取受门户反爬限制
- 简历定制:针对单个 JD 重写简历和求职信,并让第二个 agent 复核,适合投递量不大但要求每份都贴合 JD 的场景;海投场景收益不明显
- 面试准备:拿到面试后按阶段生成准备包和模拟问答,比通用题库更贴岗位描述,但质量取决于你 /setup 时喂进去的履历详细程度
- 个人知识库式求职:所有档案、评估记录、生成文档都落在本地仓库里,适合想长期维护一份可版本化的求职材料、而不是散落在各招聘网站账号里的人
ai-job-search
ai-job-search-devMIT活跃维护 有专家点评基于 Claude Code 的求职自动化框架,在本地评估职位、定制简历、撰写求职信并准备面试,数据自己掌控。
The job search that runs on your machine. AI job application framework built on Claude Code: evaluate postings, tailor CVs, write cover letters, prep interviews. Fork it and own it.
专家点评
上手难度:进阶更新于 2026-11-11把求职流程做成一套可 fork 的 Claude Code 技能集,替代手工改简历、逐条投递的重复劳动
适用场景
踩坑
- 隐私泄露风险是结构性的:README 明确说公开仓库的 fork 永远是公开的,而 /setup 会把姓名、联系方式、薪资期望写进被跟踪文件。直接按最小安装命令 fork 再跑 /setup,个人信息就进了公开仓库;必须改成私有仓库并把原仓库设为 upstream
- LaTeX 工具链是硬门槛:简历走 lualatex、求职信走 xelatex(cover.cls 依赖 fontspec),用 pdflatex 会在 fontawesome5 上报错。机器上没装完整 TeX 发行版时,第一次生成 PDF 就会卡住
- 抓取环节不可靠:README 自己承认部分招聘门户会阻止自动抓取,issue 里也有针对具体 portal 搜索实现(URL 解析、host 校验)的报错。依赖 /scrape 全自动跑通不现实,得接受手动粘贴 JD 给 /apply 的降级路径
同类对比
如果只是想要一个通用的 agent 编排框架而不是求职这一套固定流程,直接用 Claude Code 本身或其它 agent 框架自己搭;如果核心诉求是简历排版而非 AI 评估,LaTeX 模板类项目更轻。
gh repo fork ai-job-search-dev/ai-job-search --clone
cd ai-job-search
python3 tools/install_portal_clis.pyREADME 未说明硬件要求;需要 Claude Code CLI(无免费额度,需 Claude Pro/Max/Team 订阅或 Anthropic API 额度)、Python 3.10+、Bun 和带 lualatex/xelatex 的 LaTeX 发行版
项目说明
由 README 自动翻译提炼基于 Claude Code 的求职申请框架,帮你评估岗位、定制简历、写求职信并准备面试
基于 Claude Code 的求职自动化框架,在本地评估职位、定制简历、撰写求职信并准备面试,数据自己掌控。
核心能力
- 用 /setup 从文档、粘贴简历或访谈三种方式建立个人档案
- 用 /scrape 跨多个招聘门户搜索岗位并去重、按匹配度排序
- 用 /apply 评估匹配度后起草简历与求职信,再由第二个 agent 审阅修订
- 用 /rank 对抓取结果批量打分生成排序候选清单
- 用 /interview 生成分阶段面试准备包并模拟面试
容易踩的坑
公开仓库的 fork 永远是公开的,/setup 会把姓名、联系方式、薪资期望等个人信息写入被跟踪的文件,个人使用应改用私有仓库并以本仓库为 upstream;简历用 lualatex 编译、求职信需 xelatex(cover.cls 依赖 fontspec),pdflatex 常因 fontawesome5 报错;部分招聘门户会阻止自动抓取,此时需直接粘贴职位描述给 /apply
- v1.7.5最新正式版2026-11-09 · 2026-11-09 06:04 UTC
What's Changed
- fix(job-key): fold Latin letters without an NFKD decomposition and add --audit --migrate by @ai-tvang in https://github.com/ai-job-search-dev/ai-job-search/pull/637
- fix(notion-sync): re-key a row matched by URL when its Key has drifted by @ai-tvang in https://github.com/ai-job-search-dev/ai-job-search/pull/761
- fix(install): find bun at standard install locations when PATH misses it (fixes #707) by @ahmed-husssain in https://github.com/ai-job-search-dev/ai-job-search/pull/762
- fix(fetch-page): pass --compressed only when curl supports it (fixes #705) by @ahmed-husssain in https://github.com/ai-job-search-dev/ai-job-search/pull/759
- fix(scrape): force UTF-8 output in scrape_state.py by @harshajv7981 in https://github.com/ai-job-search-dev/ai-job-search/pull/744
- fix(reset): clear the writing-style patterns /setup infers from past cover letters by @divya0795 in https://github.com/ai-job-search-dev/ai-job-search/pull/825
New Contributors
- @ai-tvang made their first contribution in https://github.com/ai-job-search-dev/ai-job-search/pull/637
- @harshajv7981 made their first contribution in https://github.com/ai-job-search-dev/ai-job-search/pull/744
- @divya0795 made their first contribution in https://github.com/ai-job-search-dev/ai-job-search/pull/825
Full Changelog: https://github.com/ai-job-search-dev/ai-job-search/compare/v1.7.4...v1.7.5
- v1.7.42026-11-02 · 2026-11-02 06:04 UTC
查看更新说明
What's Changed
- fix(freehire-search): reject detail URLs on other hosts instead of asking the API for their last segment (#622) by @ayobamiseun in https://github.com/ai-job-search-dev/ai-job-search/pull/623
- fix(freehire-search): reject --description-format on detail instead of ignoring it by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/648
- freehire-search: honor Retry-After on 429/503 by @strelov1 in https://github.com/ai-job-search-dev/ai-job-search/pull/632
- freehire-search: us is a country, not a region by @strelov1 in https://github.com/ai-job-search-dev/ai-job-search/pull/630
- fix(freehire-search): say what --region is, and warn when it matches a thin bucket by @GOPNIK64 in https://github.com/ai-job-search-dev/ai-job-search/pull/651
- freehire-search: document the OR and title: operators in -q by @strelov1 in https://github.com/ai-job-search-dev/ai-job-search/pull/674
- fix(job-key): report a non-string url in --audit instead of a traceback by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/634
- fix(rank): report malformed scoring results in apply instead of a traceback by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/635
- fix(rank): report a tracker that is not UTF-8 instead of a traceback by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/639
- fix(rank): keep every --exclude key out of the sweep when the list has spaces by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/641
- fix(salary): collapse line breaks in wrapped workbook headers by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/643
- fix(salary): skip prose notes tabs instead of listing sentences as companies by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/644
- fix(salary): reject non-finite converter baselines by @hopkienne in https://github.com/ai-job-search-dev/ai-job-search/pull/665
- fix(linkedin-search): reject an unknown --remote mode instead of searching every workplace type by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/645
- fix(linkedin-search): require a value for --query instead of dropping the keywords by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/646
- fix(jobindex-search): reject a --sort other than score or date instead of sending it to the portal by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/647
- fix(jobnet-search): filter search by the ESCO occupation that occupations prints by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/649
- fix(jobdanmark-search): filter search by the administrative regions locations returns by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/650
- fix(upstream-triage): skip new upstream files inside a directory the fork removed by @rudycelekli in https://github.com/ai-job-search-dev/ai-job-search/pull/656
- test(reset): let a personalized profile carry its own ## sections by @ellyess in https://github.com/ai-job-search-dev/ai-job-search/pull/676
- fix(verify-layout): walk PATH candidates for pdftotext supporting -bbox (#655) by @Sonal-sp in https://github.com/ai-job-search-dev/ai-job-search/pull/668
- fix(notion-sync): keep expired-only jobs update-only by @Br-Charb in https://github.com/ai-job-search-dev/ai-job-search/pull/666
- docs(rank): clarify posting search during retrieval by @abdulwasea89 in https://github.com/ai-job-search-dev/ai-job-search/pull/638
- fix(ci): drop persisted checkout credentials and pass matrix.tool through env by @MaverickHQ in https://github.com/ai-job-search-dev/ai-job-search/pull/658
- ci: run the Python suite on Windows by @MadsLorentzen in https://github.com/ai-job-search-dev/ai-job-search/pull/683
- fix(freehire-search): reject an unknown --remote mode by @mvanhorn in https://github.com/ai-job-search-dev/ai-job-search/pull/661
- fix(upstream): avoid all-clear after a
- v1.7.3v1.7.3 - Least privilege, postings checked before use, and fifty-eight fixes2026-10-19 · 2026-10-19 16:42 UTC
查看更新说明
A patch release in two halves. The security model now does what the docs said it did: skills and agents get only the tools they need, the permission file ships rules that hold even in bypass mode, and every job posting is checked for hidden instructions before anything reads it. And contributors closed fifty-eight bugs in two weeks, most of them found by running the real tools against real data. Seventy-one entries.
Highlights
Least-privilege skills, agents and permissions (#642, #654)
A public review by an AI agent read the shipped configuration and concluded that local use was unsafe as configured. It had a point. The job-application skill's frontmatter granted bare
Bash,EditandWrite. The scraper pre-approvedbun runon any CLI path. And the/applyreviewer and the/rankscorers ran as general-purpose agents with every tool while they held untrusted posting text. Now:- Skill frontmatter grants nothing beyond what
settings.jsonalready allows. The security guard checks every skill and command frontmatter against an exact allowlist, and a portal skill may grant only its own CLI. settings.jsonshipsdenyrules that hold in every permission mode. They block uploads viacurl/wget,scp,ssh,nc, and reads of.envand credential folders.askrules cover edits to the permission files themselves andgit push.- The agents that read postings can read, fetch and search, but not edit or write. These are
posting-fetcher,posting-scorerandapplication-reviewer. The guard now requires every agent definition to declare its tools. - The 403 retry is
tools/fetch_page.py, not a documentedcurl. It makes one GET, checks robots.txt first, and has no upload path.
SECURITY.mdnow lists every shipped grant, its lifetime and its guard in one table, and says plainly what the permission layer does not do.Every posting passes an integrity gate (#654)
"Postings are data, never instructions" was only as strong as the model's adherence.
tools/posting_guard.pynow removes hidden HTML and comments, strips zero-width and bidi characters, quotes instruction-shaped lines, and flags mixed-script words and URLs that exist only in hidden content./apply,/rankand/interviewshow its one-line verdict. A clean posting costs one line and no prompt; on a flagged one/applyasks before continuing and/rankmarks the row. It advises and never blocks.Salary data converts and looks up correctly again (#615, #570, #571, #572, #576, #584, #590, #592, #594)
The real IDA workbook had stopped converting after a header rename (
Firmanavn og by). It converts again. A run of fixes also makes conversion robust to:- chart-only tabs,
- Unicode whitespace grouping,
- non-finite text cells,
- whitespace-only company names.
Lookups now match
NestletoNestlé, accept a BOM-prefixed file, and tell a missing index from a suppressed one.Portal CLIs reject bad input instead of searching anyway (#555, #561, #568, #573, #577, #538, #540)
Missing option values, empty facets and invalid
--formatflags now exit withBAD_ARGinstead of running an unfiltered search. Three portal-specific fixes:jobnet-searchsends external ads to the employer's own URL.jobdanmark-search detailrenders its description as text.jobbank-searchkeeps repeated filter values.
New:
python3 tools/install_portal_clis.pyinstalls every portal CLI with one cross-platform command./rankand/scrapekeep their state honest (#534, #535, #501, #608, #617)/ranknow:- retries a posting it could not fetch instead of expiring it,
- retires scored jobs whose deadline has passed, even when nothing new arrived,
- tries an installed portal CLI's
detailbeforeWebFetchfor portal URLs.
job_key.pyno longer gives two postings one key when a title or company is partly non-Latin.Fork maintainers: what to reconcile
Pull this tag rather than raw
master, then check four th - Skill frontmatter grants nothing beyond what
- v1.7.2v1.7.2 - Measured PDFs, CLIs that stop trusting the digits, tools that survive non-ASCII2026-09-29 · 2026-09-29 18:32 UTC
查看更新说明
A patch release with one theme: the checks the workflow documented now actually run, and the tools stop breaking on anything that is not ASCII. Compiled PDFs get measured instead of eyeballed, the portal CLIs stop trusting the digits in a URL, and every Python tool survives a Cyrillic posting or a Turkish robots.txt on Windows. Thirty-two entries, twenty-four of them fixes, most reported and fixed by contributors within days of each other.
Highlights
/applymeasures the PDF instead of looking at it (#378, #476, #458, #498, #465)tools/verify_layout.pymeasures what Step 5b previously only eyeballed: per-page text extent, bottom whitespace, the largest internal gap, footer collisions, and an entry header stranded at a page break. It exists for a failure every other check missed: a moderncv\cventryis unbreakable, so an entry that does not fit jumps to the next page and leaves a hole while the document still compiles, still has the right page count, and still passesverify_pdf.py. Step 5b now also runs the page-count check it had been claiming Step 5d ran (nothing in the workflow passed--pagesat all).verify_pdf.py --containssees through LaTeX's typographic substitutions, so--contains "Master's degree"and--contains "2016-2024"no longer report a keyword missing from a document that plainly contains it, and the new--ascii-datesflag reads the raw text layer for the one thing the fold must not hide: a year joined to an en-dash, which a real Workday import dropped together with the date. Poppler stays optional; a missing or xpdfpdftotextdegrades to askipped:exit 2 that now names both causes.Portal CLIs stop trusting the digits (#448, #500, #435, #460, #504, #505)
jobindex-search detailfetched any URL verbatim and, when the path did not match, returned a well-formed fake posting built from whatever page came back.linkedin-search detailtook the first six-plus-digit segment from any URL, so a Greenhouse or Lever apply link fetched whichever LinkedIn posting carried that number, with exit 0. Both now parse the host for real and exit 1 with theBAD_IDcontract before any request, matching the rest of the CLIs.jobnet-search detailno longer reports an externally hosted ad as not found,jobdanmark-search detailbacks off on 429/5xx like every other portal, andfreehire-searchdecodes HTML entities in titles and keeps the salary period, so a yearly figure no longer reads as monthly.Non-ASCII stops crashing the tools, especially on Windows (#490, #486, #485, #487, #506, #480, #503)
A piped stdout on Windows defaults to the ANSI code page, and that is how Claude Code runs every tool. A single Cyrillic, CJK, Devanagari, Polish or Turkish posting ended
/rankwithUnicodeEncodeErrorbefore any output reached the workflow; every Python tool now writes UTF-8 at entry, and the Linux CI reproduces the Windows failure throughPYTHONIOENCODING./rank's tracker matching keeps Unicode company and role names instead of collapsing them to nothing, handles a BOM on a reordered CSV header, and rejects invalid score dimensions before saving. Distinct non-Latin company names no longer share the unknown-company job key.robots_check.pydecodes curl output as UTF-8, so a robots.txt with a CJK rule no longer reads as UNCONFIRMED on a Windows locale, and a non-ASCII disallow rule that silently never matched now does. Two docstrings that quoted\hand\sunescaped printed aSyntaxWarningon every run; both are fixed and a compile check overtools/*.pyandtests/*.pykeeps the next one out.State on disk stays honest (#441, #425, #440, #455, #444)
seen_jobs.jsonkeys are now a pure function of the posting, owned bytools/job_key.pyinstead of described in prose, so two runs can no longer store one job twice by truncating a title at different points;--auditreports legacy entries and never rewrites them./gmail-syncand/outcomekeep commas, quotes and lin - v1.7.1v1.7.1 - Cheaper ranking, portals that fail loudly, forks that stop fighting CI2026-09-06 · 2026-09-06 18:19 UTC
查看更新说明
A patch release with one theme: the workflow stops paying for work it does not need and stops hiding work it failed to do.
/rankno longer drags your whole job history through the conversation on every run, the portal CLIs now refuse bad input instead of silently returning the wrong thing, and forks get a clean CI and a complete/setup. Seventeen entries, fourteen of them fixes, most of them reported and fixed by contributors within a day of each other.Highlights
/rankis bounded and keeps its state on disk (#395, #424, #425, #390)A bare
/rankused to score the entire backlog in one go and read the whole ofseen_jobs.jsoninto the conversation twice per run - a cost that grew for the life of a workspace and was paid regardless of batch size (one reporter measured a 188 KB state file).--limit <N>now bounds each run to ten jobs by default, independently of--top, and reports what it deferred.tools/rank_state.pyowns the state traffic:candidatesselects and projects only the fields a scoring agent needs,sweepruns the expiry pass on disk, andapplywrites results back atomically. Scoring policy is unchanged, and every existing write-back rule (legacylocationmigration, null deadlines never erasing a stored one, verbatim strengths and gaps) is pinned by tests./rankalso now flags postings whose storedposted_dateis more than 30 days old.Portal CLIs fail loudly instead of lying (#426, #429, #371, #416, #418, #421)
The sharpest fix of the release: the unknown-flag guard in the four bunli-based CLIs only inspected
--longflags, so an undefined short flag was silently discarded and the search ran unfiltered -search -q "sygeplejerske"on jobnet returned 18,210 ads with exit 0 where the correct flag returns 669. Both dash forms are now checked. Thedetailcommands of jobbank, jobdanmark and jobnet accept full posting URLs as the contract always said they should,linkedin-searchrejects fractional numeric flags instead of truncating them, and a one-CLI-at-a-time audit closed three crashes where a single null field from a portal API killed the whole run (a jobbank feed date, a jobnet publication date, a jobdanmark autocomplete item).Forks get a clean CI and a complete
/setup(#405, #433, #396)The
python-testsjob ran the upstream-only placeholder-integrity tests on every personalised fork and failed permanently; they now skip whenGITHUB_REPOSITORYis a fork and still run locally and upstream./setupnow fills the contact blocks inside05-cv-templates.mdand06-cover-letter-templates.md- the two templates/applycompiles from - which a full run previously left as[YOUR_NAME]and[YOUR_EMAIL];/resetrestores them. Andsettings.jsonno longer pre-approvesbun runon arbitrary files: the allowlist is one path-scoped entry per shipped portal CLI.Salary data tools stop failing silently (#414, and the
salary_lookup.pynull fix)convert_salary_excel.pycould mistake a source-citation line above a real header for the header itself and drop every company's salary data with exit 0; header detection now requires cross-cell corroboration and warns when a sheet yields no salary columns.salary_lookup.pyno longer crashes on anullmetadata or categories field.Fork maintainers: what to reconcile
Pull this tag rather than raw
master, then check three things:/rankbehaviour changed. A bare run now scores at most 10 jobs and reports the rest as deferred; pass a larger--limitif you relied on scoring everything in one go. State moves throughtools/rank_state.py; if you customisedrank.md's Step 1 or Step 4, re-read the new versions before merging your changes over them..claude/settings.jsonchanged twice. The blanketBash(bun run:*)entry is gone in favour of one path-scoped line per shipped CLI, and twopython tools/rank_state.pylines were added. A fork with its own portal skills needs its own path-scope
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
[!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. Six commands carry you from an empty profile to a tracked result:
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.
The framework encodes career guidance best practices, including structured evaluation criteria, forward-looking cover letter framing, and optional salary benchmarking.
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).
What you need
| Claude Code | The CLI. No free tier: a Claude Pro/Max/Team subscription or Anthropic API credits (pay-per-token, usually cheaper for occasional use). |
| Python 3.10+ | For the tools in tools/. |
| Bun | Runs the job search CLI tools. |
| LaTeX | A distribution with lualatex and xelatex: TeX Live, MacTeX, TinyTeX, or MiKTeX. |
| pypdf (optional) | pip install pypdf for /apply's ATS parseability check. |
- 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. - LaTeX engines. The CV compiles with
lualatex(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. - PDF text extraction.
pypdfis BSD-licensed and needs no Poppler. Popplerpdftotextremains a fallback (macOS:brew install poppler, Debian/Ubuntu:apt install poppler-utils, Windows:choco install poppler). If both are missing, the check degrades to a visual keyword review.
1. Fork and clone
gh repo fork ai-job-search-dev/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
python3 tools/install_portal_clis.py # Windows: python tools\install_portal_clis.py
It runs bun install in every portal CLI under .agents/skills/*/cli/ - discovered, not listed, so a portal you add with /add-portal or copy from another fork is included on a fresh clone. Every install runs with --ignore-scripts, and a CLI whose package.json declares an install-time lifecycle script, trustedDependencies or workspaces, or that has a bunfig.toml or .npmrc beside it, is skipped and reported rather than run. --dry-run shows what it would install.
For 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
/scrape
This 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/1234567
If the URL can't be fetched (some job portals block automated access), you can paste the job description directly instead:
/apply <paste the full job description here>
This runs the full workflow: evaluate fit, draft CV + cover letter, review with a second agent, revise, and present the final output. The posting passes a deterministic integrity gate first (hidden text, invisible characters, instruction-shaped lines, hidden links), and its one-line verdict opens the evaluation; the agents that fetch and review read-only.
[!WARNING] 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.
Commands
/setup, /scrape, and /apply form the core workflow. Ten more commands extend it once your profile is in place.
| Command | What it does |
|---|---|
/setup | Builds your profile from your documents, a pasted CV, or an interview |
/scrape | Searches job portals for postings that match your profile |
/apply | Evaluates fit, then drafts, reviews, and compiles a tailored CV and cover letter |
/rank | Batch-scores a scrape into a ranked shortlist |
/interview | Builds a stage-specific prep pack and runs a mock interview |
/outcome | Records results, archives what you sent, and drafts follow-ups |
/gmail-sync | Proposes application status updates from your Gmail |
/notion-sync | Publishes a read-only view of your pipeline to Notion |
/html-report | Generates an offline dashboard of your applications |
/expand | Adds competencies found in your public profiles and course syllabi |
/upskill | Maps your skill gaps and builds a learning plan |
/add-template | Registers your own CV or cover letter template |
/add-portal | Generates a search skill for a job board in your market |
/reset | Wipes profile data or documents - see Starting over |
/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 injob_scraper/seen_jobs.json) — or a single posting via/upskill <URL>. 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.
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.
- 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 search
This 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-template
Point 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-portal
Give 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: /setup captures every language you work in and your level (asked directly, or inferred from your CV/LinkedIn export) into a Languages table, 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 weekly checkpoint that passed CI, with its changes listed on the release page. 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.
File structure
ai-job-search/
├── CLAUDE.md # Main candidate profile + workflow rules
├── .claude/
│ ├── commands/
│ │ ├── apply.md # /apply workflow (drafter-reviewer)
│ │ ├── setup.md # /setup onboarding (documents folder, CV import, or interview)
│ │ ├── expand.md # /expand competency enrichment from documents and online presence
│ │ ├── add-template.md # /add-template register custom templates (LaTeX, Typst, ...)
│ │ ├── add-portal.md # /add-portal generate a job-portal search skill for your market
│ │ ├── rank.md # /rank triage scraped jobs into a ranked shortlist
│ │ ├── outcome.md # /outcome record application results, archive materials
│ │ ├── gmail-sync.md # /gmail-sync auto-detect application status from Gmail
│ │ ├── interview.md # /interview stage-specific prep pack + mock interview
│ │ ├── html-report.md # /html-report generate application tracker dashboard
│ │ ├── notion-sync.md # /notion-sync one-way pipeline view in a Notion database
│ │ └── reset.md # /reset wipe profile data or documents folder
│ ├── skills/
│ │ ├── job-application-assistant/ # Core application skill
│ │ │ ├── SKILL.md # Skill definition
│ │ │ ├── 01-candidate-profile.md # Your education, experience, skills
│ │ │ ├── 02-behavioral-profile.md# PI/DISC/personality assessment
│ │ │ ├── 03-writing-style.md # Tone, structure, do's and don'ts
│ │ │ ├── 04-job-evaluation.md # Scoring framework for job fit
│ │ │ ├── 05-cv-templates.md # LaTeX CV structure + tailoring rules
│ │ │ ├── 06-cover-letter-templates.md # LaTeX cover letter templates
│ │ │ └── 07-interview-prep.md # STAR examples + interview framework
│ │ ├── job-scraper/ # Job search orchestration
│ │ └── upskill/ # /upskill skill gap analysis and learning plan
│ └── settings.json # Claude Code permissions (shared, scoped)
├── .agents/skills/ # Job portal CLI tools
│ ├── jobbank-search/ # Akademikernes Jobbank (Denmark)
│ ├── jobdanmark-search/ # Jobdanmark.dk (Denmark)
│ ├── jobindex-search/ # Jobindex.dk (Denmark)
│ ├── jobnet-search/ # Jobnet.dk (Denmark, government portal)
│ ├── linkedin-search/ # LinkedIn public job listings (country-agnostic)
│ └── freehire-search/ # freehire.me tech job aggregator (multi-market, REST API)
├── assets/ # README artwork (Pip the mascot, animated SVG diagrams)
├── cv/
│ └── main_example.tex # moderncv LaTeX template
├── cover_letters/
│ ├── cover.cls # Custom cover letter LaTeX class
│ ├── cover_example.tex # Example cover letter (structural reference + CI smoke test)
│ └── OpenFonts/ # Lato + Raleway fonts
├── templates/ # Custom templates registered via /add-template
│ └── README.md # Folder layout instructions
├── documents/ # Career source materials for /setup Path A and /expand
│ ├── README.md # Folder layout instructions
│ ├── cv/ # Master CV (PDF or .tex)
│ ├── linkedin/ # LinkedIn profile export (PDF)
│ ├── diplomas/ # Degree certificates and transcripts
│ ├── references/ # Reference letters
│ └── applications/ # Past application records (<company>_<role>/)
├── .github/workflows/ci.yml # CI: LaTeX smoke compiles, skill lint, CLI typechecks
├── salary_lookup.py # Salary benchmarking tool (BYO data)
├── tools/
│ ├── check_framework_version.py # CI check: framework_version bumped when skill files change
│ ├── check_upstream_updates.py # Preview which personalized files an upstream update touches
│ ├── convert_salary_excel.py # Convert salary Excel to JSON
│ ├── fetch_page.py # GET-only browser-header retry for 403s (robots.txt enforced)
│ ├── install_portal_clis.py # Install every discovered portal CLI (bun install)
│ ├── lint_skills.py # CI lint for skills, commands, settings.json
│ ├── posting_guard.py # Integrity gate: strip hidden text, flag injection-shaped lines
│ ├── robots_check.py # Gate the browser-header retry against robots.txt
│ ├── security_guards.py # CI guards: permission allow/ask/deny, skill grants, gitignore, manifests
│ ├── upstream_triage.py # Sort upstream commits into worth-reviewing vs probably-skip
│ ├── verify_layout.py # Measure a compiled PDF's page layout (holes, orphans, footer collisions)
│ ├── verify_pdf.py # Verify a compiled PDF's page count and extractable text
│ └── README_SALARY_TOOL.md # Salary tool setup instructions
├── job_scraper/ # Scraper state (seen jobs, results)
├── gmail_sync/ # /gmail-sync state (processed message IDs, last sync date)
├── upskill/ # /upskill report output (markdown reports per run)
├── job_search_tracker.csv # Application tracking spreadsheet
└── SETUP.md # Detailed setup guide
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