career-ops is an AI job search tool built to make you apply to fewer jobs, not more. Paste a job link or description into your AI coding tool and it checks the posting is still live, scores it from 1 to 5 against your real CV, tells you to skip a weak fit, then tailors your CV as a PDF, drafts a cover letter and form answers, and adds a row to your tracker. It can also scan job boards through more than 55 provider modules, find who to contact and draft the note, and prepare you for interviews.
Santiago Fernández built it for his own search and reports 740 listings evaluated, 68 applications, 12 interviews and 1 offer; those are his numbers. It has since grown to about 81,000 stars, 616 contributors and 59 releases, with v1.39.0 current. It runs in Claude Code, Codex, OpenCode and other AI command-line tools, and free or local models work too.
The line it holds is the reason to trust it with a job search: it does not submit. The apply mode fills in a form with you field by field and stops before the Submit button, the email mode only writes drafts, and there is no mail code in the project. It also has a check that stops a tailored CV whose numbers or facts are not in your source material, though the project still tells you to read every CV before sending it.
- Repository: github.com/career-ops-hq/career-ops
- Licence: MIT (MIT License)
- Language: JavaScript. Stars: 80.8K. Forks: 14.9K. Last push: Nov 7, 2026.
- Scan: safe, Nov 7, 2026, commit 0339acb
Who it is for
Job seekers comfortable with a terminal who already use or are willing to try an AI coding tool, especially people in tech applying to many roles who want a filter and a tracker rather than a spray-and-pray bot. If you avoid terminals, the web version is only a waitlist so far.
Getting started
1. Create the project (Node.js needed). This also downloads Chromium, used for PDFs and browser checks
npx @santifer/career-ops init2. Open your AI tool in the new folder and it walks you through setup by chat
cd career-ops && claude3. Then paste a job into the chat
Evaluate this job: <paste the link or description>Swap claude for codex, opencode or another supported tool. Set PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 before installing to skip the Chromium download. Read everything it writes before you send it.
Safety scan
We cloned career-ops-hq/career-ops at commit 0339acb on Nov 7, 2026 and ran the checks described on the GitHub Tools page: credential patterns, decode-and-execute code, install-time scripts, committed binaries, risky CI workflows, every host the code talks to, known vulnerabilities in pinned dependencies, and project hygiene. A person read every hit. This is what we found.
- It does not apply for you. The README says the script never POSTs and the system never submits, and modes/apply.md tells the agent to stop before Submit, Send or Apply. The project's own legal note adds the fair caveat that this rests partly on prompts and AI models can behave unpredictably, so watch it when it is filling in a live form.
- Your CV leaves your machine, by design. Evaluations send your whole CV, profile and job descriptions to the AI provider you chose, and the README says nowhere else: no telemetry and no backend of theirs. An optional privacy guard can block requests containing values you list. Use a local model through Ollama if you want nothing sent at all.
- All 7 pattern hits are one optional feature: web/scripts/install-scan-schedule.sh and its uninstaller writing a macOS LaunchAgent or a crontab line. Scheduled scans belong to the alpha web UI and you have to run the script yourself. The worker wakes every 15 minutes, runs as your user and never as root, and per the docs never evaluates roles, applies or calls an AI model. The uninstall script removes it.
- One install hook: the postinstall in package.json downloads Chromium through Playwright, for PDFs and for checking postings. It fetches nothing else, and PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 turns it off. The 4 installer scripts the scanner counted are the install and uninstall scripts for the schedule, for shell and PowerShell.
- 2 unrated advisories across 268 packages, both in golang.org/x/text 0.3.8, an indirect dependency of the terminal dashboard; they cover crafted input, and that dashboard is not a network service. The web UI's lockfile has none. The root Node project has no lockfile we could check, so its dependencies are not covered by this count.
- No secret hits and no binaries. The 9 bare-IP URLs are all in test-all.mjs, testing that its network code refuses unsafe redirects. Most of the top hosts are job boards such as Greenhouse, Lever and Ashby, plus test placeholders.
- 28 workflows and strong hygiene: security policy, Dependabot, Renovate, CodeQL, licence and contributing guide all present, and all 3 third-party actions pinned to a commit. The scanner flagged 5 files for pull_request_target and none checks out pull request code: one, plugin-registry-validate, only mentions it in a comment, two use event metadata alone, and two check out the base branch.
What the scanner counted
| Check | Result |
|---|---|
| Secrets | None found. |
| Suspicious code | 7 pattern hits found and read; every one is listed under the raw findings. |
| Install-time code | 1 npm lifecycle script. 4 installer scripts |
| Committed binaries | None. |
| CI workflows | 28 workflows. 5 use pull_request_target, none check out the pull request head. 0 of 3 third-party actions pinned to a tag rather than a commit. |
| Network hosts | 40 distinct hosts referenced from source; most often jobs.example.com, boards.greenhouse.io, jobs.lever.co, job-boards.greenhouse.io. 9 URLs to a bare IP address, listed under the raw findings. |
| Known vulnerabilities | 2 advisories across 268 pinned packages: 0 critical, 0 high, 0 moderate, 0 low, 2 unrated. dashboard/go.mod: 21 packages, 2 advisories; web/package-lock.json: 248 packages, 0 advisories. |
| Project hygiene | Has security policy, automated dependency updates, CodeQL, licence file, contributing guide. |
| OpenSSF Scorecard | Not scored: the project is not in Scorecard's weekly index. |
The raw findings
Every hit the scanner wrote out, with a link to the exact line at the scanned commit. Secrets candidates are redacted.
Pattern hits (7)
| Where | Rule | Match |
|---|---|---|
| web/scripts/install-scan-schedule.sh:94 | persistence | PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" |
| web/scripts/install-scan-schedule.sh:160 | persistence | if ! crontab -l >"$TMP" 2>"$ERR"; then |
| web/scripts/install-scan-schedule.sh:174 | persistence | crontab - <"$NEW" |
| web/scripts/uninstall-scan-schedule.sh:11 | persistence | PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" |
| web/scripts/uninstall-scan-schedule.sh:27 | persistence | if ! crontab -l >"$TMP" 2>"$ERR"; then |
| web/scripts/uninstall-scan-schedule.sh:44 | persistence | crontab - <"$NEW" |
| web/scripts/uninstall-scan-schedule.sh:46 | persistence | crontab -r |
URLs to bare IP addresses (9)
| Where | Rule | Match |
|---|---|---|
| test-all.mjs:16263 | ip-literal-url | if (u === 'https://93.184.216.34/start') return new Response(null, { status: 302, headers: { location: 'https://93.184.216.35/final' } }); |
| test-all.mjs:16263 | ip-literal-url | if (u === 'https://93.184.216.34/start') return new Response(null, { status: 302, headers: { location: 'https://93.184.216.35/final' } }); |
| test-all.mjs:16264 | ip-literal-url | if (u === 'https://93.184.216.35/final') return new Response(JSON.stringify({ ok: 1 }), { status: 200 }); |
| test-all.mjs:16265 | ip-literal-url | if (u === 'https://93.184.216.34/bad') return new Response(null, { status: 302, headers: { location: 'https://10.0.0.1/x' } }); |
| test-all.mjs:16269 | ip-literal-url | let httpRej = false; try { await gctx.fetch('http://93.184.216.34/x'); } catch { httpRej = true; } |
| test-all.mjs:16272 | ip-literal-url | let outRej = false; try { await gctx.fetch('https://8.8.8.8/x'); } catch { outRej = true; } |
| test-all.mjs:16276 | ip-literal-url | const r = await gctx.fetch('https://93.184.216.34/start', { headers: { Authorization: 'Bearer secret' } }); |
| test-all.mjs:16277 | ip-literal-url | const cross = fetchCalls.find(c => c.url === 'https://93.184.216.35/final'); |
| test-all.mjs:16283 | ip-literal-url | let ssrfRej = false; try { await gctx.fetch('https://93.184.216.34/bad'); } catch { ssrfRej = true; } |
npm lifecycle scripts (1)
package.jsonpostinstall:node -e "process.exit(['','0','false'].includes((process.env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD||'').toLowerCase())?1:0)" || npx playwright install chromium || npx playwright install chromium || node -e…
Installer scripts (4)
- web/scripts/install-scan-schedule.ps1, 25 lines
- web/scripts/install-scan-schedule.sh, 177 lines; talks to www.apple.com
- web/scripts/uninstall-scan-schedule.ps1, 18 lines
- web/scripts/uninstall-scan-schedule.sh, 53 lines
Worst known vulnerabilities (2 of 2)
| Advisory | Severity | Package | Summary |
|---|---|---|---|
| CVE-2026-56852 | unknown | golang.org/x/text@0.3.8 | Infinite loop on invalid input in golang.org/x/text |
| CVE-2026-56851 | unknown | golang.org/x/text@0.3.8 | Panic parsing crafted input in x/text/secure/precis in golang.org/x/text |
Workflows worth a look
- .github/workflows/gh-events-feed.yml: pull_request_target
- .github/workflows/labeler.yml: pull_request_target
- .github/workflows/plugin-registry-validate.yml: pull_request_target
- .github/workflows/pr-triage.yml: pull_request_target
- .github/workflows/welcome.yml: pull_request_target
By the numbers
| Stars | 80.8K |
|---|---|
| Forks | 14.9K |
| Contributors | 616 |
| Commits | 3,935 |
| Open issues | 267 |
| Open pull requests | 166 |
| Releases | 59 |
| Latest release | career-ops-v1.39.0 |
| Licence | MIT |
| Main language | JavaScript |
| Project age | 7 months |
| Last push | Nov 7, 2026 |
| Tracked files | 3,206 |
| Lines of code | 626.3K |
| Checkout size | 50 MB |
Lines by language: JavaScript 476.1K, Markdown 74.3K, TypeScript 30.8K, Go 20.7K, YAML 10K, HTML 6,336.
Questions
Is career-ops free?
Yes, MIT-licensed, and the project says it stays free for candidates. Your only cost is the AI model, and it also runs on free and local models through OpenRouter or Ollama. SerpApi sponsors the project, and the README says sponsors never appear in evaluations or rankings.
Does career-ops apply to jobs automatically?
No. It evaluates, ranks and drafts, and you press Submit. The apply mode fills a form with you and stops before the final button, and email mode only writes drafts. It is built as a filter, and it advises against applying to roles scoring below 4 out of 5.
Where does my CV go?
It stays in files on your machine and is sent to the AI provider you choose whenever a job is evaluated. The README says the project collects nothing and has no backend. If you do not want your CV with a cloud provider, run a local model.
Will it leave anything running on my computer?
Not by default. A background task only exists if you run the scheduled scan install script from the web UI folder. It checks for due scans every 15 minutes until you run the matching uninstall script.
This post is part of GitHub Tools, where every repository is cloned and scanned before it is written up. The scan is a snapshot of one commit on one day; the repository has moved on since, so check it before you install.
