MJ

maazbin/jobpilot

Developer tools
6 stars 8 forks Quality 86 Trend 34MIT

Self-evolving, agent-driven job hunter. An MCP server (no LLM of its own) that drives YOUR logged-in Chrome to apply via email, LinkedIn Easy Apply, ATS forms & connections — and learns from every...

Overview

Your coding agent is the brain. JobPilot has no model of its own. It is the MCP tool server, the browser hands, the dedup memory, and the loop that turns a stuck page into a fix on GitHub. Indeed Apply now works in the normal signed-in window. One brain. It never starts a second brain. The worker only collects leads. Every email, Easy Apply, portal application, and connection request is a decision the brain makes, then a tool it calls. A single lead moves like this. The brain picks the channel. The tool does one concrete action. When a page wins, the system does not drop the lead and forget. It keeps the evidence and the fix comes back as code. No API keys. No SMTP. Sessions stay in your browser. Personal data stays in data/, which git ignores. Newest posts first. The brain chooses. Tools do not pick a channel on their own. Email is uncapped because it is your Gmail. The brain paces it.

README


The shape of it

One brain. It never starts a second brain. The worker only collects leads. Every email, Easy Apply, portal application, and connection request is a decision the brain makes, then a tool it calls.

flowchart TB
  you["You"] --> brain["Brain — Grok, Codex, Claude, Gemini, or any MCP CLI"]
  brain -->|"reads AGENTS.md, calls tools"| mcp["JobPilot MCP"]

  subgraph tools ["Tools the brain drives"]
    direction LR
    search["Search the open web"]
    act["Email · Easy Apply · ATS · Connect"]
    memory["Dedup memory"]
    evolve["Evolution memory"]
  end

  mcp --> tools
  search --> db[("jobs.db")]
  act --> chrome["Your logged-in Chrome"]
  chrome --> gmail["Gmail"]
  chrome --> linkedin["LinkedIn"]
  chrome --> ats["Greenhouse · Lever · Ashby · Workable"]
  act --> memory
  memory --> db

  worker["Data-only worker\nscout + dashboard"] --> db
  db --> brain

  chrome -->|"stuck page, screenshot + HTML"| evolve
  evolve -->|"patch the module"| repo["GitHub"]
  repo -->|"next session reads the fix"| brain

A single lead moves like this. The brain picks the channel. The tool does one concrete action.

flowchart LR
  lead["Fresh lead"] --> judge{"Relevant?\nAlready contacted?"}
  judge -->|no| skip["Record a skip and why"]
  judge -->|email| mail["send_email\nCV attached"]
  judge -->|Easy Apply| easy["apply_linkedin_easy\nstop at the safety cap"]
  judge -->|ATS or board| form["apply_portal\nreal Greenhouse / Lever / Ashby / Workable"]
  judge -->|poster only| connect["linkedin_connect\nshort note"]
  mail --> done["Never touch that lead again"]
  easy --> done
  form --> done
  connect --> done
  skip --> done

When a page wins, the system does not drop the lead and forget. It keeps the evidence and the fix comes back as code.

flowchart LR
  fail["Stuck page"] --> snap["Screenshot + HTML\nunder data/evolution"]
  snap --> play["PLAYBOOK.md"]
  play --> patch["Brain patches the module"]
  patch --> pr["contribute_improvements\nPR to GitHub"]
  pr --> sync["Sync main\nnext run is smarter"]
  sync --> fail

What the brain does, and what the tools do

Brain (your CLI) JobPilot
Decides Which roles, which country, which channel, what the email says, when to stop Nothing. No LLM lives here.
Acts Calls one tool per action Drives your Chrome, sends the mail, fills the form, writes the row
Remembers Reads the bank and the playbook data/jobs.db dedups email, apply, and connect across runs and across agents
Improves Patches the module that failed Saves the encounter, opens the PR, refuses to commit data/ or credentials

No API keys. No SMTP. Sessions stay in your browser. Personal data stays in data/, which git ignores.


Four channels

Newest posts first. The brain chooses. Tools do not pick a channel on their own.

Channel Tool What “done” means
Email send_email Sent from your Gmail. CV attached. Portfolio and links come from the profile. A JobPilot repo line is added only when that user saved links.jobpilot.
LinkedIn Easy Apply apply_linkedin_easy Modal walked, questions answered, “Application submitted” seen.
External form apply_portal Job-board URL resolved to the real ATS, then submitted.
Connect linkedin_connect A short note to the poster when there is no email and no form.

Email is uncapped because it is your Gmail. The brain paces it. Easy Apply is capped: JobPilot refreshes the public daily limit, stops at 80% (default 50/day, so stop at 40), and waits a random 90–240s between submissions. A failed lookup never raises the cap.


v3.0.0 — what this stable line solved

This is the line we will keep. New work happens after this tag.

The architecture

  • The worker is data-only when you run npm run agent. It scrapes into data/jobs.db and refreshes the dashboard. It does not email, apply, or connect.
  • One brain never starts another. There is no dispatcher that launches Codex, Grok, Claude, or Gemini.
  • AGENTS.md is the only operating guide. Any MCP coding agent can drive the same tools.

Outreach that used to fail quietly

  • Known-fake hiring addresses live in src/fake-emails.txt, one address per line. Send refuses that list. Adding a decoy is a file edit, not a new regular expression.
  • Titles that match the profile (backend, Python, AI, platform, and the rest of the role list) stay in the email bank. They are no longer dropped as “not the CV.”
  • An answer you type in the Questions tab survives when the worker saves the database. The newer answer wins, so a second process cannot put the question back.
  • Salary questions can convert the profile currency into SAR and QAR, and current pay stays separate from the expected range.
  • Lever’s real control is Submit application. The hidden hCaptcha button is no longer treated as the submit button. A live captcha can still stop the submit. That is an open problem, not a solved one.
  • Phone-country widgets and cookie bars are closed before an external form is submitted.
  • Easy Apply treats an intercepted LinkedIn click as a throttle and backs off, instead of hammering the page.
  • Each job can select a private CV variant from preferences.cvVariants. The canonical CV stays the fallback. Resumes never go in git.
  • Search treats Middle East, EU, relocation, and visa sponsorship as desirable. Country scope still blocks outreach outside the profile when that scope is on.

The loop around the code

  • contribute_improvements syncs, files blockers, and opens or reviews PRs. Forks track upstream. The main repo reviews useful PRs from any author.
  • The CI security gate blocks data/, credentials, and secrets. The syntax check skips files a PR deleted, so a deletion does not fail the build.
  • Commits from the evolver are authored as the GitHub username, not a personal name or a personal email.

Since v3.2.0

Indeed Apply now works in the normal signed-in window, with the debug port closed. Three in-site applications reached the page that says the application was submitted. The jobs home is https://pk.indeed.com/. A from= tracking parameter is not part of that page. Typing viewjob?jk= into the address bar stops on Additional Verification. Leave that page. Do not click the check. Glassdoor job pages load after Google sign-in, and in-site Apply now there has still not been submitted. Issue #1 is closed. Issue #2 stays open for a driven Glassdoor browser.

Path traces

Every agent reads docs/paths/ before it drives a site, then writes what happened. AGENTS.md makes this mandatory for every brain. One directory per site. One row is one hop: the URL, the option used, the next page, and the result.

flowchart LR
  read["Read docs/paths/site/path.md"] --> go["Follow a success hop"]
  go --> page["A new page opens"]
  page --> row["Add one row"]
  row --> next["The next agent starts from that row"]

Follow success and reached. Do not follow stop or refused. Correct a row that a later run proves wrong. No personal data and no screenshots. classifyBoardPage() in src/mcp/board-pages.js names the same actions. Indeed and Glassdoor each have a log. A new site gets its own directory on the first hop. The dashboard Paths tab draws every hop from those files.


Still open

