Skip to content

code-zm/jobhunter

Repository files navigation

JobHunter

Self-hosted, single-user LinkedIn job-search automation: scan, score, and get notified on Telegram. Runs on Linux (systemd) or macOS (launchd). You run your own instance; there is no shared service. Drop in your resume, run /job-setup, and the pipeline is ready.

A scheduled timer scans LinkedIn every 4 hours. Claude scores each new posting 0-100 against your resume and preferences. Matches above the threshold arrive as informational Telegram cards with the posting link — you open the link and apply on the company's site yourself. JobHunter is discovery-only: it never applies for you, never uses LinkedIn Easy Apply, and never uploads your resume anywhere. Python 3.14 stdlib only, bash, the claude CLI, and (for login) the Playwright MCP server. Zero pip dependencies.

docs/SPEC.md is the binding contract for every schema, CLI, and status in this repo.

Architecture

scan timer  (systemd --user / launchd, every 4h, plus a random 0-20 min delay)
        |
        v
scripts/scan.sh
        |
        v
scripts/harvest.mjs ------> Chromium (headless), persistent profile,
        |                   exclusive lock on data/browser.lock
        v
data/harvest.json
        |
        v
claude -p "/job-scan" ----> scores each posting 0-100 (no browser)
        |
        v
data/jobs.json  <---------- ALL reads/writes via scripts/jobstore.py
        |  score >= MIN_SCORE
        v
scripts/notify.py --------> Telegram card with the 🔗 posting link
                                  |
                                  v
                       you open the link and apply on the company site yourself

JobHunter is discovery-only. It scans LinkedIn, scores postings against your resume and preferences, and notifies you. It never applies for you, never uses LinkedIn Easy Apply, and never uploads your resume — LinkedIn is for discovery only. Applying is yours: open the card's link and apply on the company's site. You can optionally track those applications in the AppTracker webapp (see apptracker/). The full state machine is in docs/SPEC.md.

Setup

Follow this order. Steps depend on the previous ones.

  1. Create the Telegram bot. Message @BotFather, send /newbot, follow the prompts, and save the bot token it gives you.

  2. Run the installer. (Linux or macOS.)

    ./scripts/install.sh

    This creates .env from .env.example if missing, autodetects your Chromium/Chrome binary, installs the service units (systemd user units on Linux, launchd agents on macOS), and verifies the Telegram setup once credentials are filled in. Safe to re-run anytime.

  3. Fill .env. Put the token from step 1 into TELEGRAM_BOT_TOKEN. Then send your new bot any message on Telegram. Bots cannot message you until you message them first. BotFather's privacy mode setting does not matter here — it only affects group chats; for the direct-message chat this project uses, the only requirement is that you open the chat and press Start (send any message) before whoami can see it. After that:

    python3 scripts/notify.py whoami

    Copy the printed chat id into TELEGRAM_CHAT_ID. (The token is used only to send notifications — there is no bot daemon listening for messages.)

    Key Default Meaning
    TELEGRAM_BOT_TOKEN From @BotFather
    TELEGRAM_CHAT_ID From notify.py whoami
    MIN_SCORE 70 Minimum score, 0-100, for a job to be sent to Telegram
    MAX_NOTIFY_PER_SCAN 10 Card cap per scan run
    CLAUDE_BIN claude Claude CLI binary
    CHROMIUM_PATH autodetected Chromium/Chrome executable the browser drives (install.sh fills it)
    BROWSER_PROFILE_DIR ~/.jobhunter-browser Persistent browser profile holding the LinkedIn session

    .env is gitignored. The token lives nowhere else.

  4. Drop in your resume and onboard. Put your resume in profile/ as resume.md (preferred, plain markdown — scoring reads it) and/or resume.pdf. Then run an interactive Claude session and invoke the onboarding skill:

    claude "/job-setup"

    It reads your resume, asks a few questions (target roles, locations, seniority, salary floor, …), and generates profile/preferences.yaml (search queries, locations, filters, scoring notes). Review and edit it afterward.

  5. Log in to LinkedIn once.

    ./scripts/login.sh

    This opens a visible browser using the persistent profile at ~/.jobhunter-browser. Log in manually and complete any 2FA. Do not close the window — the script detects your feed within ~5 minutes, prints LOGIN OK, and closes the browser itself. All later headless runs reuse that session.

  6. Test a scan.

    ./scripts/scan.sh

    Watch Telegram. You should get job cards for anything scoring at or above MIN_SCORE, then a one-line summary. The first run can take a while.

  7. Enable scheduled scans.

    Linux (systemd):

    systemctl --user enable --now jobhunter-scan.timer

    This is a systemd user timer: without lingering it stops the moment you log out (scans only catch up at your next login). Enable lingering once:

    loginctl enable-linger "$USER"

    scripts/install.sh checks this and warns if lingering is off.

    macOS (launchd):

    launchctl load -w ~/Library/LaunchAgents/com.jobhunter.scan.plist

    launchd agents run while you are logged in; for 24/7 operation keep the Mac logged in (or convert the agents to LaunchDaemons).

Daily use

Matches arrive as Telegram cards; you apply yourself via the link. Everything else is driven from the command line — Telegram is outbound-only (there is no bot listening for commands).

The job card

Each match arrives as an informational card (no buttons):

🎯 87/100 — Security Analyst
🏢 Acme Corp — Omaha, NE (hybrid)
💰 $70k–$85k
📝 Strong SOC/SIEM and detection engineering overlap; entry-level friendly.
🔗 https://www.linkedin.com/jobs/view/4012345678/

Open the 🔗 link and apply on the company's site yourself. Optionally record the application in the AppTracker webapp (apptracker/) to track its lifecycle.

Control (from a terminal)

Action Command
Scan now ./scripts/scan.sh
Job counts per status python3 scripts/jobstore.py stats
Pause all runs touch data/paused
Resume rm data/paused

Files that matter

Path What it is Touch it?
profile/resume.md Source of truth for scoring; you drop it in Yes, keep current
profile/resume.pdf Optional PDF resume; an input to /job-setup if no resume.md Optional
profile/preferences.yaml Search queries, locations, filters, scoring notes Generated by /job-setup; edit freely
.env Token, chat id, caps, browser paths. Gitignored Yes
data/jobs.json The job store and state machine Only via scripts/jobstore.py
data/paused Flag file; if present, runs exit immediately touch/rm it
data/browser.lock lock target serializing browser access No
data/logs/ scan-*.log Read when debugging
$BROWSER_PROFILE_DIR (default ~/.jobhunter-browser) Persistent Chromium profile holding the LinkedIn session No

All durable state is in data/ plus the browser profile. Back those up and you can rebuild the rest from the repo.

Troubleshooting

Telegram says the session expired or was challenged. The skills detect login walls, checkpoints, and authwall redirects, alert you, and stop the run. Fix:

./scripts/login.sh

Headless runs keep getting challenged. LinkedIn challenges headless browsers more often. Run ./scripts/scan.sh --watch to harvest with a visible browser. Caveat: a headful browser launched by a systemd user unit needs your session's display environment imported first:

systemctl --user import-environment WAYLAND_DISPLAY DISPLAY

or simpler, run scans from a terminal while you stay headful.

A run appears stuck, or you started two at once. The harvester takes an exclusive lock on data/browser.lock for the whole run, since there is only one browser profile. A second run waits up to 15 minutes for the lock, then gives up. This is by design; just let it finish.

Scans stop running after you log out. The systemd user timer only runs while you have a session unless lingering is enabled. Check with loginctl show-user "$USER" --property=Linger and fix with loginctl enable-linger "$USER".

Where to look. Run logs are in data/logs/ as scan-*.log, newest timestamp last. For the scheduled runs, the systemd journal also has them:

journalctl --user -u jobhunter-scan -f

Use responsibly

Automated activity on LinkedIn violates their User Agreement. Accounts doing it can be restricted or banned, and detection is plausible regardless of how careful the tooling is. This project mitigates, not eliminates: it only reads LinkedIn (discovery — never Easy Apply, never an account, never a resume upload), the harvest is capped and paced 2-4 seconds between page loads, and it drives your own authenticated session at human speed. The risk remains yours. Keep MAX_NOTIFY_PER_SCAN conservative, keep the scan window modest, and do not turn discovery into a scraper.


Docs last updated 2026-06-23. Interfaces are versioned by docs/SPEC.md.

About

claude, find me a job, make no mistakes

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors