Jobhub
My whole job hunt, automated.
- Status
- Running on my server
- Tracking
- 190+ job boards
- Tests
- 340+ Python · 170+ JS
- Stack
- Python · Flask · React · TS · Docker
- 190+ job boardsGreenhouse, Lever, Ashby, Workday
- SQLiteevery posting, deduped and scored
- Claude Code agentsandboxed worker; bad runs can't touch good output
- LaTeXtailored resume + cover letter
- Firefox extensionfills the form, cancellable countdown, then submits
The problem
Applying to one co-op job was three chores: paste the listing into a form, SSH into my server to run the tailoring by hand, then download the resume and cover letter and upload them one by one. Multiply that by every posting worth applying to and most of my time went to the busywork, not the applications.
I wanted to tap one button on my phone and come back to something ready to send.
What I built
Three pieces that run together in Docker on my home server, reachable only over Tailscale:
- Jobwatch polls 190+ company job boards every 30 minutes and keeps every posting in SQLite, about 16,000 so far.
- Jobhub is the dashboard, a Flask API and a React app I use from my phone or laptop. I filter postings, queue the ones I want in batches, and track where each application stands.
- A worker runs Claude Code headless, one job at a time. For each posting it rates my fit out of 10, tailors a LaTeX resume and cover letter, and writes down what it changed.
A Firefox extension finishes the job. It uploads the PDFs, fills the application and submits it.
Keeping an agent honest
An agent that writes in my name and applies for me has to fail safely, so most of the work went into what happens when it’s wrong.
- It can’t damage the source material. Tool deny rules protect the master resume, the prompts and the tracker.
- A bad run can’t cost me a good one. The worker has timeouts and kills the whole process group, and it backs up PDFs before a revision and restores them if the run fails.
- It asks instead of guessing. Anything it can’t confirm from my resume becomes a question I answer on my phone. A job with open questions waits, and answering them queues a revised run.
- Low-fit jobs stop there. They’re skipped and never applied to.
- It can’t apply twice. Before submitting, it re-checks the posting and records its ID, and a posting already recorded is never submitted again. If the page doesn’t respond, it tells me to check instead of retrying.
- I can always stop it. A 5-second countdown with a Cancel button runs before every submit, and leaving the page cancels it too.
One decision I changed
The first design stopped before the final click, so I’d always press submit myself. A few days in, I changed my mind and let it submit once everything else was settled. The safeguards above are what made that comfortable, and the countdown is there because I still wanted a last chance to stop it.
Where it is now
It runs on my server every day. It’s covered by 340+ Python tests and 170+ JavaScript tests.