The stealthiest, UNIX-iest, ethical Job Search Automator
Python
2
1 commits
updated Sep 28, 2026
The stealthiest, UNIX-iest, ethical Job Search Automator, with a Hacker-in-the-Loop approach.
https://github.com/user-attachments/assets/47336c4f-1ca8-40e4-869d-5496bf7fae53
In 2026's job market, there are many job application automation tools, some of them FOSS. This one's mine, and relies on these tools:
beachpatrol for browser
automation of your own daily-driver browser, androffume for resume files
management.
beachpatrol to drive
your existing, authenticated browser. To LinkedIn, you are just a normal
user clicking around.faaah.unittest, no extra
framework.beachpatrol RequirementThe 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.
Then, in order:
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
symlinking them into the expected folder). Fill the config in.beachpatrol profile which is logged into LinkedIn.
Put that beachpatrol profile name in config.toml on the
beachpatrol_profile key.any-llm-sdk reads provider keys
from env (OPENAI_API_KEY, SHOBR_AI_MODEL, and OPENAI_BASE_URL).
Naturally, 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).SHOBR_MAIN_CV_PATH (or see next step).tailor step, shobr will offer to clone
the latest roffume
release into ~/shobr-resumes (or point cv_toolchain_dir in
config.toml at an existing checkout). This folder will keep track of
all your resume variation inputs (markdown) and outputs (PDFs).<cv_toolchain_dir>/resume.md
(SHOBR_MAIN_CV_PATH overrides).SHOBR breaks the job search process into 5 pipeline stages.
You can run:
shobr status
shobr next
Example shobr status output:
$ 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
Scrapes the LinkedIn job search results based on your config.toml keywords
and locations, running them through a basic regex pre-filter.
shobr discovered
shobr discover
beachpatrol to search and scrape leads.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.
shobr enriched
shobr enrich-next
shobr enrich <posting_id>
Scores enriched jobs against your personal Markdown profile and deal-breakers.
shobr screen-llm-next
shobr screen-llm-all
shobr screen-next
$EDITOR or --score/--reason.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.
shobr tailor-next
shobr tailored
Local Kanban-style tracking for your applications.
shobr track <posting_id> <status> [--note TEXT]
applied, interviewing, offer, rejected,
etc.).shobr tracked
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
config.tomlcv_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. The CV toolchain directory will
ultimately contain all the generated CVs and other data, in its internal
"per-application" directories.
beachpatrol_profile (required)beachpatrol_profile = "job-hunter"
beachpatrol browser profile holding the logged-in LinkedIn session.
beachpatrol_browser (default "chromium")beachpatrol_browser = "chromium"
beachpatrol browser to drive.
titles (required, list of strings)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 (optional, list of strings)workplace_types = ["on-site", "hybrid", "remote"]
Appended to the same search OR query. Possible values are: on-site,
hybrid, remote.
geo (optional list of strings)geo = ["new-york-city", "san-francisco-bay-area"]
Geo targets for the query, referred to BY NAME through the [geo_ids] map.
[geo_ids] (optional table, name = digits-only id)[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 (optional list, can be empty)reject_employment_type = ["Internship"]
Employment types rejected at enrichment. Possible values are: Full-time,
Part-time, Contract, Temporary, Internship.
presence_locations (optional list, can be empty)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.
[reject_title] (optional table, label = Python regex)[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>'.
profile/*.md and the Main CVThe 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 generate tailored CVs. Referred by either
SHOBR_MAIN_CV_PATH or <cv_toolchain_dir>/resume.md.
profile/user-detail.mdWork history and proficiencies in more detail than the CV.
profile/fit-criteria.mdWhat makes a lead worth pursuing, in your own words.
profile/deal-breakers.mdVeto rules (if found to match, it produces a score of 1, meaning that the
lead is discarded).
profile/resume-guide.mdYour own rules and suggestions on how to tailor your main CV to a particular application. It might include formatting rules.
profile/cover-guide.mdGuide 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 # main resume (unless pointed elsewhere by SHOBR_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/
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
$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
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.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 human signals possible).next and friends are one-per-invocation). You are free
to automate it to your heart's content via cron jobs, systemd timers, or
even your phone-controlled AI swarm mining crypto on Hetzner datacenters.shobr next do at most one scraping, exactly for this). Keep volumes
human.profiles/ info) goes to third parties.
screen-llm and tailor send your resume, profile docs, and job postings
to whichever LLM provider any-llm-sdk is pointed at. That is your name,
work history, and location scoping on someone else's servers. Prefer
less-evil providers, or use local models.beachpatrol.beachpatrol would be the most direct comparison here.MIT
1 commits
Python
92.7%
HTML
4.5%
JavaScript
2.8%
The stealthiest, UNIX-iest, ethical Job Search Automator
Python
2
1 commits
updated Sep 28, 2026
The stealthiest, UNIX-iest, ethical Job Search Automator, with a Hacker-in-the-Loop approach.
https://github.com/user-attachments/assets/47336c4f-1ca8-40e4-869d-5496bf7fae53
In 2026's job market, there are many job application automation tools, some of them FOSS. This one's mine, and relies on these tools:
beachpatrol for browser
automation of your own daily-driver browser, androffume for resume files
management.
beachpatrol to drive
your existing, authenticated browser. To LinkedIn, you are just a normal
user clicking around.faaah.unittest, no extra
framework.beachpatrol RequirementThe 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.
Then, in order:
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
symlinking them into the expected folder). Fill the config in.beachpatrol profile which is logged into LinkedIn.
Put that beachpatrol profile name in config.toml on the
beachpatrol_profile key.any-llm-sdk reads provider keys
from env (OPENAI_API_KEY, SHOBR_AI_MODEL, and OPENAI_BASE_URL).
Naturally, 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).SHOBR_MAIN_CV_PATH (or see next step).tailor step, shobr will offer to clone
the latest roffume
release into ~/shobr-resumes (or point cv_toolchain_dir in
config.toml at an existing checkout). This folder will keep track of
all your resume variation inputs (markdown) and outputs (PDFs).<cv_toolchain_dir>/resume.md
(SHOBR_MAIN_CV_PATH overrides).SHOBR breaks the job search process into 5 pipeline stages.
You can run:
shobr status
shobr next
Example shobr status output:
$ 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
Scrapes the LinkedIn job search results based on your config.toml keywords
and locations, running them through a basic regex pre-filter.
shobr discovered
shobr discover
beachpatrol to search and scrape leads.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.
shobr enriched
shobr enrich-next
shobr enrich <posting_id>
Scores enriched jobs against your personal Markdown profile and deal-breakers.
shobr screen-llm-next
shobr screen-llm-all
shobr screen-next
$EDITOR or --score/--reason.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.
shobr tailor-next
shobr tailored
Local Kanban-style tracking for your applications.
shobr track <posting_id> <status> [--note TEXT]
applied, interviewing, offer, rejected,
etc.).shobr tracked
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
config.tomlcv_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. The CV toolchain directory will
ultimately contain all the generated CVs and other data, in its internal
"per-application" directories.
beachpatrol_profile (required)beachpatrol_profile = "job-hunter"
beachpatrol browser profile holding the logged-in LinkedIn session.
beachpatrol_browser (default "chromium")beachpatrol_browser = "chromium"
beachpatrol browser to drive.
titles (required, list of strings)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 (optional, list of strings)workplace_types = ["on-site", "hybrid", "remote"]
Appended to the same search OR query. Possible values are: on-site,
hybrid, remote.
geo (optional list of strings)geo = ["new-york-city", "san-francisco-bay-area"]
Geo targets for the query, referred to BY NAME through the [geo_ids] map.
[geo_ids] (optional table, name = digits-only id)[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 (optional list, can be empty)reject_employment_type = ["Internship"]
Employment types rejected at enrichment. Possible values are: Full-time,
Part-time, Contract, Temporary, Internship.
presence_locations (optional list, can be empty)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.
[reject_title] (optional table, label = Python regex)[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>'.
profile/*.md and the Main CVThe 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 generate tailored CVs. Referred by either
SHOBR_MAIN_CV_PATH or <cv_toolchain_dir>/resume.md.
profile/user-detail.mdWork history and proficiencies in more detail than the CV.
profile/fit-criteria.mdWhat makes a lead worth pursuing, in your own words.
profile/deal-breakers.mdVeto rules (if found to match, it produces a score of 1, meaning that the
lead is discarded).
profile/resume-guide.mdYour own rules and suggestions on how to tailor your main CV to a particular application. It might include formatting rules.
profile/cover-guide.mdGuide 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 # main resume (unless pointed elsewhere by SHOBR_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/
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
$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
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.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 human signals possible).next and friends are one-per-invocation). You are free
to automate it to your heart's content via cron jobs, systemd timers, or
even your phone-controlled AI swarm mining crypto on Hetzner datacenters.shobr next do at most one scraping, exactly for this). Keep volumes
human.profiles/ info) goes to third parties.
screen-llm and tailor send your resume, profile docs, and job postings
to whichever LLM provider any-llm-sdk is pointed at. That is your name,
work history, and location scoping on someone else's servers. Prefer
less-evil providers, or use local models.beachpatrol.beachpatrol would be the most direct comparison here.MIT
1 commits
Python
92.7%
HTML
4.5%
JavaScript
2.8%