
Show HN: Shobr – Job seach CLI via browser automation
The stealthiest, UNIX-iest, Job Search Automator, with a
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
The stealthiest, UNIX-iest, Job Search Automator, with a Hacker-in-the-Loop approach.
demo.mp4
Introduction
In 2026's job market, there are many job application automation tools, some of them FOSS. This one's mine, and relies on:
Daily-Driver Stealth: We don't use headless browsers. SHOBR uses beachpatrol to drive your existing, authenticated browser. To LinkedIn, you are just a normal user clicking around.
Human-in-the-Loop: SHOBR prepares, proposes, and verifies. It writes the drafts and builds the PDFs, but the final application submit is always done by you. This is no "spray and pray", but you can still pray to any API-compatible deities.
LLM-light By Design: Automated, high-quality tuning of resume to job requires some LLM, there's not much leeway around it. But this project uses as little LLM as possible, and doesn't demand an Agent driver (like other projects in this space). If you want more, it should be trivial to ask an LLM to write a SKILL or an MCP server on top of SHOBR.
LLM-provider-Agnostic: Uses an LLM abstraction (any-llm). So, you can run SHOBR's LLM steps on OpenAI, Anthropic, a local model, or hijack a local LLM agent subscription via faaah.
Event-Sourced Data: All data (discovered jobs, screenings, tracking) is saved in append-only JSONL event logs and projected into state files. You can interrupt the pipeline, or recompute lead approval with new rules, at any time without data loss.
Markdown-Based CV Toolchain: Resumes are tailored in Markdown and compiled to ATS-readable PDFs (via Groff and Pandoc). All deliverables are put in per-application folders.
Full E2E Red-Green TDD: Built with the stdlib's unittest, no extra framework.
Not Vibecoded: 3500 LOC (including comments) at time of writing. Somewhat atypical in this space.
Installation
beachpatrol Requirement
The one hard requirement of this project is beachpatrol. You can think of it as a browser that you're meant to use as your daily driver, but which is also fully automatable (via a clever "Playwright wrapper" approach).
Why beachpatrol? Well, job search requires scraping. Ideally scraping done using your actual authenticated credentials. So, what better way to avoid detection than using your actual daily-driver browser to do the scraping? (It should be virtually identical to regular use, provided you don't break any ToS).
Other "job search automation tools" either use unauthenticated requests or headless browsers, or ask you to extract / copy your authenticated credentials into their automated browsers. Our beachpatrol approach aims to do them all one better by using your actual daily-driver browser.
If you're interested, see beachpatrol's README.
With beachpatrol already setup, shobr requires Python >= 3.14 with uv:
git clone https://github.com/sebastiancarlos/shobr
cd shobr
uv sync # install the single runtime dependency, any-llm-sdk[openai]
uv tool install . # Put the shobr CLI on PATH
shobr --help
Because SHOBR leverages roffume to compile Markdown resumes into PDFs, you will need some standard Unix text-processing tools on your system: groff and pandoc.
SHOBR breaks the job search process into 5 pipeline stages.
$ shobr status
-
DISCOVERY
-
Total Leads Found: 59
-
Rejected by Filter: 10
-
Pending Enrichment: 3
-
ENRICHMENT
-
Total Enriched: 48
-
Rejected by Filter: 2
-
Pending Screening: 24
-
SCREENING
-
Total Screened: 45
-
Skipped: 6
-
Lacking LLM Review: 1
-
Pending Human Review: 23
-
LLM Scores: Human Scores: 5: 11 5: 2 (1 to tailor) 4: 9 4: 6 (5 to tailor) 3: 9 3: 4 (4 to tailor) 2: 8 2: 4 (4 to tailor) 1: 8 1: 6
-
Pending Tailoring: 14
-
TAILORING
-
Packages Built: 2
-
Pending Review: 0
-
TRACKING
-
Applied: 2
-
Interviewing: 0
-
Offer: 0
-
Rejected: 0
-
Ghosted: 0
-
Withdrawn: 0
- Discovery
Scrapes the LinkedIn job search results based on your config.toml keywords and locations, running them through a basic regex pre-filter.
Visits individual job pages to extract full descriptions, salary ranges, and Easy Apply links. Done one at a time to pace requests and avoid rate-limits.
Scores enriched jobs against your personal Markdown profile and deal-breakers.
For jobs marked "Pursue", SHOBR uses the LLM to rewrite your base resume.md to highlight relevant skills. It then uses the roffume (Groff/Pandoc) toolchain to ensure the rewrite perfectly fits on one page, looping rewrites if it overflows.
Local Kanban-style tracking for your applications.
Everything SHOBR knows about you lives under $XDG_CONFIG_HOME/shobr/ (default ~/.config/shobr): one config.toml plus a profile/*.md folder. shobr setup scaffolds all of them with instructional templates.
config.toml # filter rules, geo map, toolchain + browser wiring profile/ user-detail.md fit-criteria.md deal-breakers.md # screen-llm inputs resume-guide.md cover-guide.md # tailor-only inputs
The profile folder defaults to $XDG_CONFIG_HOME/shobr/profile and can be relocated with the profile_dir config.
cv_toolchain_dir (required)
cv_toolchain_dir = "~/shobr-resumes"
Home of the CV toolchain (a roffume git checkout). The main resume defaults to <cv_toolchain_dir>/resume.md (but can be configured with main_cv_path). The CV toolchain directory will ultimately contain all the generated CVs and other data, in its internal "per-application" directories.
profile_dir = "~/.config/shobr/profile"
Custom directory holding the profile markdown files.
main_cv_path = "~/shobr-resumes/resume.md"
Custom path to the main CV.
beachpatrol_profile = "job-hunter"
beachpatrol browser profile holding the logged-in LinkedIn session.
beachpatrol_browser = "chromium"
beachpatrol browser to drive.
Note: chromium is recommended. firefox works too, but its stealth plugin is weaker, so LinkedIn can cause errors more more often. See beachpatrol's Known Issues.
titles = ["Technical Lead", "Software Engineer", "Senior Software Engineer"]
Job titles fed to LinkedIn search as one ORed keyword query. Like "Software Engineer", "Fullstack Developer", etc.
workplace_types = ["on-site", "hybrid", "remote"]
Appended to the same search OR query. Possible values are: on-site, hybrid, remote.
geo = ["new-york-city", "san-francisco-bay-area"]
Geo targets for the query, referred to BY NAME through the [geo_ids] map.
[geo_ids] new-york-city = "111111111" san-francisco-bay-area = "222222222"
Maps each geo name to a LinkedIn geoId. The names are totally customizable, but should represent the name of a real-world location. You have to obtain the id directly from the LinkedIn Jobs URLs (geoId=), after performing a search for a given location. Note that LinkedIn often has several ids per place (city vs metro area).
reject_employment_type = ["Internship"]
Employment types rejected at enrichment. Possible values are: Full-time, Part-time, Contract, Temporary, Internship.
presence_locations = ["New York"]
Places acceptable for presence-required work. Values are literal strings of names of locations (matched case-insensitive). Remote postings pass anywhere. "On-site" and "hybrid" postings must name a listed location.
stale_after_days = 2
Days after the last enrichment check when a lead counts as stale. It's important to re-enrich stale leads to ensure they're still open.
Costly actions (screening, tailoring, etc) refuse stale rows unless passing --force.
reject_recent_application_days = 90
Reject leads at companies with already tracked application, within this many days of the lead's last sighting.
[reject_title] golang = "\bgolang\b" devops = "\bdevops\b"
Filters by pre-filter. Matched against job title. The leads are rejected with reason title contains '<label>'.
While only LinkedIn has built-in support, a custom job board provider can be added by hacking away at the code. The interface to do so is quite straigthforward (defined at src/shobr/providers/base.py). If you do add another provider, you can override global configuration for it with the [providers.<name>] TOML form.
Also, every CLI command supports the --provider <name> flag to define which provider to act upon (defaulting to Linkedin otherwise).
The LLM stages read your profile as plain markdown files. Initialize the profile templates with shobr setup, and then fill the files yourself.
Your main CV, used as a base to screen and generate tailored CVs. Resolved by: SHOBR_MAIN_CV_PATH, the main_cv_path config, then <cv_toolchain_dir>/resume.md.
Work history and proficiencies in more detail than the CV.
What makes a lead worth pursuing, in your own words.
Veto rules (if found to match, it produces a score of 1, meaning that the lead is discarded).
Your own rules and suggestions on how to tailor your main CV to a particular application. It might include formatting rules.
Guide about how to write the cover letter for a given application. Explain tone, length, etc.
SHOBR relies on roffume, a CV toolchain. The first tailor run offers to clone it (clones a pinned release) into ~/shobr-resumes.
roffume isn't hardwired. SHOBR talks to it through a CV toolchain interface (four methods: scaffold, build, page_check, finalize) defined by the CvToolchain abstract class in cv_toolchain.py. Any tool that implements that interface can be swapped in for roffume (via some soft forking-and-hacking).
<cv_toolchain_dir>/ # default: ~/shobr-resumes resume.md # default main resume (see main_cv_path) resume.pdf # built main resume applications/<slug>/ # one per tailored posting resume.md # tailored resume (rewritten until it fits one page) cover-letter.md # generated cover letter notes.md # source posting URL *.pdf # built outputs
SHOBR File Structure
shobr/ pyproject.toml README.md test.py E2E test suite test-fixtures/ synthetic HTML fixtures (fake data) backing the E2E tests src/shobr/ beachpatrol-commands/ beachpatrol commands (.js files) templates/ LLM prompts, profile scaffolds, config default core.py cross-functional core cli.py argument parsing + entry point browser.py beachpatrol integration ai.py Minimal LLM-provider integration notification.py notifications (unwired lead source, not a stage) discovery.py discovery stage enrichment.py enrichment stage screening.py screening stage tailoring.py tailoring stage (CV toolchain contract) tracking.py tracking stage pipeline.py next/dispatcher config.py config.toml loading + validation color.py terminal palette
Application Data ($XDG_DATA_HOME/shobr/)
SHOBR uses an Event Sourcing pattern. Every pipeline stage has an append-only events.jsonl log, which is replayed to create a .json projection of current state.
notifications/ events.jsonl -> notifications.json # Notification queue discovery/ events.jsonl -> discovery.json # Discovery stage enrichment/ events.jsonl -> enrichment.json # Scraped job details screening/ events.jsonl -> screening.json # LLM and Human scores tailoring/ events.jsonl -> tailoring.json # CV generation status tracking/ events.jsonl -> tracking.json # Kanban funnel status smoke/ linkedin-homepage.html # smoke-test-browser dump
Known Limitations
LinkedIn only.
Unlike other tools in this space, this one's focused only on LinkedIn (hi LinkedIn legal team!). Having said that, it shouldn't be that hard to go full "Uncle Bob" on the codebase and abstract away some other providers as soon as popular demand (or the author's demand).
LinkedIn DOM drift will eventually break.
Extraction depends on LinkedIn's markup (data-testid, card keys, pill icons). When it changes, commands fail loudly and write nothing, by design. Your humble servant here hopes to fix this as needed. After all, if LLMs can hack Hugging Face, they can easily help me figure out the new DOM structure in a matter of minutes.
Hard beachpatrol requirement.
No unauthenticated or headless mode. You need beachpatrol driving a real browser logged into LinkedIn (ideally your daily-driver browser, to naturally expand to all your automation requirements, and to provide the most huma
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。