Job Hunter Skill
Automates your job search: scrapes LinkedIn, Indeed, and 100+ job boards daily, scores every posting against your resume, and saves a ranked tracker to Obsidian. No LinkedIn account needed.
Quick Start
First-Time Setup
Set up virtual environment:
bash ~/.claude/skills/job-hunter/scripts/setup_venv.shConfigure preferences:
~/.venv/job-hunter/bin/python3 ~/.claude/skills/job-hunter/scripts/setup_config.pySet your resume path (PDF, DOCX, or TXT)
Set Obsidian vault path
Usage
Run the job hunt:
/job-hunt
Or with specific options:
/job-hunt --keywords "software engineer" --location "San Francisco"
How It Works
Two complementary providers run on a smart schedule:
| Provider | Source | Cost | Schedule |
|---|---|---|---|
| JobSpy | LinkedIn guest API + Indeed | Free, no key | Every day |
| JSearch | Google Jobs aggregator (100+ boards) | 200 req/month free | Every 2 days |
On days both run, results are merged and deduplicated. On off-days, JobSpy runs alone.
Workflow
When invoked, the skill executes these steps:
1. Load Configuration
- Read config from
~/.config/job-hunter/config.json - Validate paths and optional API keys
2. Read Resume
- Load resume from configured path
- Extract key skills for matching
3. Search Jobs — JobSpy (daily) + JSearch (every 2 days)
JobSpy hits LinkedIn's undocumented public guest API — no login, no API key, returns full job descriptions (~60 results/run).
JSearch queries Google Jobs via RapidAPI, aggregating postings from LinkedIn, Indeed, Dice, Remotive, and 100+ company career pages (~100 results/run).
The scheduler uses a state file (~/.job-hunter-jsearch-last-run) to decide whether JSearch runs. If both run, their outputs are merged and deduplicated:
JobSpy ~60 jobs ─┐
├─ deduplicate → ~130–160 unique jobs
JSearch ~100 jobs─┘
4. Score against resume
Each job is scored 0–100 based on skill keyword matching:
- +5 per skill match (Python, SQL, machine learning, etc.)
- Bonus: role alignment (+15), healthcare domain (+10), EHR tools (+8), remote (+5)
- Skills are extracted automatically from your resume (PDF/DOCX/TXT)
4b. AI Re-rank (interactive runs only)
Keyword scoring is literal — it can't distinguish "we use Python for Excel macros" from "ML platform team". When the skill runs interactively (not via cron), Claude re-ranks the top jobs by actually reading them:
- Prep — build a compact packet of the top ~30 keyword-scored jobs + resume:
~/.venv/job-hunter/bin/python3 ~/.claude/skills/job-hunter/scripts/rerank.py prep \ -i /tmp/scored.json -o /tmp/rerank_packet.md --top 30 - Judge — Claude reads
/tmp/rerank_packet.mdand scores every job 0–100 for holistic fit (seniority, domain, day-to-day work vs. resume — not keyword counts). Claude writes the result to/tmp/rerank_scores.jsonas:
Every job id from the packet must appear. Keep reasons under 80 chars.{"<job id>": {"ai_score": 88, "ai_reason": "Strong healthcare + Python overlap"}} - Apply — merge scores back and re-sort:
~/.venv/job-hunter/bin/python3 ~/.claude/skills/job-hunter/scripts/rerank.py apply \ -i /tmp/scored.json -r /tmp/rerank_scores.json
Each job gains final_score (AI score when present, keyword score otherwise) and the tracker sorts/filters by it, adding a Why column. Cron runs skip this step automatically — write_tracker.py falls back to keyword scores.
5. Save Job Tracker to Obsidian
Writes Job Tracker - YYYY-MM-DD.md with all jobs scoring > 70, sorted by score descending. Idempotent — skips if today's file already exists.
| # | Score | Title | Company | Salary | Remote | Source | Link |
|---|-------|-------|---------|--------|--------|--------|------|
| 1 | 100 | Data Engineer II | Dana-Farber Cancer Institute | - | Yes | Indeed | [Apply](url) |
| 2 | 100 | Senior Health Data Informaticist | Veeva Systems | - | Yes | Linkedin | [Apply](url) |
6. Wait for User Selection — STOP HERE
After saving the tracker:
"Saved X jobs to Job Tracker - YYYY-MM-DD.md in Obsidian. Review the file and tell me which numbers you'd like cover letters for (e.g. '1, 3')."
Never auto-generate cover letters. Always wait for user to select.
7. Generate Cover Letters
~/.venv/job-hunter/bin/python3 ~/.claude/skills/job-hunter/scripts/generate_cover_letters.py \
--jobs /path/to/scored_jobs.json \
--indices "1,3"
Saves to Cover Letters/YYYY-MM-DD/Company - Title.md in your Obsidian vault.
Configuration Reference
Config file: ~/.config/job-hunter/config.json
{
"jsearch_api_key": "your-rapidapi-key",
"search_provider": "jobspy",
"resume_path": "/path/to/resume.pdf",
"obsidian_vault": "~/Documents/Obsidian Vault/job-hunter",
"user_name": "Your Name",
"min_score": 70,
"search": {
"keywords": ["data analyst", "healthcare data analyst", "research analyst"],
"location": "Boston, MA",
"remote": true,
"time_range": "week",
"max_results_per_query": 20,
"job_domains": ["linkedin.com", "indeed.com"],
"linkedin_fetch_description": true,
"request_delay": 3,
"proxies": [],
"user_agent": "",
"country_indeed": "USA"
}
}
Key Fields
| Field | Description |
|---|---|
jsearch_api_key |
RapidAPI key for JSearch (free tier: 200 req/month) |
search_provider |
"jobspy" (default) or "jsearch" for manual override |
min_score |
Tracker only includes jobs scoring above this (default 70) |
search.max_results_per_query |
Max 20 for JobSpy, max 10 for JSearch |
search.job_domains |
Used by JobSpy; maps to linkedin, indeed, glassdoor, zip_recruiter. Glassdoor is off by default (upstream 400 errors) |
search.linkedin_fetch_description |
Full LinkedIn descriptions (1 extra request/job). Set false if you hit 429 rate limits |
search.request_delay |
Seconds between JobSpy calls (default 3) — reduces rate-limiting |
search.proxies |
Proxy list ["user:pass@host:port"] — JobSpy round-robins through them; the reliable fix for persistent LinkedIn 429s |
search.user_agent |
Override JobSpy's default browser user-agent if it gets blocked |
search.country_indeed |
Indeed/Glassdoor country (default "USA") |
Scripts
| Script | Purpose |
|---|---|
scripts/jobspy_scraper.py |
LinkedIn/Indeed via guest API (no key) |
scripts/jsearch_scraper.py |
Google Jobs via RapidAPI |
scripts/run_search.py |
Provider-switching search CLI |
scripts/score_jobs.py |
Score jobs against resume (keyword-based) |
scripts/rerank.py |
Prep/apply AI re-ranking by Claude (interactive runs) |
scripts/generate_cover_letters.py |
Generate cover letter templates |
scripts/write_tracker.py |
Write ranked Job Tracker markdown to Obsidian |
scripts/daily_job_hunt.sh |
Full pipeline orchestration (cron) |
scripts/setup_config.py |
Interactive config setup |
scripts/setup_venv.sh |
Virtual environment setup |
scripts/common/ |
Shared modules (config, scoring, dedup) |
Obsidian Structure
job-hunter/
├── Job Tracker - 2026-02-28.md # Today's tracker
├── Job Tracker - 2026-02-27.md # Yesterday's tracker
├── Cover Letters/
│ ├── 2026-02-28/
│ │ ├── Dana-Farber - Data Engineer II.md
│ │ └── Veeva Systems - Senior Health Data Informaticist.md
│ └── 2026-02-27/
Scheduling
Set up a daily cron to run the full pipeline automatically:
# Run job hunt daily at 10am
0 10 * * * bash ~/.claude/skills/job-hunter/scripts/daily_job_hunt.sh
The script decides internally whether to also run JSearch (every 2 days). To force a JSearch run:
rm ~/.job-hunter-jsearch-last-run
API Usage & Cost
| Provider | Free Tier | Usage per run | Monthly (daily cron) |
|---|---|---|---|
| JobSpy | Unlimited | 0 requests | $0 |
| JSearch | 200 req/month | 7–14 requests | ~98 req/month ✓ |
Troubleshooting
No jobs found
- Check
~/.job-hunter.log— the run now prints per-site totals (Per-site totals: linkedin=0, indeed=25) so you can see which board failed - Broaden search keywords in config
- Each site is scraped independently, so one board failing no longer wipes out the others' results
LinkedIn returns 0 results / 429 errors
- LinkedIn aggressively rate-limits (~100 results per IP, then blocks). In config:
- Set
search.linkedin_fetch_description: false(biggest win — cuts requests by ~95%) - Lower
search.max_results_per_query - Increase
search.request_delay(e.g. 10) - Add
search.proxies— per JobSpy maintainers, proxies are the only durable fix
- Set
- Indeed has no rate limiting and full descriptions — results still flow while LinkedIn cools down (blocks typically clear within hours)
JSearch API errors
- Verify key at rapidapi.com
- Check monthly usage (200 free requests)
- Delete
~/.job-hunter-jsearch-last-runto retry
Glassdoor 400 errors in logs
- Known upstream issue in JobSpy — Glassdoor is now excluded from default sites; remove
glassdoor.comfromjob_domainsif you added it
Virtual environment issues
- Re-run
bash ~/.claude/skills/job-hunter/scripts/setup_venv.sh - Requires Python 3.10+