Made by: Heba Alazzeh
Checks a set of tech-internship and full-time new-grad sources every hour, skips anything it's already posted, filters out defense-industry employers, and drops a concise alert into Slack with role, company, location, posting date, and a direct apply link.
- All job alerts — internships and full-time — post to one channel, each message labeled [Internship] or [Full-Time] so they're easy to tell apart in a single feed.
(Set in config.py as SLACK_CHANNEL, overridable via env var.)
Default sources (edit in config.py):
- Internships: SimplifyJobs Summer2026-Internships, SimplifyJobs Summer2027-Internships, vanshb03 Summer2026-Internships (off-season list), RemoteOK filtered to "intern" titles
- Full-time: SimplifyJobs New-Grad-Positions, RemoteOK filtered to non-"intern" titles
These are actively-maintained, scrape-friendly public lists rather than scraping LinkedIn/Indeed directly, which is more reliable and avoids ToS issues. You can add more sources — see "Adding a source" below.
Any listing whose company name matches the blocklist in config.py
(DEFENSE_COMPANY_BLOCKLIST) is dropped entirely — never posted at all.
Currently blocked: Raytheon/RTX, Lockheed Martin, Northrop Grumman,
Boeing, General Dynamics, L3Harris, BAE Systems, Leonardo, Thales,
Anduril, Palantir, Shield AI, Saronic, Skydio, Epirus, Firestorm Labs,
Helsing, Rebellion Defense, Govini, Rocket Lab, and Apex Space.
Matching is a case-insensitive substring check on the company name, so
it's intentionally broad. A couple of names are generic or dual-use
(e.g. "Boeing" also does commercial aviation, "Leonardo" is a common
name, "Rocket Lab"/"Apex Space" do civil space work too) — that's a
deliberate trade-off toward not letting anything defense-related slip
through, at the cost of occasionally also filtering an unrelated listing
that happens to share the name. To extend the list without touching
code, set DEFENSE_COMPANY_BLOCKLIST_EXTRA as a comma-separated env var
(e.g. "kratos,leidos") — it adds to, rather than replaces, the
defaults.
This repo is designed to be safe to make public — the token is never stored in code, only in GitHub Actions secrets. Before publishing:
SLACK_BOT_TOKENmust never be committed. It only ever lives in the repo's Settings → Secrets and variables → Actions → Secrets (encrypted, not visible even to you after saving) or in a local.envfile — which.gitignorealready excludes..env.exampleis safe to commit (no real values);.envis not and is gitignored.seen_listings.jsononly contains job listing IDs and a timestamp — no secrets, safe to be public and to get committed back by the workflow.- If a token ever does leak (e.g. pasted somewhere by accident), revoke
it immediately in the Slack app's OAuth & Permissions page
(reinstall to get a new one) — a leaked bot token only allows posting
messages as this bot (
chat:writescope), not account takeover, but it should still be rotated right away.
All tunable settings live in config.py / environment variables:
| Setting | Purpose | Default |
|---|---|---|
SLACK_BOT_TOKEN |
Bot token (required unless DRY_RUN) | — |
DRY_RUN |
If true, log what would post instead of calling Slack; no token needed |
false |
SLACK_CHANNEL |
Channel ID all alerts post to | C0BPUEWSEM6 |
MAX_LISTING_AGE_DAYS |
Ignore listings older than this | 14 |
ROLE_INCLUDE_KEYWORDS |
Comma-separated; only post if role title matches one | (none — allow all) |
ROLE_EXCLUDE_KEYWORDS |
Comma-separated; skip if role title matches one | (none) |
DEFENSE_COMPANY_BLOCKLIST_EXTRA |
Comma-separated; extra companies to block, added to the built-in list | (none) |
SEEN_STORE_PATH |
Where dedupe state is saved | seen_listings.json |
Example: only software/data roles, skip anything requiring an advanced degree:
export ROLE_INCLUDE_KEYWORDS="software,swe,backend,frontend,full-stack,data,ml,machine learning"
export ROLE_EXCLUDE_KEYWORDS="phd,mba"Every listing gets a stable ID (the source's own ID when available,
otherwise a hash of company+role+link). seen_listings.json tracks every
ID the bot has ever fetched — not just ones it posted — so a listing that
gets filtered out today (e.g. too old, wrong keyword) won't be
re-evaluated and possibly posted later once it "ages into" being new.
Only truly new IDs trigger a Slack message.
Each alert is its own Slack message, labeled with its job type since both post to the same channel:
🧑🎓 Internship Software Engineer Intern @ Acme Corp 📍 San Francisco, CA 📅 Posted Aug 05, 2026 Apply here Source: SimplifyJobs Summer2027-Internships
💼 Full-Time Software Engineer @ Stripe 📍 San Francisco, CA 📅 Posted Aug 05, 2026 Apply here Source: SimplifyJobs New-Grad-Positions
Most internship-list GitHub repos that follow the SimplifyJobs
listings.json format work out of the box — just add a new entry to
SOURCES in config.py with "type": "github_json" and the raw file URL.
For a different shape of source (an RSS feed, a different JSON API, a
company careers page with a public API), write a fetch_<type>(source)
function in sources.py that returns the normalized listing dict shape
documented at the top of that file, then register it in FETCHERS.
- Scraping LinkedIn, Handshake, or Indeed directly isn't included here — those sites actively block automated scraping and it would violate their Terms of Service. The GitHub-list + RemoteOK approach avoids that while still surfacing the same roles (Simplify's own bot scrapes company career pages hourly and feeds these lists).
- Slack's API rate limits chat.postMessage to roughly 1 message/second
per channel;
slack_notifier.post_allpaces requests accordingly. - If a source's format changes upstream, that source will log an error and be skipped for that run — it won't crash the whole job.