{"slug": "shobr-job-seach-cli-via-browser-automation-event-sourcing-llm", "title": "Shobr: Job seach CLI via browser automation, event-sourcing, LLM", "summary": "Developer Sebastian Carlos released Shobr, an open-source job-search automation CLI that drives a user's own authenticated daily-driver browser via the beachpatrol Playwright wrapper rather than a headless browser, keeping the final application submit human-in-the-loop. Shobr stores all discovered jobs, screenings and tracking data in append-only JSONL event logs projected into state files, uses the any-llm abstraction to stay LLM-provider-agnostic, and compiles Markdown resumes to ATS-readable PDFs through Groff and Pandoc. The project requires Python 3.14 or later with uv, plus groff and pandoc, and totals roughly 3,500 lines of code including comments.", "body_md": "**The stealthiest, UNIX-iest, Job Search Automator, with a\nHacker-in-the-Loop approach.**\n\n## demo.mp4\n\nIn 2026's job market, there are many job application automation tools, some of them FOSS. This one's mine, and relies on:\n\n- [`beachpatrol`](https://github.com/sebastiancarlos/beachpatrol) for browser\nautomation of your*own daily-driver browser* , and\n- [`roffume`](https://github.com/sebastiancarlos/roffume) for resume files\nmanagement.\n\n- **Daily-Driver Stealth:** We don't use headless browsers. SHOBR uses[`beachpatrol`](https://github.com/sebastiancarlos/beachpatrol) to drive\nyour existing, authenticated browser. To LinkedIn, you are just a normal\nuser clicking around.\n- **Human-in-the-Loop:** SHOBR prepares, proposes, and verifies. It writes the\ndrafts and builds the PDFs, but the final application submit is always done\nby you. This is*no \"spray and pray\"* , but you can still pray to any\nAPI-compatible deities.\n- **LLM-light By Design:** Automated, high-quality tuning of resume to job\nrequires 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\nother projects in this space). If you want more, it should be trivial to ask\nan LLM to write a SKILL or an MCP server on top of SHOBR.\n- **LLM-provider-Agnostic:** Uses an LLM abstraction\n([`any-llm`](https://github.com/mozilla-ai/any-llm) ). So, you can run\nSHOBR's LLM steps on OpenAI, Anthropic, a local model, or hijack a local LLM\nagent subscription via[`faaah`](https://github.com/sebastiancarlos/faaah) .\n- **Event-Sourced Data:** All data (discovered jobs, screenings, tracking) is\nsaved in append-only JSONL event logs and projected into state files. You\ncan interrupt the pipeline, or recompute lead approval with new rules, at\nany time without data loss.\n- **Markdown-Based CV Toolchain:** Resumes are tailored in Markdown and\ncompiled to ATS-readable PDFs (via Groff and Pandoc). All deliverables are\nput in per-application folders.\n- **Full E2E Red-Green TDD:** Built with the stdlib's`unittest` , no extra\nframework.\n- **Not Vibecoded** : 3500 LOC (including comments) at time of writing.\nSomewhat atypical in this space.\n\nThe **one hard requirement** of this project is\n[**`beachpatrol`**](https://github.com/sebastiancarlos/beachpatrol). You can\nthink of it as a browser that you're meant to use as your daily driver, but\nwhich is *also* fully automatable (via a clever \"Playwright wrapper\"\napproach).\n\nWhy `beachpatrol`? Well, job search requires scraping. Ideally scraping done\nusing your *actual authenticated credentials*. So, what better way to avoid\ndetection than using your *actual daily-driver browser to do the scraping*?\n(It should be virtually identical to regular use, provided you don't break any\nToS).\n\nOther \"job search automation tools\" either use unauthenticated requests or\nheadless browsers, or ask you to extract / copy your authenticated credentials\ninto their automated browsers. Our `beachpatrol` approach aims to do them all\none better by *using your actual daily-driver browser*.\n\nIf you're interested, see [beachpatrol's\nREADME](https://github.com/sebastiancarlos/beachpatrol).\n\nWith `beachpatrol` already setup, `shobr` requires Python >= 3.14 with\n[`uv`](https://docs.astral.sh/uv/):\n\n```\ngit clone https://github.com/sebastiancarlos/shobr\ncd shobr\nuv sync               # install the single runtime dependency, `any-llm-sdk[openai]`\nuv tool install .     # Put the `shobr` CLI on `PATH`\nshobr --help\n```\n\nBecause SHOBR leverages `roffume` to compile Markdown resumes into PDFs, you\nwill need some standard Unix text-processing tools on your system: `groff` and\n`pandoc`.\n\nThen, in order:\n\n1. Run `shobr setup` to scaffold the SHOBR config file under`$XDG_CONFIG_HOME/shobr/config.toml` , the profile templates, and to make`shobr` 's own`beachpatrol` commands available to`beachpatrol` (by\nsymlinking them into the expected folder). Fill the config in.\n2. Ensure you have one `beachpatrol` profile which is logged into LinkedIn.\nPut that*`beachpatrol` profile name* in`config.toml` on the`beachpatrol_profile` key.\n3. For the parts of SHOBR requiring LLMs, `any-llm-sdk` reads provider keys\nfrom env (`OPENAI_API_KEY` ,`SHOBR_AI_MODEL` , and`OPENAI_BASE_URL` ).\nNaturally, you can use any LLM API provider you want through`any-llm-sdk` (or even hijack a locally available LLM agent subscription by using[`faaah`](https://github.com/sebastiancarlos/faaah) ).\n4. For the parts of SHOBR requiring to read your **main CV** , you can refer\nto it via the env`SHOBR_MAIN_CV_PATH` (or see next step).\n5. For the parts of SHOBR requiring authoring CVs and application\ndirectories, you need to configure a **CV toolchain.**  - The first time you reach the `tailor` step,`shobr` will offer to clone\nthe latest[`roffume`](https://github.com/sebastiancarlos/roffume) release into`~/shobr-resumes` (or point`cv_toolchain_dir` in`config.toml` at an existing checkout). This folder will keep track of\nall your resume variation inputs (markdown) and outputs (PDFs).\n  - Then, the *main CV* defaults to`<cv_toolchain_dir>/resume.md` (`SHOBR_MAIN_CV_PATH` overrides).\n6. The first time you reach the \n\nSHOBR breaks the job search process into *5 pipeline stages*.\n\nYou can run:\n\n- **`shobr status`**  - See details about every pipeline stage.\n- **`shobr next`**  - Have SHOBR automatically prompt you for the next logical action across the entire pipeline (rather than running the manual, \"plumbing\" command directly).\n\n**Example `shobr status` output:**\n\n``` bash\n$ shobr status\n\n- DISCOVERY\n  - Total Leads Found:     59\n  - Rejected by Filter:    10\n  - Pending Enrichment:    3\n\n- ENRICHMENT\n  - Total Enriched:        48\n  - Rejected by Filter:    2\n  - Pending Screening:     24\n\n- SCREENING\n  - Total Screened:        45\n  - Skipped:               6\n  - Lacking LLM Review:    1\n  - Pending Human Review:  23\n  - LLM Scores:    Human Scores:\n    5: 11          5: 2 (1 to tailor)\n    4: 9           4: 6 (5 to tailor)\n    3: 9           3: 4 (4 to tailor)\n    2: 8           2: 4 (4 to tailor)\n    1: 8           1: 6\n  - Pending Tailoring:     14\n\n- TAILORING\n  - Packages Built:        2\n  - Pending Review:        0\n\n- TRACKING\n  - Applied:               2\n  - Interviewing:          0\n  - Offer:                 0\n  - Rejected:              0\n  - Ghosted:               0\n  - Withdrawn:             0\n```\n\nScrapes the LinkedIn job search results based on your `config.toml` keywords\nand locations, running them through a basic regex pre-filter.\n\n- **`shobr discovered`**  - Prints the stored leads summary without fetching.\n- **`shobr discover`**  - Triggers `beachpatrol` to search and scrape leads.\n- Triggers \n\nVisits 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.\n\n- **`shobr enriched`**  - Prints all enriched jobs.\n- **`shobr enrich-next`**  - Fetches the detail page for the oldest non-enriched lead.\n- **`shobr enrich <posting_id>`**  - Fetches a specific job.\n\nScores enriched jobs against your personal Markdown profile and deal-breakers.\n\n- **`shobr screen-llm-next`**  - Asks the LLM to score the next lead (1-5) and write reasoning.\n- **`shobr screen-llm-all`**  - Batch runs the LLM against all unscored leads.\n- **`shobr screen-next`**  - Records your own verdict for the next lead (score 1-5 plus reason),\nvia `$EDITOR` or`--score` /`--reason` .\n- Records your own verdict for the next lead (score 1-5 plus reason),\nvia \n\nFor jobs marked \"Pursue\", SHOBR uses the LLM to rewrite your base `resume.md`\nto highlight relevant skills. It then uses the `roffume` (Groff/Pandoc)\ntoolchain to ensure the rewrite perfectly fits on one page, looping rewrites\nif it overflows.\n\n- **`shobr tailor-next`**  - Builds the application package (Resume + Cover Letter) for the next pursue-able job.\n- **`shobr tailored`**  - Prints all generated packages.\n\nLocal Kanban-style tracking for your applications.\n\n- **`shobr track <posting_id> <status> [--note TEXT]`**  - Updates pipeline status (` applied` ,`interviewing` ,`offer` ,`rejected` ,\netc.).\n- Updates pipeline status (\n- **`shobr tracked`**  - Prints a high-level overview of your entire funnel.\n\nEverything SHOBR knows about *you* lives under\n`$XDG_CONFIG_HOME/shobr/` (default `~/.config/shobr`): one `config.toml` plus\na `profile/*.md` folder. `shobr setup` scaffolds all of them with\ninstructional templates.\n\n```\nconfig.toml      # filter rules, geo map, toolchain + browser wiring\nprofile/\n  user-detail.md fit-criteria.md deal-breakers.md   # screen-llm inputs\n  resume-guide.md cover-guide.md                    # tailor-only inputs\ncv_toolchain_dir = \"~/shobr-resumes\"\n```\n\nHome of the *CV toolchain* (a `roffume` git checkout). The *main resume*\ndefaults to `<cv_toolchain_dir>/resume.md`. The *CV toolchain* directory will\nultimately contain all the generated CVs and other data, in its internal\n\"per-application\" directories.\n\n```\nbeachpatrol_profile = \"job-hunter\"\n```\n\n`beachpatrol` browser profile holding the logged-in LinkedIn session.\n\n```\nbeachpatrol_browser = \"chromium\"\n```\n\n`beachpatrol` browser to drive.\n\n```\ntitles = [\"Technical Lead\", \"Software Engineer\", \"Senior Software Engineer\"]\n```\n\nJob titles fed to LinkedIn search as one ORed keyword query. Like \"Software Engineer\", \"Fullstack Developer\", etc.\n\n```\nworkplace_types = [\"on-site\", \"hybrid\", \"remote\"]\n```\n\nAppended to the same search OR query. Possible values are: `on-site`,\n`hybrid`, `remote`.\n\n```\ngeo = [\"new-york-city\", \"san-francisco-bay-area\"]\n```\n\nGeo targets for the query, referred to BY NAME through the `[geo_ids]` map.\n\n```\n[geo_ids]\nnew-york-city = \"111111111\"\nsan-francisco-bay-area = \"222222222\"\n```\n\nMaps each geo name to a LinkedIn geoId. The names are totally customizable,\nbut should represent the name of a real-world location. You have to obtain\nthe id directly from the LinkedIn Jobs URLs (`geoId=`), after performing a\nsearch for a given location. Note that LinkedIn often has several ids per\nplace (city vs metro area).\n\n```\nreject_employment_type = [\"Internship\"]\n```\n\nEmployment types rejected at enrichment. Possible values are: `Full-time`,\n`Part-time`, `Contract`, `Temporary`, `Internship`.\n\n```\npresence_locations = [\"New York\"]\n```\n\nPlaces 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.\n\n```\n[reject_title]\ngolang = \"\\\\bgolang\\\\b\"\ndevops = \"\\\\bdevops\\\\b\"\n```\n\nFilters by pre-filter. Matched against job title. The leads are rejected with\nreason `title contains '<label>'`.\n\nThe LLM stages read your profile as plain markdown files. Initialize the\nprofile templates with `shobr setup`, and then fill the files yourself.\n\nYour main CV, used as a base to generate tailored CVs. Referred by either\n`SHOBR_MAIN_CV_PATH` or `<cv_toolchain_dir>/resume.md`.\n\nWork history and proficiencies in more detail than the CV.\n\nWhat makes a lead worth pursuing, in your own words.\n\nVeto rules (if found to match, it produces a score of `1`, meaning that the\nlead is discarded).\n\nYour own rules and suggestions on how to tailor your main CV to a particular application. It might include formatting rules.\n\nGuide about how to write the cover letter for a given application. Explain tone, length, etc.\n\nSHOBR relies on [`roffume`](https://github.com/sebastiancarlos/roffume), a\nCV toolchain. The first `tailor` run offers to clone it (clones a pinned\nrelease) into `~/shobr-resumes`.\n\n`roffume` isn't hardwired. SHOBR talks to it through a **CV toolchain\ninterface** (four methods: `scaffold`, `build`, `page_check`, `finalize`)\ndefined by the `CvToolchain` abstract class in `cv_toolchain.py`. Any tool\nthat implements that interface can be swapped in for `roffume` (via some soft\nforking-and-hacking).\n\n```\n<cv_toolchain_dir>/        # default: ~/shobr-resumes\n  resume.md                # main resume (unless pointed elsewhere by SHOBR_MAIN_CV_PATH)\n  resume.pdf               # built main resume\n  applications/<slug>/     # one per tailored posting\n    resume.md              # tailored resume (rewritten until it fits one page)\n    cover-letter.md        # generated cover letter\n    notes.md               # source posting URL\n    *.pdf                  # built outputs\nshobr/\n  pyproject.toml\n  README.md\n  test.py                   E2E test suite\n  test-fixtures/            synthetic HTML fixtures (fake data) backing the E2E tests\n  src/shobr/\n    beachpatrol-commands/   beachpatrol commands (.js files)\n    templates/              LLM prompts, profile scaffolds, config default\n    core.py                 cross-functional core\n    cli.py                  argument parsing + entry point\n    browser.py              beachpatrol integration\n    ai.py                   Minimal LLM-provider integration\n    notification.py         notifications (unwired lead source, not a stage)\n    discovery.py            discovery stage\n    enrichment.py           enrichment stage\n    screening.py            screening stage\n    tailoring.py            tailoring stage (CV toolchain contract)\n    tracking.py             tracking stage\n    pipeline.py             next/dispatcher\n    config.py               config.toml loading + validation\n    color.py                terminal palette\n```\n\nSHOBR uses an Event Sourcing pattern. Every pipeline stage has an append-only\n`events.jsonl` log, which is replayed to create a `.json` projection of\ncurrent state.\n\n``` php\nnotifications/ events.jsonl -> notifications.json  # Notification queue\ndiscovery/     events.jsonl -> discovery.json      # Discovery stage\nenrichment/    events.jsonl -> enrichment.json     # Scraped job details\nscreening/     events.jsonl -> screening.json      # LLM and Human scores\ntailoring/     events.jsonl -> tailoring.json      # CV generation status\ntracking/      events.jsonl -> tracking.json       # Kanban funnel status\nsmoke/         linkedin-homepage.html              # smoke-test-browser dump\n```\n\n- **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).\n- **LinkedIn DOM drift will eventually break.**  - Extraction depends on LinkedIn's markup (`data-testid` , card keys, pill\nicons). When it changes, commands fail loudly and write nothing, by\ndesign. Your humble servant here hopes to fix this as needed. After all,\nif LLMs can hack Hugging Face, they can easily help me figure out the\nnew DOM structure in a matter of minutes.\n- Extraction depends on LinkedIn's markup (\n- **Hard beachpatrol requirement.**  - No unauthenticated or headless mode. You need `beachpatrol` driving a real\nbrowser logged into LinkedIn (ideally your daily-driver browser, to\nnaturally expand to all your automation requirements, and to provide the\nmost human signals possible).\n- No unauthenticated or headless mode. You need \n- **No database.**  - State is flat JSON files, not a database. This is actually a good thing (at current scale)\n- **No scheduler.**  - Pacing is manual (`next` and friends are one-per-invocation). You are free\nto automate it to your heart's content via cron jobs, systemd timers, or\neven your phone-controlled AI swarm mining crypto on Hetzner datacenters.\n- Pacing is manual (\n\n- **LinkedIn ToS is your risk to take.**  - Automating a daily-driven browser with a logged-in account, however\nnative, may violate LinkedIn's terms. Pace yourself (Our commands like\n`shobr next` do at most one scraping, exactly for this). Keep volumes\nhuman.\n- Automating a daily-driven browser with a logged-in account, however\nnative, may violate LinkedIn's terms. Pace yourself (Our commands like\n- **Your CV (and `profiles/` info) goes to third parties.**  - `screen-llm` and`tailor` send your resume, profile docs, and job postings\nto whichever LLM provider`any-llm-sdk` is pointed at. That is your name,\nwork history, and location scoping on someone else's servers. Prefer\nless-evil providers, or use local models.\n- **No credential extraction.**  - shobr never asks for your LinkedIn password or session tokens;\nauthentication lives entirely in your daily-driver browser via\n`beachpatrol` .\n- shobr never asks for your LinkedIn password or session tokens;\nauthentication lives entirely in your daily-driver browser via\n\n- **[career-ops](https://github.com/santifer/career-ops)** (~72k stars)\n  - Markdown/filesystem-based skill set loaded by a coding agent. Human-in-the-loop by design, with scoring, CV tailoring, and interview prep.\n  - Very similar to SHOBR. But SHOBR comes with its own browser automation setup, rather than relying on the agent doing it by itself. Also, SHOBR flow is CLI-based and limited in LLM usage; the orchestration is programmatic, rather than agent-driven.\n- **[Morning Stack](https://morningstack.app/)** (commercial)\n  - Overnight batch job that scrapes boards, verifies listings are still live, fact-checks tailored resumes, and presents results by morning.\n  - SHOBR shares the \"prepares, then human submits\" pattern but runs interactively, uses the user's authenticated browser, and doesn't verify that listings are still live (we assume that LinkedIn is good at figuring this out and exposing it).\n- **[Simplify](https://simplify.jobs/copilot)** (commercial)\n  - Browser extension that autofills application forms from a saved profile; human clicks submit.\n  - SHOBR doesn't autofill applications. The user (or the browser's autofill features) handle the full final application.\n- **[AIHawk](https://github.com/feder-cr/AIHawk)** (~30k stars)\n  - LLM-driven apply-bot using a patched Playwright fork. Auto-submits applications at scale. Received some online backlash for clogging recruiter inboxes and a LinkedIn cease-and-desist (which forced a code dumbing down).\n  - SHOBR avoids automated submissions to avoid a cease-and-desist (although we would love that sort of free publicity!)\n- **[JobSpy](https://github.com/speedyapply/JobSpy)** (~4k stars)\n  - HTTP-based job board scraper using TLS fingerprint impersonation (no browser). Very well documented schemas.\n  - SHOBR uses a real browser session for discovery.\n- **[browser-use](https://github.com/browser-use/browser-use)** (~110k stars)\n  - General-purpose browser agent framework. Not job-specific.\n  - I guess `beachpatrol` would be the most direct comparison here.\n\nMIT", "url": "https://wpnews.pro/news/shobr-job-seach-cli-via-browser-automation-event-sourcing-llm", "canonical_source": "https://github.com/sebastiancarlos/shobr", "published_at": "2026-09-28 08:05:47+00:00", "updated_at": "2026-09-28 08:18:58.215413+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents"], "entities": ["Shobr", "Sebastian Carlos", "beachpatrol", "roffume", "any-llm", "faaah", "LinkedIn", "Pandoc"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/shobr-job-seach-cli-via-browser-automation-event-sourcing-llm", "markdown": "https://wpnews.pro/news/shobr-job-seach-cli-via-browser-automation-event-sourcing-llm.md", "text": "https://wpnews.pro/news/shobr-job-seach-cli-via-browser-automation-event-sourcing-llm.txt", "jsonld": "https://wpnews.pro/news/shobr-job-seach-cli-via-browser-automation-event-sourcing-llm.jsonld"}}