A remote job search generates far more noise than signal: alert digests, recruiter spam, reposts of the same role, US-only listings that read "Remote." shrike ingests that stream, kills what doesn't fit with a reason attached, and tracks the rest to outcome — so every role is either dropped fast or followed up on, never lost in an inbox.
Named for the butcher-bird: it strikes fast and keeps a visible larder of every catch, returning until each one is finished.
Most job trackers are passive — a spreadsheet you update by hand. shrike is an active filter first and a tracker second: it rejects roles that don't match your stack, region, or comp floor before they ever reach a list, collapses reposts of the same job into one entry, and won't let a reply silently close an application unless the classifier is actually confident. Everything runs locally against one SQLite file — no server, no account, no telemetry.
- Ingest job mail from Gmail (read-only OAuth) and Work At A Startup dumps
- Filter hard with a configurable profile: junk titles, off-stack roles, region locks, compensation floors
- Triage the survivors in a keyboard TUI (shortlist / ignore / snooze)
- Track applications with due follow-ups; replies auto-classify (interview / assignment / offer / rejection) with guardrails
- Everything local: one SQLite file, secrets in the macOS keychain, no server, no telemetry
$ shrike scan --verbose
14 new, 6 excluded, 2 dupes, 0 suppressed, 3 synced (1 rejected, 1 interview), 0 unmatched
excluded: TalentBridge — Virtual Assistant (banned-title:virtual assistant)
excluded: Meridian Labs — Senior Frontend Engineer (region-locked:US)
excluded: Forgepoint — React Native Developer (off-profile-discipline:react native)
excluded: Anchorstone — Fullstack Engineer ($22/hr) (cheap-hourly-rate:$22/hr)
Every drop carries the exact rule that fired — nothing gets excluded on a hunch, and every reason is greppable back to the config or filter code that produced it.
Remote engineers running a high-volume search across Gmail alerts, LinkedIn digests, YC/Work At A Startup, and direct recruiter mail — particularly contractors and B2B/EOR candidates who need region and comp filtering a generic job board doesn't offer, and who'd rather own their pipeline data than hand it to a SaaS.
With Bun ≥ 1.1:
git clone https://github.com/xantorres/shrike && cd shrike
bun install && bun link
Or grab a compiled binary from Releases (macOS arm64/x64, Linux x64) — no runtime required.
shrike init # scaffold ~/.config/shrike/config.json
shrike auth # authorize Gmail (readonly), store refresh token in keychain
shrike scan # ingest job mail, filter, sync replies
shrike triage # keyboard triage of new opportunities
shrike apply 12 # record an application for opportunity 12
shrike next # what's due today
Works without Gmail too: shrike add --company X --role Y tracks anything
manually.
shrike init scaffold the config file
shrike scan [--verbose] fetch gmail, ingest, filter, sync replies
shrike add [url] --company <c> --role <r> [--location --rate --engagement --dossier]
shrike import <path.md> [--due-in 7] import a markdown pipeline table
shrike triage interactive; --json lists pending
shrike decide <opp-id> <verdict> ignore | shortlist | apply_intent | snooze
shrike apply <opp-id> [--channel --cv --next --due --on]
shrike list [--all] applications, active or all
shrike update <app-id> [--stage --status --next --due --note]
shrike next due and overdue actions
shrike show <app|opp> <id> full record with events and decisions
shrike search <query> fuzzy match across companies/roles/aliases
shrike note <opp|app|contact> <id> "text"
shrike alias add|list <opp-id> alternate company names for matching
shrike intents apply-intent decisions not yet applied
shrike export [--out <path>] one-way markdown snapshot
shrike waas ingest Work At A Startup JSON from stdin
shrike apply-queue [--out path] click-to-apply HTML page of pending jobs
shrike discover [--titles --locations --exclude --ats --since 30]
ATS X-ray searches + freelance launcher
shrike auth (re-)authorize Gmail OAuth
Every command accepts --json. The database lives at
~/.local/share/shrike/shrike.db (override with SHRIKE_DB).
~/.config/shrike/config.json (override with SHRIKE_CONFIG); see
config.example.json for the full shape. Every section
is optional — a check whose section is absent is off.
{
"gmail": {
"clientId": "<oauth-client-id>.apps.googleusercontent.com",
"clientSecretRef": "keychain:shrike.gmail.client",
"refreshTokenRef": "keychain:shrike.gmail.refresh"
},
"filter": {
"profile": {},
"region": { "eligibleCodes": ["PT"] },
"comp": { "annualFloorK": 60, "monthlyFloorK": 5, "hourlyFloor": 40 }
},
"discover": {
"titles": ["Frontend Engineer", "React Developer"],
"locations": ["Remote", "Portugal", "EU"]
}
}
filter.profile— stack filtering.{}enables the built-in example profile (React/TypeScript web); setonProfileSignals/offProfileSignalsto describe your own stack. Absent = no stack filtering.filter.region— region filtering for remote candidates.eligibleCodesis your home country; roles locked to other single countries, US-onsite listings, and US-work-authorization requirements get dropped. Broad-region listings (EU/EMEA/worldwide) are kept. Absent = no region filtering.filter.comp— drop listings whose stated pay is under your floor (annual K, monthly K, hourly). Absent = no comp filtering.filter.bannedTitles/filter.bannedLocations— replace the built-in junk-title and onsite-location lists.gmail.clientSecretRef— secret refs arekeychain:<service>,env:<VAR>, orliteral:<value>. Keychain refs readsecurity find-generic-password -a $USER -s <service> -w.
- Create an OAuth client (type "Desktop app") in Google Cloud Console with the Gmail API enabled, and paste its client id into the config.
- Store the client secret:
security add-generic-password -U -a "$USER" -s shrike.gmail.client -w - Run
shrike auth— it opens the consent screen, catches the loopback callback, and stores the refresh token in your keychain. Scope isgmail.readonly; shrike never modifies your mail.
Refresh tokens for OAuth clients still in "Testing" publishing status expire after 7 days; publish the client to "In production" to stop the weekly re-auth.
Order per candidate row: banned titles → banned/region-locked locations →
US-work-authorization body phrases → region lock (Remote (XX) country sets;
pan-EU sets with ≥4 EU/EEA members are kept) → stack profile → engineering
role check → comp floors. The first hit drops the row and is recorded as the
exclusion reason (shrike scan --verbose shows them all).
Replies never close an application on a hunch: rejection detection requires
explicit rejection context, positive kinds win ties, mail older than your last
manual update is ignored, and ghosted applications are only ever revived by
you (shrike update <id> --status active --next "..." --due ...).
Run a daily scan on macOS with the launchd template:
cp docs/ops/shrike.scan.plist ~/Library/LaunchAgents/com.example.shrike.scan.plist
launchctl load ~/Library/LaunchAgents/com.example.shrike.scan.plist
(Edit the two REPLACE-ME paths in the plist first.) Logs go to
/tmp/shrike-scan.log and /tmp/shrike-scan.err.
bun install
bun test # ~380 tests, in-memory SQLite, no network
bun run typecheck
bun run lint
bun run build:bin # compile a standalone binary to dist/shrike
Architecture in one paragraph: core/ is pure logic (filtering, matching,
reply classification, fingerprint dedupe), db/ is a per-table repository
layer over bun:sqlite with snapshot-plus-migrations schema management,
sources/ are adapters (Gmail with injected fetch, Work At A Startup),
services/ compose those into use-cases (ingest, expiry, application
changes), and cli/ is a thin command layer — the interactive triage TUI is
a pure reducer with an IO shell. All external input (config, Gmail API
responses, stdin payloads) is Zod-validated at the boundary. More in
docs/design.md.