Personal LinkedIn job discovery and tracking CLI
Project description
jobhunt
Personal LinkedIn job discovery and tracking CLI. Playwright-based scraper with SQLite storage, multi-profile support, and aggregator filtering.
⚠️ LinkedIn ToS & Rate Limiting
This tool automates a real browser session on LinkedIn. Excessive or frequent use can trigger bot detection, CAPTCHA challenges, or temporary account restrictions.
Recommended usage:
- Run
discoverat most once or twice per day per profile- Avoid running
--all-profilesacross many profiles in rapid succession- If LinkedIn presents a CAPTCHA or checkpoint, stop and wait several hours before retrying
- Using
--headlesswith automation credentials increases detection risk — use sparingly
Features
- Automated discovery — Playwright drives a real Chromium browser, extracts career-site URLs from each listing
- Aggregator filtering — two-pass filter skips job aggregators (Lensa, Dice, Wiraa, etc.) before visiting their pages
- Multi-profile support — separate search preferences for different job-search personas
- Job lifecycle tracking —
new → opened → applied / skipped - Interactive review mode — keyboard-driven triage: open in browser, skip, or move to next
- Excel export — one-command export with configurable output path
- Headless mode — run without a visible browser window using saved encrypted credentials
- Proxy support — route Playwright through an HTTP/SOCKS proxy
Installation
Prerequisites: Python 3.10+, a LinkedIn account.
From PyPI
pip install jobhunt-cli
# Install Playwright's Chromium browser
playwright install chromium
# Install bundled Claude Code skills (optional)
jobhunt skills install
From source
git clone <repo-url>
cd jobhunt
python3 -m venv venv
source venv/bin/activate
pip install -e ".[dev]"
playwright install chromium
jobhunt --help
All user data is stored in ~/.config/jobhunt/:
profiles/— per-profile YAML filesjobhunt.db— SQLite databaseaggregators.yaml— global aggregator blocklistconfig.yaml— default profile and export path settingssession/state.json— saved LinkedIn browser session
First Run
On the first command you run, jobhunt will automatically launch a setup wizard:
# Just run any command — the wizard starts automatically if no profile exists
jobhunt list
The wizard will ask for:
- A profile name (e.g. your first name)
- Your target job titles (one per line)
- Your search locations (one per line)
After setup, edit the profile to add your full details:
nano ~/.config/jobhunt/profiles/<your-name>.yaml
Then run discover — a browser window will open for you to log in manually:
jobhunt discover
Commands
Discovery
# Discover jobs for the active profile
jobhunt discover
# Run without a visible browser window (requires saved credentials — see Headless section)
jobhunt discover --headless
# Discover for every profile
jobhunt discover --all-profiles
# Override search parameters for this run only
jobhunt discover --location "Remote" --location "Austin, TX"
jobhunt discover --days 14
jobhunt discover --title "ML Engineer" --title "Backend Engineer"
# Narrow to a specific company
jobhunt discover --company Stripe --company Airbnb
# Quick test — 1 location, 1 page, 5 jobs (verify session and profile without a full run)
jobhunt discover --dry-run
# Verbose output (page navigations, SQL inserts)
jobhunt discover --verbose
Discovery summary: 3 new · 1 easy apply · 12 skipped (8 aggregators filtered)
Listing Jobs
# Show new and opened jobs (default)
jobhunt list
# Show all jobs including applied and skipped
jobhunt list --status all
# Show jobs for a specific profile
jobhunt --profile alice list
Reviewing Jobs
# Interactive one-by-one review
jobhunt review
# Keys: [o]pen in browser [s]kip [n]ext [q]uit
Acting on Individual Jobs
# Open the career URL in your browser and mark the job as 'opened'
jobhunt open 42
# Mark a job as skipped (hidden from list and export)
jobhunt skip 42
Exporting
# Export to ~/job_tracker.xlsx (default)
jobhunt export
# Export to a specific path
jobhunt export --output ~/Documents/jobs_march.xlsx
# Set a persistent default export path in ~/.config/jobhunt/config.yaml:
# export_path: ~/Documents/jobs.xlsx
# Then just run:
jobhunt export
Skipped jobs are excluded. All other statuses (new, opened, applied) are included.
Dashboard
# Show job counts by status for the active profile
jobhunt dashboard
# Show counts for a specific profile
jobhunt --profile alice dashboard
Flushing the Database
# Delete all jobs for the active profile (prompts for confirmation)
jobhunt flush
# Delete jobs for a specific profile
jobhunt --profile alice flush
# Wipe all jobs across every profile
jobhunt flush --all-profiles
Profiles
# List all profiles (* marks the default)
jobhunt profile list
# Create a new profile
jobhunt profile create alice
# Switch the default profile
jobhunt profile set-default alice
# Edit a profile in $EDITOR (defaults to nano)
jobhunt profile edit
jobhunt profile edit alice
# Delete a profile
jobhunt profile delete alice
# Save LinkedIn credentials for headless login (password is encrypted at rest)
jobhunt profile set-credentials
# Remove saved credentials
jobhunt profile clear-credentials
# Run any command as a specific profile
jobhunt --profile alice discover
jobhunt --profile alice list
Profile resolution order:
--profileflagdefault_profilein~/.config/jobhunt/config.yaml- Error if no default is set but profiles exist — run
jobhunt profile set-default <name> - First-run wizard if no profiles exist at all
Profile YAML structure
personal:
name: "Your Name"
email: "you@example.com"
phone: "+1-555-000-0000"
linkedin_url: "https://linkedin.com/in/yourhandle"
github_url: "https://github.com/yourhandle"
location: "Austin, TX"
linkedin_email: "you@example.com" # optional: for headless auto-login
linkedin_password_enc: "gAAA..." # set via: jobhunt profile set-credentials
preferences:
target_titles:
- "Software Engineer"
- "Backend Engineer"
locations:
- "Austin, TX"
- "Remote"
max_days_posted: 7
min_salary: 140000 # set to null to disable
blocked_domains: [] # profile-specific domain blocklist additions
blocked_companies: [] # profile-specific company blocklist additions
work_history:
- company: "Acme Corp"
title: "Senior Engineer"
start: "2021-06"
end: null
description: "..."
tech: [Python, AWS]
skills: [Python, Java, AWS]
education:
- degree: "B.S. Computer Science"
school: "University of Texas at Austin"
year: 2019
certifications:
- "AWS Solutions Architect Associate"
resume_path: "/path/to/Resume.docx"
Claude Code Skills
jobhunt ships bundled Claude Code skills for AI-assisted job hunting workflows.
# Install skills to ~/.claude/skills/
jobhunt skills install
# List available skills
jobhunt skills list
# Overwrite existing skill files
jobhunt skills install --force
Available skills: jobhunt-discover, jobhunt-profile-creator, jobhunt-search-tuner, jobhunt-triage, jobhunt-track-application, identify-aggregators.
Aggregator Blocklist
Two-pass filter runs during discover:
- Pass 1 (before page visit): company name checked against the blocklist
- Pass 2 (after career URL extracted): career-site domain checked
# View the current blocklist
jobhunt aggregator list
# Global blocklist (affects all profiles)
jobhunt aggregator block-domain spamjobs.io
jobhunt aggregator unblock-domain spamjobs.io
jobhunt aggregator block-company "Jobs Via Dice"
jobhunt aggregator unblock-company "Jobs Via Dice"
# Profile-specific additions
jobhunt --profile alice aggregator block-domain foo.com
jobhunt --profile alice aggregator block-company "Some Staffing Co"
Headless Mode
Run discover without a visible browser window — useful for scheduled/automated runs.
Requires saved credentials first:
# Save your LinkedIn email and password (password is Fernet-encrypted at rest)
jobhunt profile set-credentials
# Now run headless
jobhunt discover --headless
The encryption key lives at ~/.config/jobhunt/.key (mode 600). If the key file is deleted, saved passwords are unrecoverable — re-run set-credentials.
⚠️ Headless mode is more likely to trigger LinkedIn bot detection than a normal browser session. Use sparingly.
Proxy Support
Route Playwright through a proxy by adding a proxy: key to ~/.config/jobhunt/config.yaml:
default_profile: alice
proxy:
server: "http://proxyhost:8080"
username: "user" # optional
password: "pass" # optional
Supports http://, https://, socks4://, and socks5:// servers. Remove the proxy: key to disable.
Job Statuses
| Status | Meaning |
|---|---|
new |
Discovered, not yet opened |
opened |
Career URL opened via jobhunt open <id> |
applied |
Applied — set manually |
skipped |
Dismissed — excluded from list and export |
To mark a job as applied:
sqlite3 ~/.config/jobhunt/jobhunt.db "UPDATE jobs SET status='applied' WHERE id=42;"
config.yaml
~/.config/jobhunt/config.yaml controls global settings:
default_profile: alice
export_path: ~/Documents/jobs.xlsx # optional: persistent default export path
proxy: # optional: Playwright proxy
server: "http://proxyhost:8080"
File Structure
jobhunt/ ← project root
├── pyproject.toml ← package definition + entry point
├── requirements.txt
├── src/jobhunt/
│ ├── cli.py ← Click commands (entry point)
│ ├── scraper.py ← Playwright scraper + session management
│ ├── scraper_utils.py ← Pure parsing helpers
│ ├── filters.py ← Aggregator detection
│ ├── db.py ← SQLite layer
│ ├── exporter.py ← Excel export
│ ├── crypto.py ← Fernet encryption for credentials
│ ├── paths.py ← CONFIG_DIR constant
│ └── defaults/aggregators.yaml ← Bundled default blocklist
└── tests/
~/.config/jobhunt/ ← all runtime data
├── config.yaml
├── aggregators.yaml
├── .key ← Fernet encryption key (mode 600)
├── profiles/
│ └── <name>.yaml
├── jobhunt.db
└── session/state.json
Running Tests
source venv/bin/activate
pytest tests/ -v
# 113 tests — no live LinkedIn connection required
Troubleshooting
| Problem | Fix |
|---|---|
| Bot detection / CAPTCHA | Wait several hours; reduce run frequency |
open <id> opens nothing |
Easy Apply only job — no external career URL exists |
| Profile not found | Run jobhunt profile list to see available profiles |
| Session expired mid-run | Tool detects and prompts for manual re-login |
| Key file deleted | Re-run jobhunt profile set-credentials |
| openpyxl deprecation warnings in tests | Cosmetic; safe to ignore |
| Setup wizard cancelled | Re-run any command to restart it |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jobhunt_cli-1.0.0.tar.gz.
File metadata
- Download URL: jobhunt_cli-1.0.0.tar.gz
- Upload date:
- Size: 45.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b0445f91ec8871e6623a885f1d12a1836584d5e1fdabbec7a9df826d610343ef
|
|
| MD5 |
e5c6083fc63bdb387cd887121c086ea8
|
|
| BLAKE2b-256 |
0b48f82dd35a13325e2419c0c8fd96b4f7135dc6bd0fdab375d9da6074e4b46b
|
Provenance
The following attestation bundles were made for jobhunt_cli-1.0.0.tar.gz:
Publisher:
python-publish.yml on urknin/jobhunt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jobhunt_cli-1.0.0.tar.gz -
Subject digest:
b0445f91ec8871e6623a885f1d12a1836584d5e1fdabbec7a9df826d610343ef - Sigstore transparency entry: 1116029905
- Sigstore integration time:
-
Permalink:
urknin/jobhunt@5fff3d0644f95603e2d3ead43766ceb273fc5d3e -
Branch / Tag:
refs/tags/1.0.0 - Owner: https://github.com/urknin
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@5fff3d0644f95603e2d3ead43766ceb273fc5d3e -
Trigger Event:
release
-
Statement type:
File details
Details for the file jobhunt_cli-1.0.0-py3-none-any.whl.
File metadata
- Download URL: jobhunt_cli-1.0.0-py3-none-any.whl
- Upload date:
- Size: 38.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4caaa7f91c1f084bc9dc08932fd334b12a61c7528d74b2a43d9e5db607af5796
|
|
| MD5 |
e5d61dc00a5c3e25b14cc40acc08cfe4
|
|
| BLAKE2b-256 |
04a778fc69137ca08858cf19866131eca03edf614fbe5418467ce423bca0b94b
|
Provenance
The following attestation bundles were made for jobhunt_cli-1.0.0-py3-none-any.whl:
Publisher:
python-publish.yml on urknin/jobhunt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
jobhunt_cli-1.0.0-py3-none-any.whl -
Subject digest:
4caaa7f91c1f084bc9dc08932fd334b12a61c7528d74b2a43d9e5db607af5796 - Sigstore transparency entry: 1116030352
- Sigstore integration time:
-
Permalink:
urknin/jobhunt@5fff3d0644f95603e2d3ead43766ceb273fc5d3e -
Branch / Tag:
refs/tags/1.0.0 - Owner: https://github.com/urknin
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@5fff3d0644f95603e2d3ead43766ceb273fc5d3e -
Trigger Event:
release
-
Statement type: