Autonomous job discovery platform that finds, filters, and scores open positions — so you spend time applying, not searching.
JobHunter monitors company career pages directly, filters out irrelevant roles, and scores every job against your skill profile. Open the dashboard each morning and you'll find a ranked list of opportunities worth your time — no scrolling through LinkedIn, no missed postings.
- Configure once — describe your skills, target locations, and role preferences in
profile.yaml - It runs continuously — jobs are crawled from company career pages and aggregators, filtered, scored, and persisted
- Check the dashboard — Daily Digest shows new opportunities ranked by fit; one click opens the application
- Track progress — mark jobs applied, see your pipeline at a glance
- Docker and Docker Compose
- An AI API key (Anthropic or Google Gemini) — used for AI-powered features and some scrapers
git clone https://github.com/sdeonvacation/jobhunter.git
cd jobhunterEdit profile.yaml at the project root. At minimum, set your identity, role filters, and location preferences. See Configuration for the full reference and role examples.
export JOBHUNTER_AI_API_KEY=your-api-key
export JOBHUNTER_AI_PROVIDER=anthropic # or "openai" for Gemini/OpenAI-compatibledocker compose up -dThis launches:
- PostgreSQL on port 5435
- LinkedIn MCP on port 8000 (requires first-time auth — see LinkedIn Setup)
- API on http://localhost:8081
- Dashboard on http://localhost:3003
curl -X POST http://localhost:8081/api/admin/crawl
curl -X POST http://localhost:8081/api/admin/scoreOpen http://localhost:3003 — your Daily Digest is ready.
Jobs are discovered from two sources:
1. Company career pages (primary) — direct scraping from company ATS systems. Add a company via the dashboard or API:
curl -X POST http://localhost:8081/api/companies \
-H "Content-Type: application/json" \
-d '{"name": "Acme Corp", "careersUrl": "https://acme.com/careers"}'JobHunter auto-detects the ATS platform (Greenhouse, Lever, Ashby, Workday, and 12+ more) and sets up crawling.
2. Aggregators (supplemental) — LinkedIn, Indeed, BerlinStartupJobs, Arbeitnow. Configured in application.yaml under aggregator.sources. Run on every crawl cycle.
LinkedIn job search requires a one-time browser authentication. The linkedin-mcp container handles this automatically using a headless browser — you just need to provide credentials.
# Set credentials before starting
export LINKEDIN_EMAIL=you@example.com
export LINKEDIN_PASSWORD=yourpassword
docker compose up -d linkedin-mcpOn first start, the container authenticates and saves a session to ~/.linkedin-mcp. Subsequent starts reuse it.
If authentication fails, restart the container — it will re-authenticate automatically.
Note: LinkedIn scraping is for personal use only. Respect LinkedIn's Terms of Service.
The MCP server exposes JobHunter to any AI assistant that supports the Model Context Protocol (Claude, OpenCode, Cursor, etc.).
npm install -g jobhunter-mcpOr run without installing:
npx jobhunter-mcpClaude Code (.claude/settings.json or project .mcp.json):
{
"mcpServers": {
"jobhunter": {
"command": "npx",
"args": ["jobhunter-mcp"],
"env": { "JOBHUNTER_API_URL": "http://localhost:8081" }
}
}
}OpenCode (opencode.json):
{
"mcp": {
"jobhunter": {
"type": "local",
"command": ["npx", "jobhunter-mcp"],
"environment": { "JOBHUNTER_API_URL": "http://localhost:8081" },
"enabled": true
}
}
}| Tool | Description |
|---|---|
get_top_jobs |
Best overall opportunities ranked by match score |
get_top_jobs_today |
Today's new jobs sorted by match score |
get_jobs |
Search by skill or keyword |
get_job_description |
Full job description text |
get_job_keywords |
Tech keywords extracted from a job (for resume tailoring) |
mark_job_applied |
Mark a job as applied |
add_company |
Register a new company career page |
find_contacts |
Find recruiters/hiring managers at a company |
connect_with |
Send a LinkedIn connection request |
Jobs are identified by UUID or 8-character short ID (shown on the dashboard). Tools also accept a full job URL for get_job_keywords.
get_top_jobs(5)
→ 1. [a3f2c8d1] Senior Backend Engineer @ Acme Corp | Berlin | APPLY (match: 85)
2. [b7e4f9a2] Platform Engineer @ BigCo | Remote | APPLY (match: 78)
get_job_keywords("a3f2c8d1")
→ Java, Spring Boot, Kubernetes, Kafka, AWS, PostgreSQL, microservices, ...
mark_job_applied("a3f2c8d1")
→ Marked as applied.
All behavior is controlled by two YAML files at the project root. No code changes needed.
Defines who you are, what roles you want, and how jobs are scored.
Your identity (used for cover letters and AI tailoring):
name: Sam
title: Backend Engineer
years-of-experience: 4Role filter — whitelist of title patterns to keep, then blacklist to reject:
filters:
role:
include-patterns:
- "developer"
- "engineer"
- "backend"
- "back[\\s-]end"
- "\\bjava\\b" # \\b = word boundary, prevents matching "javascript"
- "fullstack"
exclude-keywords:
- "manager"
- "director"
- "intern"
- "devops"Patterns are case-insensitive regex. A job title must match at least one include-pattern AND zero exclude-keywords to be kept.
Location and experience:
location:
target-cities:
- "berlin"
- "munich"
- "remote"
remote-patterns:
- "remote"
- "remote.*germany"
language:
target: "en"
exclude-patterns:
- "german\\s+c[12]" # exclude roles requiring C1/C2 German
- "fluent\\s+german"
yoe:
max-years: 5Restart the API after editing.
scoring:
primary-skills: ["java", "spring boot", "kotlin"]
primary-skill-cap: 70 # max match score if none of these appear in the JD
skill-weights:
java: 5.0
spring boot: 4.5
kubernetes: 3.0
aws: 2.5
thresholds:
apply-match: 60
maybe-match: 40primary-skills act as a gate — if none appear in a job description, the match score is capped. This prevents peripheral tools from inflating the score on clearly unrelated roles.
Backend Engineer (Java/Spring)
name: Sam
title: Backend Engineer
years-of-experience: 4
skills:
- name: Java
proficiency: expert
- name: Spring Boot
proficiency: expert
- name: Kubernetes
proficiency: advanced
filters:
role:
include-patterns:
- "\\bjava\\b"
- "developer"
- "software"
- "backend"
- "back[\\s-]end"
- "fullstack"
- "full[\\s-]stack"
- "engineer"
exclude-keywords:
- "manager"
- "frontend"
- "designer"
- "devops"
- "intern"
location:
target-cities: ["berlin", "munich", "hamburg", "remote"]
yoe:
max-years: 5
scoring:
primary-skills: ["java", "spring boot"]
primary-skill-cap: 70
skill-weights:
java: 5.0
spring boot: 4.5
kubernetes: 3.0Product Designer
name: Alex
title: Product Designer
years-of-experience: 3
skills:
- name: Figma
proficiency: expert
- name: User Research
proficiency: advanced
- name: Design Systems
proficiency: advanced
filters:
role:
include-patterns:
- "designer"
- "\\bux\\b"
- "\\bui\\b"
- "product\\s+design"
- "interaction\\s+design"
- "visual\\s+design"
exclude-keywords:
- "engineer"
- "developer"
- "manager"
- "intern"
location:
target-cities: ["berlin", "munich", "remote"]
yoe:
max-years: 5
scoring:
primary-skills: ["figma", "user research"]
primary-skill-cap: 70
skill-weights:
figma: 5.0
user research: 4.5
design systems: 4.0Data Scientist (Python/ML)
name: Jordan
title: Data Scientist
years-of-experience: 3
skills:
- name: Python
proficiency: expert
- name: PyTorch
proficiency: advanced
- name: SQL
proficiency: expert
filters:
role:
include-patterns:
- "data\\s+scientist"
- "machine\\s+learning"
- "\\bml\\b"
- "\\bai\\b"
- "deep\\s+learning"
- "research\\s+engineer"
exclude-keywords:
- "manager"
- "intern"
- "analyst"
location:
target-cities: ["san francisco", "new york", "remote"]
remote-patterns: ["remote", "remote.*us"]
language:
target: "en"
yoe:
max-years: 5
scoring:
primary-skills: ["python", "pytorch", "machine learning"]
primary-skill-cap: 70
skill-weights:
python: 5.0
pytorch: 4.5
sql: 3.0DevOps / Platform Engineer
name: Riley
title: Platform Engineer
years-of-experience: 5
skills:
- name: Kubernetes
proficiency: expert
- name: Terraform
proficiency: expert
- name: AWS
proficiency: advanced
filters:
role:
include-patterns:
- "devops"
- "dev\\s*ops"
- "platform"
- "infrastructure"
- "\\bsre\\b"
- "site\\s+reliability"
- "kubernetes"
exclude-keywords:
- "manager"
- "intern"
- "frontend"
location:
target-cities: ["london", "amsterdam", "remote"]
remote-patterns: ["remote", "remote.*eu"]
yoe:
max-years: 7
scoring:
primary-skills: ["kubernetes", "terraform", "aws"]
primary-skill-cap: 70
skill-weights:
kubernetes: 5.0
terraform: 4.5
aws: 4.0| Section | What it controls |
|---|---|
name, title, years-of-experience |
Identity for cover letters and AI tailoring |
skills[] |
Each skill: name, proficiency (expert/advanced/intermediate), category |
preferences |
Salary expectations, seniority, employment type |
filters.role.include-patterns |
Whitelist: title must match at least one (regex) |
filters.role.exclude-keywords |
Blacklist: title matching any of these is rejected |
filters.location.target-cities |
Allowed city names (case-insensitive substring) |
filters.location.remote-patterns |
Regex patterns that identify remote positions |
filters.language.target |
ISO language code ("en", "de", etc.) |
filters.language.exclude-patterns |
Reject jobs requiring a language level (e.g. C2 German) |
filters.yoe.max-years |
Skip jobs requiring more than N years of experience |
scoring.skill-weights |
Per-skill weight for match scoring |
scoring.skill-variants |
Regex patterns for recognizing each skill in JDs |
scoring.primary-skills |
Core skills — score capped at primary-skill-cap if none match |
scoring.thresholds |
Score cutoffs for APPLY / MAYBE / SKIP recommendations |
Controls what the get_job_keywords MCP tool extracts from job descriptions. Each category contains regex patterns matched against JD text.
Customize this with your own skill set. The default ships with software engineering terms, but any profession works:
design_tools:
- Figma
- Sketch
- Adobe\s*XD
methodologies:
- design\s*thinking
- user\s*research
- usability\s*test\w*
- A\/B\s*test\w*Changes take effect on the next MCP tool invocation — no restart needed.
ATS platforms (direct career page scraping): Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Personio, Recruitee, JOIN, BambooHR, Breezy, SAP SuccessFactors, TeamTailor, iCIMS, Jobvite, Pinpoint
Aggregators: LinkedIn (via MCP scraper), Indeed (via JobSpy), BerlinStartupJobs (AI-parsed), Arbeitnow (REST API)
Useful for manual triggers and monitoring:
curl -X POST http://localhost:8081/api/admin/crawl # crawl all company pages
curl -X POST http://localhost:8081/api/admin/score # score all unscored jobs
curl -X POST http://localhost:8081/api/admin/crawl/aggregators # run aggregators only
curl http://localhost:8081/api/admin/health # service health reportFor contributors or those running services outside Docker.
- Java 21 (Temurin)
- Node.js 18+
- Docker (PostgreSQL)
uvx— for LinkedIn MCP server (pip install uv)
# Set AI provider credentials
export JOBHUNTER_AI_API_KEY=your-key
export JOBHUNTER_AI_PROVIDER=openai # or anthropic
make build # build API JAR (first time only)
make dev # start DB + MCP + API + DashboardServices start on:
- API: http://localhost:8080
- Dashboard: http://localhost:3000
- LinkedIn MCP: http://localhost:8000
| Command | Description |
|---|---|
make dev |
Start all services |
make stop |
Stop app services (DB left running) |
make restart |
Stop and start all services |
make build |
Rebuild the API JAR |
make test |
Run API unit tests |
make logs |
Tail the API log |
make status |
Show which services are running |
# Database only
docker compose up -d db
# API
cd api && ./gradlew bootRun
# Dashboard
cd dashboard && npm install && npm run devjobhunter/
├── api/ # Spring Boot 3 backend (Java 21)
├── dashboard/ # React 18 + Vite + Tailwind dashboard
├── mcp-server/ # TypeScript MCP server
├── scripts/ # Start scripts (start-api.sh, start-dashboard.sh, dev.sh)
├── profile.yaml # Your profile, filters, and scoring config
├── keywords.yaml # Keyword extraction patterns for MCP
└── docker-compose.yml
My jobs aren't being filtered correctly.
Check that your include-patterns cover the role titles you want. Add a \\b word boundary around short terms (\\bjava\\b) to avoid false matches. Restart the API after editing.
A job requiring 8+ years appeared in my results.
The YOE filter runs when descriptions are available. Jobs ingested without a description get descriptions backfilled on the next enrichment cycle, after which the filter re-runs. You can also trigger POST /api/admin/refilter-language to re-evaluate all jobs.
berlinstartupjobs shows "AI provider not available".
This source uses AI to parse the page. Set JOBHUNTER_AI_API_KEY in your environment. If using Docker, the variable must be set before starting the container (or added to a .env file in the project root).
LinkedIn shows EMPTY.
LinkedIn requires the MCP server to be running and authenticated. Check make status and the LinkedIn Setup section above.
How do I add a company?
Use the dashboard "Companies" page, the add_company MCP tool, or the REST API directly. JobHunter auto-detects the ATS from the careers URL.
