Skip to main content
interninbox

Find internships from your terminal. Zero API keys, nothing leaves your machine.

PyPI Python CI License: MIT No API keys

$ interninbox scan --new-only

COMPANY   TITLE                                   LOCATIONS       POSTED      URL
stripe    Software Engineering Intern (Summer)    New York, NY    2026-08-01  https://stripe.com/jobs/...
linear    Product Engineering Intern              Remote          2026-07-28  https://jobs.ashbyhq.com/linear/...
plaid     Data Science Intern                     San Francisco   -           https://jobs.lever.co/plaid/...

3 internships across 3 companies

List your target companies once, then get every matching internship from their public job boards in one command. interninbox reads the documented public board APIs of Greenhouse, Lever, and Ashby (the same endpoints each company's own careers page calls). It can also read the official USAJOBS API for federal Pathways internships.

Why interninbox

  • 🔒 Private by design. No accounts, no API keys, no LLMs, no telemetry. The only things ever written are your config and a local state file.
  • 📍 Search by location. A country, a US state, or a city, with smart aliases: "California" finds boards that wrote "CA", and vice versa.
  • 🎯 Search by role. Nine curated presets (software, cybersecurity, finance, and more) or your own whole-word keywords.
  • 🏢 100+ curated companies. Big and small, every slug live-verified. Scan your own list, or sweep a whole tier of the registry.
  • 🧭 First-run wizard. No config? Answer three questions, get results, and optionally save them for next time.
  • 📬 A personal feed. --new-only shows just what appeared since your last scan; run it on a schedule and it becomes your morning internship digest.
  • 🤝 Polite by construction. Sequential, rate-limited requests, an honest User-Agent, documented public APIs only. No scraping.

Install

pipx install interninbox      # recommended: isolated, on your PATH
uv tool install interninbox   # or with uv

Requires Python 3.11+. Installing straight from git works too (pipx install git+https://github.com/hiratinspace/interninbox), or run from a checkout with uv sync && uv run interninbox --help.

Updating

Already have an older version? Update it the same way you installed it:

pipx upgrade interninbox            # if you used pipx
uv tool upgrade interninbox         # if you used uv
pip install --upgrade interninbox   # if you used pip

Then check with interninbox --version. Upgrades are safe: your interninbox.toml keeps working and the state file migrates itself. If an upgrade insists you are already current but the version looks old, force it with the installer's --force (pipx / uv tool) or --force-reinstall (pip).

Quickstart

Install, then just run:

interninbox scan

On a fresh terminal with no config, scan opens a short wizard. It asks where you want to work, which role types, and which companies (your own list, or a tier of the registry with a rough scan-time estimate), scans immediately, and offers to save your answers to interninbox.toml for next time. Blank answers mean "no preference." The wizard only appears on a real terminal with no config; cron jobs and pipes are never interrupted (pass --interactive to force it).

Prefer to set things up by hand?

interninbox init          # 1. write a starter interninbox.toml here
interninbox companies     # 2. print the curated registry to copy from
interninbox scan          # 3. scan every configured company

Edit interninbox.toml between steps 2 and 3, then re-run interninbox scan whenever you want fresh results. Add --new-only to see only what's new since your last scan.

Commands

Command What it does
interninbox scan Scan every configured company and print matching internships
interninbox init Write a starter interninbox.toml (refuses to overwrite)
interninbox companies Print the curated registry as ready-to-paste ats:slug entries, with each company's size and tags
interninbox roles Print the role presets and the exact keywords each expands to
interninbox --version Print the version

scan flags

Flag Effect
--config PATH Use a config other than ./interninbox.toml
--json Emit machine-readable JSON instead of the table
--markdown Emit a Markdown table (paste it anywhere)
--new-only Show only listings not seen by a previous scan
--state PATH Use a state file other than the one derived from the config name
--interactive Ask location/role/company questions before scanning (automatic on a terminal when no config exists). With an existing config, the answers apply to that run only unless you save them: a one-shot override of locations, roles, and registry that leaves everything else untouched
--quiet, -q Suppress the banner and per-company progress lines

An interactive scan opens with the wordmark ("intern" in white, "inbox" in blue), then prints per-company progress:

    _       __                      _       __
   (_)___  / /____  _________      (_)___  / /_  ____  _  __
  / / __ \/ __/ _ \/ ___/ __ \    / / __ \/ __ \/ __ \| |/_/
 / / / / / /_/  __/ /  / / / /   / / / / / /_/ / /_/ />  <
/_/_/ /_/\__/\___/_/  /_/ /_/   /_/_/ /_/_.___/\____/_/|_|

  > find internships. in the terminal.

It goes to stderr (never stdout), so piped --json / --markdown output stays clean. It appears only on a real terminal, honors NO_COLOR, uses a theme-proof 256-color blue, and vanishes under pipes, redirects, and cron. Pass --quiet to silence it (and the progress lines) anywhere.

Exit codes: 0 on success (a single company that fails prints a one-line warning and never aborts the scan); 1 when the config is invalid or every company failed.

Configuration

interninbox init writes this file; every key is explained inline.

# Target companies as "ats:slug".
companies = [
    "greenhouse:stripe",   # job-boards.greenhouse.io/<slug>
    "lever:plaid",         # jobs.lever.co/<slug>
    "ashby:linear",        # jobs.ashbyhq.com/<slug>
]

# Also sweep the bundled curated registry: "none" (default), "top" (~50
# well-known boards), "all", "large", or "startups". Unioned with `companies`.
registry = "none"

[filters]
# Extra title keywords that count as an internship signal, on top of the
# built-in one (intern, internship, co-op, summer analyst, apprentice, ...).
include_keywords = []

# Named role presets that narrow to a field. Run `interninbox roles` to list
# them. Their keywords merge into match_keywords. Example: roles = ["cybersecurity"].
roles = []

# Drop any listing whose title contains one of these (case-insensitive).
exclude_keywords = ["mechanical"]

# Keep only listings whose location contains one of these as a whole word
# (case-insensitive). Empty = keep every location.
locations = ["New York", "Remote"]

# When true (the default), remote listings always pass the locations filter.
remote_ok = true

# Optional: federal Pathways internships via the official USAJOBS API.
[usajobs]
enabled = true
email = "you@example.com"        # the email your API key is registered under
keywords = ["software"]
api_key_env = "USAJOBS_API_KEY"  # environment variable holding your key
Key Type Default Meaning
companies list of "ats:slug" required unless registry/[usajobs] is set Boards to scan; ats is greenhouse, lever, or ashby
registry "none", "top", "all", "large", "startups" "none" Also scan the curated registry; unioned with companies (duplicates removed)
filters.include_keywords list of strings [] Extra title keywords OR-ed with the built-in internship signal (broadens)
filters.match_keywords list of strings [] Whole-word title keywords required on top of the signal (narrows)
filters.roles list of strings [] Named role presets whose keywords merge into match_keywords
filters.exclude_keywords list of strings [] Title substrings that drop a listing
filters.locations list of strings [] Whole-word location terms to keep; empty keeps everything
filters.remote_ok bool true Whether remote listings bypass the locations filter
usajobs.enabled bool false Turn the USAJOBS adapter on
usajobs.email string none The email your USAJOBS key is registered under
usajobs.keywords list of strings [] Extra keyword filter for the USAJOBS query
usajobs.api_key_env string "USAJOBS_API_KEY" Environment variable holding your key

A commented copy ships as interninbox.example.toml.

Finding a company's slug

Open a company's careers page and read the URL of an actual job listing:

You see Add to your config
job-boards.greenhouse.io/acme/... "greenhouse:acme"
jobs.lever.co/acme/... "lever:acme"
jobs.ashbyhq.com/acme/... "ashby:acme"

If a scan reports HTTP 404 from <host>: check the slug exists, the slug is wrong or the company changed ATS providers. interninbox companies lists the full registry of known-good entries to start from.

How matching works

All matching is local, deterministic heuristics that are fast, free, and predictable:

  1. Internship signal. Word-boundary regexes on the title (intern, internship, co-op, summer analyst, apprentice, student trainee, and friends), OR any of your include_keywords. Word boundaries matter: "International Program Manager" and "Internal Tools Engineer" do not match.
  2. match_keywords / roles requirement. If set, the title must also contain one of them as a whole word. This narrows ("internship AND security"); include_keywords broadens.
  3. Staff-role exclusion. Roles about interns rather than for them (recruiter, program manager) and seniority markers (Senior, Staff, II/III) are dropped.
  4. Your filters. exclude_keywords, then locations / remote_ok.

A listing with no stated location is dropped when locations is set (there is nothing to match against). Leave locations = [] to keep such listings.

Location matching is whole-word and case-insensitive: "NY" matches "Albany, NY" but not "Sunnyvale, CA". Common US-state and country forms are expanded for you, so "California" also matches "CA", and "NYC""New York", "UK""United Kingdom", "US"/"USA""United States". Two safety rules keep the short forms honest:

  • A full state name expands to its comma-anchored code ("Oregon"", OR"), never the bare code, so "Oregon" matches "Portland, OR" but never the word or in "Remote in USA or Canada" (the same anchor protects IN, ME, OK, HI, and the pronoun-safe US).
  • "LA" deliberately means Louisiana only; for Los Angeles, spell it out.

Role presets

Narrow to a whole field with a named preset instead of hand-listing keywords:

[filters]
roles = ["cybersecurity"]   # keeps only security internships

Run interninbox roles to see every preset and the exact whole-word keywords it expands to: software, data, cybersecurity, finance, business, marketing, design, product, and hardware. A preset's keywords merge into match_keywords (both mean "titles I want to see"), so roles = ["software"] keeps titles containing "software", "backend", "platform", and friends. Nothing is magic: the command prints the keywords, and an unknown role name fails with the list of valid ones.

The company registry

interninbox bundles a curated registry of 100+ internship-hiring companies across Greenhouse, Lever, and Ashby: a mix of large public companies and startups, each tagged by industry. Every slug was live-verified against its public board API when the registry was authored (scripts/verify_registry.py); companies do migrate ATSes, so a slug that later goes stale degrades gracefully (one warning line) rather than crashing a scan.

Sweep a tier without listing companies by hand:

registry = "top"   # ~50 of the most-recognized boards
Tier What it scans
"top" ~50 of the most-recognized companies
"all" the whole registry
"large" only the large / public companies
"startups" only the startups

The tier is unioned with any companies you list (duplicates removed). Big sweeps are slow on purpose: polite pacing floors same-host requests at 500 ms, so a scan of 20+ boards prints an honest estimate up front (e.g. scanning 103 boards, roughly ~2 min).

Contributing an entry: add a RegistryCompany(ats, slug, name, size, tags) row to src/interninbox/registry.py, then run scripts/verify_registry.py. It must report a live PASS (HTTP 200) before the entry ships. Never commit an unverified slug.

--new-only and the state file

Every scan records what it saw in a small state file next to your config. With --new-only, only listings absent from that file are shown, so "new" always means "since my last scan", whether or not earlier scans used the flag. A listing counts as seen once it has been fetched, even if your filters hid it, so loosening a filter later won't flood --new-only with old posts.

  • First scan: everything is new. Missing or corrupt state file: everything counts as new (one warning, never a crash).
  • Each config gets its own state file (interninbox.toml.interninbox-state.json, work.toml.interninbox-state.work.json), so two configs in one directory don't share state. Override with --state PATH.
  • The file stores only a listing key and the date it was last seen (no URLs); entries not seen for a year are pruned, so it never grows without bound.
  • init writes only the TOML; if your config lives in a git repo, add .interninbox-state.json to your .gitignore yourself.

Run it on a schedule (cron, launchd, a shell alias) and --new-only becomes a personal internship feed.

USAJOBS (optional)

Federal Pathways internships come from the official USAJOBS Search API, which needs a free key:

  1. Request one at https://developer.usajobs.gov/apirequest/.
  2. Export it: export USAJOBS_API_KEY=....
  3. Set [usajobs] enabled = true and email = "...".

Per USAJOBS's documented contract, requests must carry the registered email as the User-Agent; interninbox sends exactly that, for that host only. If [usajobs] is enabled but the key variable is unset, the scan skips it with an info line and carries on.

How a scan works

flowchart LR
    A[interninbox.toml] --> B["Fetcher<br/>polite HTTP, one per scan"]
    B --> C1[Greenhouse boards API]
    B --> C2[Lever postings API]
    B --> C3[Ashby posting API]
    B --> C4["USAJOBS API<br/>(optional)"]
    C1 & C2 & C3 & C4 --> D["Internship signal<br/>+ staff-role exclusion"]
    D --> E["Your filters<br/>roles, keywords, locations"]
    E --> F["State diff<br/>(--new-only)"]
    F --> G[Table / JSON / Markdown]

Politeness is enforced in one place (src/interninbox/fetch.py) that every adapter goes through, so it is not a setting you can forget: sequential requests, ≥500 ms between same-host calls, a 15-second timeout, at most one retry (on transient failures only), and an honest User-Agent. Only documented public APIs are used, with no HTML scraping and nothing behind a login.

Scope, honestly

This tool does one-shot local scans. That is its whole job, done politely and fast. It deliberately does not verify a listing is still live, deduplicate reposts across boards, watch continuously, or apply for you.

Title-only matching also misses some real internships: bare "Trainee", titles like "Software Engineer (Intern) II" (the seniority filter wins), and languages beyond the built-in German/French patterns. include_keywords can widen the net. Continuous verification, curation, and instant alerts are what the hosted Interninbox product (coming soon) does. This CLI is the honest local version: you run it, you own your data, and nothing phones home.

FAQ

Why only Greenhouse, Lever, and Ashby? They expose documented public board APIs designed for exactly this. More sources may come; PRs welcome if the source has a documented public API.

Does it store or send my data anywhere? No. The only writes are your config and the local state file. There is no telemetry of any kind.

A company I added returns 404. The slug is wrong or the company changed ATS providers. See Finding a company's slug.

Can it email or notify me? Not built in. Pipe --json into whatever you like, or run it on a schedule with --new-only and a mail hook.

Development

uv sync
uv run pytest        # the full offline suite (MockTransport + synthetic fixtures)
uv run ruff check .

Every test is offline, with no recorded third-party data, ever (provenance note). Source lives in src/interninbox/ (adapters, filters, fetcher, registry, wizard, CLI). Support is best-effort via GitHub issues; see CONTRIBUTING.md.

License

MIT © 2026 Interninbox contributors

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

interninbox-0.2.1.tar.gz (1.2 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

interninbox-0.2.1-py3-none-any.whl (44.7 kB view details)

Uploaded Python 3

File details

Details for the file interninbox-0.2.1.tar.gz.

File metadata

  • Download URL: interninbox-0.2.1.tar.gz
  • Upload date:
  • Size: 1.2 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for interninbox-0.2.1.tar.gz
Algorithm Hash digest
SHA256 ff0dd3a9f45d423185da8d834931b2816e07d9541fbb7b3b2de46a986e46ec85
MD5 c8a3ba3b873eac08afbd3eaf73d741d1
BLAKE2b-256 7e3114f6a2d2200a8e58faa328e4752f4ea36d19d2d9c5453def3e09d474385f

See more details on using hashes here.

Provenance

The following attestation bundles were made for interninbox-0.2.1.tar.gz:

Publisher: release.yml on hiratinspace/interninbox

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file interninbox-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: interninbox-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 44.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for interninbox-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 68adc022d1d4270b39a48f92b26c84f129fba37ff8d4c648644949e0ca1fee2b
MD5 0e859aaa13162999db3386d55b0bb403
BLAKE2b-256 86b9198a2b9a4beaa4df905f4ec7da38781f2a39362fac06e9dbae2db037ce62

See more details on using hashes here.

Provenance

The following attestation bundles were made for interninbox-0.2.1-py3-none-any.whl:

Publisher: release.yml on hiratinspace/interninbox

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page