These are real walls. JobPilot records them and routes around them. It does not pretend to slip past a bot check.

  • Glassdoor — a driven browser stops on “Humans only” (#2). A normal window plus Google sign-in works. Job pages there show Apply on employer site. In-site Apply now has not been submitted.
  • Workday, iCIMS, Taleo, SuccessFactors — account-gated, multi-step ATS (#3)
  • Some job-board redirects — Arbeitnow, We Work Remotely, Himalayas (#4)
  • hCaptcha on Lever — the submit button is found. A captcha challenge still needs a person.

Questions, asked once

JobPilot does not invent salary, visa, or notice period.

flowchart LR
  q["Unknown question"] --> park["Questions tab"]
  park --> you["You answer once"]
  you --> keep["Stored answer"]
  keep --> reuse["Reused on the next form\nfuzzy match included"]
  profile["Profile + CV"] --> auto["Agent-answered row"]
  auto --> review["You can override it"]

Verify the path with node scripts/verify-loop.mjs.

If LinkedIn or Gmail is logged out, the dashboard shows one login link and that site waits. It does not retry in a loop.

Glassdoor, then Indeed

Sign in to these two with the worker stopped and the debug window closed. The dashboard button Open Glassdoor for sign-in opens the JobPilot profile as a normal Chrome window.

  1. On Glassdoor, choose Continue with Google.
  2. Then sign in to Indeed with that same Google account.

Opening Indeed first sticks on a check with no form. JobPilot does not solve that check. The notes for the next agent are in docs/board-login.md.

Keep the debug port closed after sign-in. Indeed Apply now is the in-site form in that same window. The hop log is docs/paths/indeed/path.md. Applications reached “Your application has been submitted”. A captcha challenge still stops the apply. The debug-port path is unchanged. Issue #1 is closed. Issue #2 stays open. Glassdoor’s log is docs/paths/glassdoor/path.md.


Start

1. Clone

git clone https://github.com/maazbin/jobpilot
cd jobpilot

2. Open the folder in your agent

Say:

Find relevant jobs, write to the people who are hiring, and keep the evolution loop moving.

The repo already contains the MCP config. start.js installs dependencies, opens Chrome on the debug port, and serves the tools. Approve the server if the CLI asks.

Set JOBPILOT_NO_AUTOWORKER=1 on the MCP server so it stays a tool server and does not start a second worker.

Agent Config in the repo You
Claude Code .mcp.json Open the folder, approve the server
Cursor .cursor/mcp.json Enable it in Settings, MCP
Kiro .kiro/settings/mcp.json Open the folder
VS Code / Copilot .vscode/mcp.json Start the server
Gemini CLI .gemini/settings.json Run gemini from the folder
Codex ~/.codex/config.toml Add the jobpilot server there. Do not repoint CODEX_HOME.
Grok .grok/config.toml grok mcp add jobpilot --scope project node -- start.js

3. Let the brain drive

AGENTS.md is the standing guide. The brain checks onboard_status, opens http://127.0.0.1:7788/onboard if the profile has gaps, and then searches, judges, and acts. You upload the CV in that form. You never paste a file path into chat.

Keep the hands alive in another terminal:

npm install
npm run agent

That process only scrapes and refreshes the dashboard. Stop it with the dashboard button or npm run stop.

npm run worker still contains the older action lanes. The supported setup is npm run agent plus your own CLI as the brain, so outreach is never double-sent.

CV variants

The canonical CV is the fallback. Optional variants stay in the private profile:

{
  "preferences": {
    "cvVariants": [
      { "id": "aws", "label": "AWS CV", "path": "/private/path/aws.pdf", "focuses": ["aws"], "keywords": ["eks", "s3"] }
    ]
  }
}

JobPilot does not invent skills or employers to fill a gap.


Dashboard

Local only: http://127.0.0.1:7788

  • Pipeline — fresh leads, newest first, with per-lane status.
  • Questions — one answer, remembered. Agent-answered rows can be overridden.
  • Evolution — open encounters, screenshot, HTML snapshot, learned answers.
  • Paths — every hop in docs/paths/, one flow per site. Green is success, blue was reached, red is a stop or a refusal.
  • Audit — emails sent, applications, connects. Dedup warnings included.

The UI is split under src/dashboard/ so one section can change without a rewrite of the page.


Layout

jobpilot/
├── start.js                 # MCP entry: install, Chrome, respawn
├── mcp-server.js            # tool server + evolution loop
├── AGENTS.md                # the only brain guide; path traces are required
├── docs/paths/              # one hop log per site; read it, then update it
├── src/
│   ├── worker.js            # scout and dashboard; outreach only if not data-only
│   ├── evolve.js            # encounters, answers, strategy
│   ├── evolve-loop.js       # evolution while the worker is down
│   ├── contribute.js        # sync, PR, review — never commits data/
│   ├── tracker.js           # jobs.db, dedup, answer merge across processes
│   ├── fake-emails.txt      # known-fake addresses, one per line
│   ├── dashboard/           # local UI
│   └── mcp/                 # gmail, easy apply, forms, connect, answers, sources
├── scripts/agent-forever.mjs # never-stop supervisor for the data-only worker
└── data/                    # profile, CV, DB — gitignored

Privacy

  • Profile, CV, jobs, answers, and the Chrome profile stay in data/.
  • Contribution refuses to stage data/, .env, drafts, logs, screenshots, or the database. CI checks the same rule.
  • Dedup is permanent. Easy Apply stops before the account limit.

Contributing

See CONTRIBUTING.md. A useful change is a new ATS path, a new public source, or a fix for an encounter in data/evolution/PLAYBOOK.md, followed by contribute_improvements.

License

MIT © maazbin and JobPilot contributors.

View this README on GitHub

Install

This server does not publish a one-line install command.

Open the repository installation guide

Configuration

{ "mcpServers": { "jobpilot": { "command": "node", "args": ["ABSOLUTE/PATH/TO/start.js"], "env": { "JOBPILOT_NO_AUTOWORKER": "1" } } } }