| name | job-hunter |
| description | Automated job hunting skill that searches jobs via JobSpy (LinkedIn guest API, free)
and JSearch (RapidAPI/Google Jobs, 200 req/month free), matches them to your resume
using scoring, generates personalized cover letters, and saves results to Obsidian.
Triggers: /job-hunt, "find jobs", "job search", "hunt for jobs"
|
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.sh
-
Configure preferences:
~/.venv/job-hunter/bin/python3 ~/.claude/skills/job-hunter/scripts/setup_config.py
-
Set 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.md and 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.json as:
{"<job id>": {"ai_score": 88, "ai_reason": "Strong healthcare + Python overlap"}}
Every job id from the packet must appear. Keep reasons under 80 chars.
- 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:
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
- 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-run to retry
Glassdoor 400 errors in logs
- Known upstream issue in JobSpy — Glassdoor is now excluded from default sites; remove
glassdoor.com from job_domains if you added it
Virtual environment issues
- Re-run
bash ~/.claude/skills/job-hunter/scripts/setup_venv.sh
- Requires Python 3.10+