git diff for your CV.
You rewrite your CV for each job. Then you forget what changed. You can't compare versions. Next time you start from the same generic file.
ApplyDiff is local-first job search, CV tailoring, and interview prep in one binary (CLI, TUI, and Web). It scans job boards without spending model tokens. It scores each posting against your structured CV. You review every AI edit hunk by hunk before it goes into the PDF. Applications and interview loops live in one SQLite file on your machine.
Quick start · How it works · Web UI flow · How to use · Comparison · CLI reference
Most AI job tools chase volume. They rewrite the whole resume, fill the form, and send 300 applications into silence. You lose track of what changed. You can't check if a number was made up. Next time you start over from the same generic file.
ApplyDiff is built for control:
- Every application is a branch. Your master CV (
cv/master_cv.json) stays as-is. Each job gets its own branch off master. - Every edit is a hunk. The model suggests field-level hunks (
@@ summary @@,@@ exp.gofood.b1 @@,@@ skills.backend @@) with a reason and supporting facts. You don't get a full rewrite to proofread from scratch. - You approve every hunk. Accept, reject, or edit each hunk in the CLI, the keyboard TUI, or the Web UI. Only approved hunks go into
output/<date>-<company>-<role>.pdf. facts.jsonblocks fake numbers. If a number is not in your verified facts file, the facts check drops it.
- Submit for you.
applydiff applyonly records that you applied, and through which channel. You click Submit. - Invent numbers or skills. Missing requirements show up as
gap. They are not stuffed into the CV. - Scrape behind a login. Sources use public applicant tracking system (ATS) APIs and RSS/JSON feeds, with caps and dedupe. Full list: docs/references/sources.md.
- Call home. No account, no cloud sync, no telemetry. Your CV, branches, hunks, and tracker live in one local
applydiff.dbSQLite file.
| Surface | Command | Best for |
|---|---|---|
| CLI | applydiff <command> |
Scripts, cron scans, pipes (--json, --dry-run) |
| TUI | applydiff tui |
Fast hunk review and timed interview drills, keyboard only |
| Web | applydiff serve |
Side-by-side paper CV preview, inline hunk buttons, the full pipeline |
All three call the same Go core in internal/ and share one SQLite file (applydiff.db). The UI has no business logic.
Local dev: make dev (API on :8080, Nuxt on :3000 with /api proxied). Embedded UI: make web && make serve.
flowchart LR
Scan["Jobs: Scan"] --> Feed["jobs table\n(discovery feed)"]
CV["CV editor:\nSave master"] --> Match["Match: score\n?job="]
Feed --> Match
Match -->|"RecordMatchPipeline"| Apps["applications table\n(status matched)"]
Match -->|"Shortlist / Tailor / Skip"| Apps
Match -->|"Tailor button"| Diff["Diff: hunks\n?job="]
Apps --> Export["Export PDF"]
Diff --> Export
| Tab | What it is | When a role shows up |
|---|---|---|
| Jobs | Scanned listings, filters, risk flags | After Scan (POST /api/scan) — every stored posting |
| CV editor | Structured master CV | You click Save master (PUT /api/cv/master) — required before match |
| Match | Fit score + requirements map | Open from a job row (/match?job=…); successful score runs RecordMatchPipeline → applications.status = matched |
| Applications | Pipeline tabs (All, Matched, Shortlisted, Tailoring, Ready, With CV diff) | INNER JOIN on applications only — not every scanned job; sub-tabs filter by status (With CV diff = tailor proposal exists) |
| Diff | Per-field CV hunks (accept / reject) | After Tailor on Match, or Generate hunks on Diff — uses the LLM |
| Interview | Kits, mocks, STAR bank | Same pipeline roles; creates an application record when needed |
Brief and QA notes: docs/visual-briefs/web-pipeline-flow.md. Scan-only path: assets/editorial-scan-web-flow.svg.
Install a release binary (it embeds the Nuxt 3 web UI and SQLite migrations):
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/pahlevikun/applydiff/main/scripts/install.sh | bash
# Windows (PowerShell)
irm https://raw.githubusercontent.com/pahlevikun/applydiff/main/scripts/install.ps1 | iex# Homebrew (after you publish the tap; see packaging/homebrew/applydiff.rb)
brew install pahlevikun/tap/applydiff
# Scoop (after you publish the bucket; see packaging/scoop/applydiff.json)
scoop bucket add applydiff https://github.com/pahlevikun/scoop-applydiff
scoop install applydiffThen import your master CV and run the pipeline:
applydiff cv import cv/master_cv.json
applydiff scan
applydiff tui
applydiff serve # http://localhost:8080Build from source (Go + Node):
git clone https://github.com/pahlevikun/applydiff && cd applydiff
make web && make cli
./bin/applydiff serveWant a one-command walkthrough on sample data? From a git checkout, run ./scripts/demo.sh.
You don't need an API key if you already have a coding-agent CLI logged in (claude, codex, cursor-agent, or hermes). PDF builds use xelatex on PATH when available, otherwise Tectonic (brew install tectonic). Set APPLYDIFF_PDF_ENGINE to force one engine.
ApplyDiff keeps a structured JSON master CV plus cv/facts.json for numbers and claims you have checked. Import loads sibling facts.json from the same directory:
./bin/applydiff cv import cv/master_cv.jsonIf the model suggests a number during tailoring or STAR story writing, and that number is not in your master CV or facts.json, the facts check rejects it.
ApplyDiff looks for a coding-agent CLI on your PATH first. If none is there, it falls back to an API key.
Option A. Coding-agent CLI (recommended, no API key):
If claude (claude -p), codex (codex exec), cursor-agent, or hermes is on your PATH, ApplyDiff uses it. You can also pin a command:
export LLM_PROVIDER=cli
export LLM_CLI="claude -p" # or "codex exec", "cursor-agent"Option B. OpenAI-compatible API:
export LLM_PROVIDER=api
export LLM_API_KEY="sk-..."
export LLM_MODEL="gpt-4o-mini" # default: gpt-4o-mini
export LLM_API_URL="https://api.openai.com/v1/chat/completions" # or Ollama / OpenRouterOption C. Offline fake mode (tests and CI):
export LLM_PROVIDER=fakeEdit jobs/config.yaml to turn sources on, add Greenhouse company tokens, and set hard vs soft filters:
- Hard filters (
hard: true) skip listings that don't match, and they say why. Examples:regions: [SEA, EU, global],arrangement: [remote, hybrid],min_seniority: [senior],max_posted_days: 14. - Soft filters (
hard: false, boost: N) keep the listing and bump its score. Examples:visa_sponsorship,eor_friendly,async_friendly,weekly_days: ["4", "4.5"],salary_min,must_have_keywords.
Preview the HTTP calls without writing to SQLite, or scan with a preset:
./bin/applydiff scan --dry-run
./bin/applydiff scan --source greenhouse
./bin/applydiff scan --preset aggressive --max-risk 30The
aggressivepreset keeps only fresh postings (<= 5 hoursold) with low competition.
List scanned jobs, open one posting, then score it against your master CV:
# List all scanned jobs (add --json for scripting)
./bin/applydiff jobs
# View the job text, risk/scam flags, and match breakdown
./bin/applydiff show <job-key>
# Score the posting against your master CV (literal / synonym / gap per requirement)
./bin/applydiff match <job-key>Propose a tailored branch for a job, then read the unified diff:
# Propose field-level hunks for this job
./bin/applydiff tailor <job-key>
# View colored unified diff in the terminal
./bin/applydiff diff <job-key>
# Accept or reject specific hunks (1-based) or approve all
./bin/applydiff approve <job-key> --hunk 1,2
./bin/applydiff reject <job-key> --hunk 3
./bin/applydiff approve <job-key> --allOr review live:
- TUI (
./bin/applydiff tui):Tab/Shift+Tab: switch screens (Feed,Job,Diff,Sources,Applications,Interview)j/kor↑/↓: move across jobs or hunks (g/Gfor top/bottom)/: filter jobs by title, company, or regionEnter/o: open the selected joba/x/Space: accept, reject, or toggle a hunk on theDiffscreenb: build the LaTeX PDF for the active job branch?: keyboard help (qto quit)
- Web UI (
./bin/applydiff serve):- Open
http://localhost:8080. Review+/-marks on the CV sheet, check ATS keyword tags, and click Accept, Reject, or Edit per hunk.
- Open
When every hunk is accepted or rejected, build the PDF and record that you applied:
# Compile output/<YYYY-MM-DD>-<company>-<role>.pdf
./bin/applydiff build <job-key>
# Record your manual application and channel
./bin/applydiff apply <job-key> --mode manual --channel referral --referrer "@alice"
# Reply and interview rates per channel
./bin/applydiff statsEach tracked job can have an interview kit. That includes a predicted loop, 6-step timed system design drills, timed mocks with a 5-dimension rubric, and a STAR story bank that only uses numbers from your facts:
./bin/applydiff interview kit <job-key> # Generate per-job interview loop & prep kit
./bin/applydiff interview drill <job-key> # Run the 6-step timeboxed system design drill
./bin/applydiff interview mock <job-key> # Start a timed mock session
./bin/applydiff interview story <job-key> # Draft/view facts-verified STAR stories
./bin/applydiff interview score <job-key> # Score mock performance across 5 dimensions
./bin/applydiff interview export <job-key> # Export interview prep packAll state lives in one SQLite file. Copy it to another machine with:
./bin/applydiff export backup.db
./bin/applydiff import backup.db| Area | What you get |
|---|---|
| Scan without tokens | Built-in adapters for Greenhouse, Remotive, RemoteOK, Jobicy, We Work Remotely, and Working Nomads. Dedupe by normalized URL and posting ID. Dead links get dropped before any LLM spend. |
| Filters | Hard filters skip with a reason. Soft filters boost the score. Cover work setup, 4-day week, region, timezone overlap, seniority, pay, visa, employer of record (EOR) / contractor, and how fresh the posting is. |
| Scam screen | Rule-based flags for email-domain mismatch, pay-to-work fees, ID before interview, clone-and-run malware traps, and missing company domains. Flags warn you. They never hide the listing. |
| Match | Scores the posting against your CV. Each requirement is literal, synonym, or gap, with evidence. |
| Hunk tailoring | Suggests field edits ({ field_id, before, after, rationale, evidence[] }). facts.json and the ATS keyword check both apply. |
| LaTeX PDF and proof page | Builds ATS-readable PDFs with xelatex (output/<date>-<company>-<role>.pdf), plus a one-page proof sheet from the same verified facts. |
| Channel stats | Tracks draft, ready, and applied across board, referral, inbound, linkedin, and email. applydiff stats shows reply and interview rates per channel. |
| Interview prep | Per-job loop, 6-step timed system design, 5-dimension rubric, and a STAR bank that rejects unverified numbers. |
ApplyDiff borrows ideas from career-ops, ai-job-search, and job-sequencer. The extra piece is per-hunk diff approval on a branched CV.
| Capability | ApplyDiff | career-ops | ai-job-search | job-sequencer |
|---|---|---|---|---|
| Approval granularity | Per-hunk (accept / reject / edit each line change) |
Whole document | Whole document (drafter-reviewer) | Whole document (Drafting → Ready) |
| CV version model | Master JSON + isolated branch per job | Markdown cv.md |
LaTeX .tex files |
JSON profile + revision artifacts |
| Surfaces included | CLI + TUI + embedded Web UI in 1 binary | Agent slash commands + Go TUI | Agent slash commands (/setup, /apply) |
React Web UI + Fastify API |
| Runtime footprint | Single Go binary + pure-Go SQLite | Node.js + Playwright + Go | Python + Bun + Claude Code | Node.js 24 + Bun + Vite |
| Facts check | facts.json number and claim check |
cv:verify-facts script |
Drafter-reviewer prompt + checklist | Claim validator + factual-auditor agent |
| Scam and legitimacy check | Risk rules + score (0-100) |
Block G posting legitimacy check | Prompt-level untrusted-input rules | Same-run provenance + host allowlist |
| PDF engine | LaTeX (xelatex) + ATS text checks |
HTML + Playwright Chromium | LaTeX (lualatex / xelatex) + pdftotext |
LaTeX (lualatex / xelatex) + pdfinfo |
| Auto-submit | Never (manual by default) | Never (You press Submit) |
Never | Never |
| Channel | Command |
|---|---|
| curl (macOS / Linux) | curl -fsSL https://raw.githubusercontent.com/pahlevikun/applydiff/main/scripts/install.sh | bash |
| PowerShell (Windows) | irm https://raw.githubusercontent.com/pahlevikun/applydiff/main/scripts/install.ps1 | iex |
| Homebrew | brew install pahlevikun/tap/applydiff (tap repo required; template in packaging/homebrew/) |
| Scoop | scoop bucket add applydiff <your-bucket-url> then scoop install applydiff (template in packaging/scoop/) |
| GitHub Releases | Download applydiff_<version>_<os>_<arch>.tar.gz or .zip from Releases |
Set INSTALL_DIR to change the install location (default ~/.local/bin). Pin a version with APPLYDIFF_VERSION=1.0.0.
Docker (Web UI + API + CLI + TeX Live in one image).
docker build -t applydiff .
docker run -p 3000:3000 -v ~/.applydiff:/data applydiffFrom source. make web && make cli. See Development & verification.
applydiff cv import <path> Import structured master CV (and sibling facts.json)
applydiff scan Pull jobs from configured sources (--source, --preset, --min-risk, --max-risk)
applydiff jobs List scanned jobs with filter scores and keep/skip decisions
applydiff show <job-key> Show job details, risk flags, and match breakdown
applydiff match <job-key> Compute requirement match score against master CV
applydiff tailor <job-key> Propose field-level tailoring hunks (LLM)
applydiff diff <job-key> Render unified diff per hunk (--color auto|always|never)
applydiff approve <job-key> Accept hunks (--hunk 1,2 or --all)
applydiff reject <job-key> Reject hunks (--hunk 3)
applydiff build <job-key> Compile LaTeX PDF for an approved branch into output/
applydiff apply <job-key> Record application submission (--channel, --referrer, --mode)
applydiff stats Show reply and interview conversion rates per channel
applydiff interview <subcommand> kit | mock | drill | story | score | list | export
applydiff tui Launch the interactive Bubble Tea terminal UI
applydiff serve Launch the embedded Web UI and HTTP API (--port 8080)
applydiff export <file> Export SQLite database snapshot to a file
applydiff import <file> Import SQLite database snapshot from a file
Global flags: --json (JSON on stdout for scripts and pipes) · --dry-run (preview, don't write to SQLite).
Exit codes: 0 ok · 2 no match · 3 needs approval · 4 LLM error · 5 LaTeX error.
| Variable | Purpose |
|---|---|
DATABASE_PATH |
SQLite file path (default applydiff.db) |
JOBS_CONFIG |
Job sources and filters YAML (default jobs/config.yaml) |
PORT |
HTTP port for applydiff serve (default 8080) |
OUTPUT_ROOT |
PDF output directory (default output/) |
FACTS_PATH |
Verified facts JSON (default cv/facts.json in repo, else facts.json under data home) |
MASTER_CV_JSON |
Structured master CV JSON (default cv/master_cv.json) |
MASTER_CV_TEX |
Master CV LaTeX source (default cv/master_cv.tex) |
LLM_PROVIDER |
cli (auto-detected), api, or fake (offline) |
LLM_CLI |
Coding-agent command when LLM_PROVIDER=cli (e.g. claude -p, codex exec) |
LLM_API_KEY |
API key when LLM_PROVIDER=api |
LLM_MODEL |
Model name when LLM_PROVIDER=api (default gpt-4o-mini) |
LLM_API_URL |
Chat completions URL when LLM_PROVIDER=api |
APPLYDIFF_PDF_ENGINE |
Force xelatex or tectonic (default: xelatex if on PATH, else Tectonic) |
applydiff/
├── cmd/applydiff/ # Unified CLI, HTTP API server, and Bubble Tea TUI entrypoint
├── internal/
│ ├── source/ # Job board adapters (greenhouse, remotive, remoteok, jobicy, wwr, workingnomads)
│ ├── scan/ # Bounded scanner orchestration & deduplication
│ ├── filter/ # Hard/soft filter evaluator & presets (e.g. aggressive)
│ ├── risk/ # Deterministic scam & legitimacy rule engine
│ ├── match/ # CV requirement matcher (literal / synonym / gap)
│ ├── tailor/ # LLM hunk generator, parser, facts gate, and ATS checker
│ ├── interview/ # Interview loop kit, 6-step drill timer, rubric, STAR story bank
│ ├── store/ # Pure-Go SQLite persistence & timestamped SQL migrations
│ ├── webembed/ # Embedded Nuxt 3 static build served by `applydiff serve`
│ └── runtime/ # Shared application service layer (used by CLI, TUI, and Web)
├── web/ # Nuxt 3 frontend (Jobs, Match, Diff, My CV, Interview, Sources)
├── jobs/config.yaml # Declarative sources, search queries, and filter rules
├── cv/ # Master CV: master_cv.json, master_cv.tex, facts.json
└── scripts/ # demo.sh walkthrough and release.sh cross-compiler
Run the tests without live LLM credentials or an extra database:
make test # Run all unit, migration, filter, risk, TUI snapshot, and API tests (go test ./...)
make web # Build the Nuxt 3 frontend into internal/webembed/public
make cli # Build bin/applydiff with embedded assets
make docker # Build the self-contained Docker image (UI + API + TeX)
make release # Local cross-compile into dist/ (CI uses GoReleaser on tag push)
make release-snapshot # Local GoReleaser dry run (requires goreleaser CLI)Migrations are timestamped SQL files in internal/store/migrations/.
MIT.