Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

internship-agent

Finds internships, scores them against your profile, writes tailored materials, fills the application form, and submits it — with an approval gate in front of the submit button by default.

Built and verified against live Greenhouse, Lever, and Ashby application forms.

Read this first

Two things you should know before pointing this at real employers.

Automated submission violates most portals' terms of service. LinkedIn, Indeed, Workday, and Handshake all prohibit it, and accounts do get flagged. That's why autonomy: review is the default: the agent does everything up to the submit button, and you approve each one with a keypress. Full auto exists, it's one config line, and it's your call — but the risk is real and it's yours.

A bad application is worse than no application. The generator is instructed never to claim anything your profile doesn't support, and lists whatever the posting asked for that you don't have. Field matching refuses to guess: if your school isn't in a dropdown it picks "Other" rather than the nearest lookalike, because "Adams State University" is a different school from yours and that's your name on the form. Read the review screen. It exists for a reason.

What it actually does

discover ──▶ score ──▶ prepare ──▶ [review] ──▶ submit
   │           │          │            │           │
   │           │          │            │           └─ replays the form, verifies
   │           │          │            │              it matches what you approved,
   │           │          │            │              then clicks submit
   │           │          │            └─ one key per application: y / n / skip
   │           │          └─ cover letter + answers, fills the form, screenshots it
   │           └─ cheap keyword rules, then a model pass on the survivors
   └─ GitHub internship lists, Greenhouse/Lever/Ashby APIs, Handshake

Nothing is submitted until you approve it, unless you change autonomy.

Setup

Requires Python 3.11+.

git clone <your-repo-url> internship-agent
cd internship-agent

python -m venv .venv
.venv\Scripts\activate          # Windows;  source .venv/bin/activate elsewhere
pip install -e ".[dev]"
python -m playwright install chromium

cp .env.example .env                              # add your ANTHROPIC_API_KEY
cp config/config.example.yaml config/config.yaml
cp profile/profile.example.yaml profile/profile.yaml
cp /path/to/your/resume.pdf profile/resume/resume.pdf

Fill in profile/profile.yaml, then:

apply doctor

doctor checks the API key, the resume (including whether text can actually be extracted from it), Chromium, and which profile fields are still blank. Get it green before your first real run.

Your resume, profile, config, and database are all gitignored. Nothing personal is committed.

Daily use

apply run          # discover -> score -> prepare
apply review       # approve or reject, one key each
apply submit       # send the approved batch

Or step by step:

Command What it does
apply discover Fetch postings. --source greenhouse to run just one.
apply score Rule pass, then model scoring on survivors.
apply prepare Generate materials, fill forms, screenshot.
apply review The approval gate.
apply submit Submit everything approved. Prompts first; --yes to skip.
apply status Counts, what's waiting on you, what failed and why.
apply show <id> Everything about one application, including its history.
apply reset failed Requeue failures after fixing the cause.

In apply review: y approve, n reject, s skip, o open the screenshot, u open the form in a browser, c show the cover letter, q quit.

Configuration

Everything lives in config/config.yaml. The parts that matter most:

Autonomy

autonomy: review     # review | auto | hybrid
  • review (default) — fills and queues. You approve. Nothing is ever sent without a keypress.
  • auto — submits immediately after filling. No human in the loop.
  • hybrid — auto-submits only when the gate passes, queues everything else:
hybrid_gate:
  min_score: 85
  require_no_unmapped_fields: true   # any field it couldn't map -> you review it
  require_no_freetext: true          # any generated free-text -> you review it

Regardless of mode, the agent refuses to auto-submit a form with an interactive captcha or an in-portal Handshake application.

Matching

match:
  min_rule_score: 40      # below this, never reaches the model (saves money)
  min_score: 60           # below this, skipped
  titles_exclude: [senior, staff, manager, phd]
  drop_if_text_matches: ["security clearance", "must be a us citizen"]

Cost

score_model runs once per candidate posting; generate_model runs once per prepared application. Both default to claude-opus-5. Scoring is the high-volume one — set score_model: claude-sonnet-5 to cut that cost, and note that the rule pass already filters most junk before any model call happens.

Rate limiting

limits:
  max_prepare_per_run: 15
  max_submit_per_run: 10
  delay_between_applications_s: 20   # don't set this to 0
  max_per_company: 3

Sources

Source Notes
github_lists Curated GitHub internship lists. Public, high signal, no scraping risk. Handles both the markdown and HTML table formats those repos use, and reads their 🔒 / 🇺🇸 / 🛂 markers as closed / citizenship-required / no-sponsorship.
greenhouse, lever, ashby Public JSON endpoints, one per company. Add board tokens (the slug in the company's careers URL) to companies. Full job descriptions and the most automatable forms.
handshake Your university portal. Needs a login: run apply login-handshake once and sign in by hand. Best-effort — Handshake changes its DOM often. Applications that submit inside Handshake are review-only.

How form filling works

One JavaScript pass extracts a descriptor for every field on the page — a reload-stable CSS selector, its type, its human label, its options. Python then decides what goes where. That's why the same code handles Greenhouse, Lever, Ashby, and custom forms without per-vendor selector lists.

src/internship_agent/apply/fieldmap.py is the lookup table: a label regex maps to a semantic key, and the key resolves against your profile. It's deliberately boring and readable — when a form asks something new, add a pattern and a branch.

Things it handles because live forms demanded it:

  • react-select comboboxes — clicks open, reads only the options belonging to that combobox, clicks the match. Scoping matters: a page-wide option query once answered "Overall GPA" with "American Samoa".
  • Server-filtered autocompletes — types progressively shorter queries until options appear.
  • Number inputs2027-05 becomes 2027 for a "graduation year" field.
  • GPA bands — places 3.8 into 3.75 - 4.0.
  • Degree synonyms — your "Bachelor of Science" matches their "Bachelor's Degree".
  • EEO questions — selects "Decline to self-identify" unless you filled in demographics.
  • Captchas — detected, and submission is refused with an explanation rather than a silent no-op. Distinguishes the invisible reCAPTCHA v3 badge (harmless) from a real challenge.

Why submit re-fills the form

apply submit doesn't replay saved keystrokes. It re-opens the form, fills it again, and compares the result against what you approved. If anything differs it aborts instead of submitting. Approval means something specific: you approved that set of values, not "whatever this form looks like later".

Running without an API key

The pipeline needs a model for exactly two things: judging how well a posting fits you, and writing the cover letter plus free-text answers. Everything else is HTTP requests and browser automation.

Offline mode hands those two jobs to a headless Claude Code session (claude -p) running on your Claude subscription. No ANTHROPIC_API_KEY, no API call. The handoff is two JSON files in work/:

apply export-work    ->  work/pending.json    (the postings, plus instructions)
claude -p ...        ->  work/completed.json  (scores, cover letters, answers)
apply import-work    ->  straight into the database
Step Needs a model Offline
discover no unchanged
score rule pass no unchanged
score model pass yes skipped; the rule score stands
writing materials yes Claude Code, via import-work
form filling, screenshots, digest no unchanged, but see below
submit no unchanged; re-fills and verifies as always

Turn it on in config.yaml:

offline: true

It is a config setting, not just a flag, on purpose. A form prepared offline has to be re-filled offline at submit time. If submit runs online it adds model-written answers that weren't there at approval, the refill no longer matches what you approved, and every submission aborts with "form changed since you approved it". One setting keeps both halves in sync. --offline / --online override it per command when you need to.

Check the setup with apply doctor — in offline mode it verifies the claude CLI instead of an API key.

Offline fills fewer fields

This is the real cost. Online, when a form asks something your profile doesn't cover, the model answers it. Offline there is no model at fill time, so those questions get flagged instead:

Situation Online Offline
Field matched by fieldmap.py filled from your profile same
Cover letter, why-this-company generated on the spot from imported materials
A novel long-answer question model writes an answer REQUIRED unfilled:
A dropdown with no profile match model picks from real options no matching option:

Anything flagged REQUIRED blocks automatic submission, so it lands in the "need you" half of the digest with its URL. A form that asks nothing unusual fills identically either way; a form with three essay questions does not.

The morning spreadsheet (default)

If you'd rather apply by hand, the daily run can produce a worksheet instead of filling forms. This is the default mode, and it needs no API key and no browser, so it finishes in under a minute instead of half an hour.

apply sheet --limit 20

Writes artifacts/internships-YYYY-MM-DD.xlsx (plus a stable internships-latest.xlsx) and opens it. One row per posting:

Column What
Score, Company, Role, Location, Posted, Source the posting
Link clickable; reads CHECK - ... if the posting looks dead
Why it fits the scoring reasons
Watch out for citizenship/sponsorship notes, and what the posting wants that your profile doesn't cover
Cover letter the full tailored letter, ready to paste
Applied? / Date applied / Notes empty, for you

Because there's no browser, dead postings can't be caught by filling them, so each link is fetched over HTTP first and flagged if it 404s or the page says the role is closed. Postings already written to a sheet aren't offered again; --repeat overrides that. The per-company cap still applies, so one employer can't take the whole sheet.

Run it daily with scripts\daily-sheet.ps1, which does discover -> score --offline -> export-work -> claude -p -> import-work -> sheet.

The daily run (form filling)

This is the other mode: the agent opens each application form and fills it. It needs the browser, and the essay questions ("what project are you most proud of?") need an API key to be answered, so without one a fair number of forms end up blocked on one or two fields. Switch to it with:

powershell -ExecutionPolicy Bypass -File scripts\install-schedule.ps1 -Mode Fill
powershell -ExecutionPolicy Bypass -File scripts\install-schedule.ps1
powershell -ExecutionPolicy Bypass -File scripts\install-schedule.ps1 -At 06:30 -Count 25
powershell -ExecutionPolicy Bypass -File scripts\install-schedule.ps1 -Remove

Start-ScheduledTask   -TaskName InternshipAgent-Daily   # run it now
Get-ScheduledTaskInfo -TaskName InternshipAgent-Daily   # last result, next run

scripts/daily.ps1 runs, in order: discover, score --offline, export-work, claude -p, import-work, prepare --offline, digest. It takes a global mutex so a manual run can't collide with the scheduled one and corrupt the shared browser profile, and it exits non-zero if any stage fails so Task Scheduler reports the truth.

The daily run submits nothing. It stops after filling and writing the digest.

When it actually runs. The task uses an interactive logon token, because filling forms needs the desktop session holding your logged-in browser profile. So it cannot run while you're logged out; a locked screen is fine. It requests WakeToRun, but power plans can veto wake timers — where that happens the run starts when the machine next wakes, which may be hours after the scheduled time.

Path What
artifacts/digest.md the morning summary, overwritten each run
artifacts/logs/daily-YYYY-MM-DD.log one per day
artifacts/screenshots/ the filled form for each application
artifacts/cover-letters/ the letter handed to each form
work/*.json the current handoff, cleared at the start of every run

Your part in the morning

Read artifacts/digest.md, which splits what was filled into "Ready to submit" and "Filled, but need you". Then:

apply submit --all

--all approves everything prepared that can actually be submitted and holds back the rest, printing each with a reason and its URL:

Held back when Why
interactive captcha the agent cannot solve it
a required field is unfilled an incomplete application is worse than none
no fields were filled the posting is almost certainly closed
the resume did not attach the one thing that must not be missing
no submit button found usually a multi-page form
Workday or Handshake needs an account or an in-portal flow

The digest and --all share one predicate, so the digest cannot promise something submit would refuse.

Realistic throughput

"20 prepared" does not mean "20 sent". Every Lever form carries hCaptcha, about a third of postings are Workday, expired postings fill zero fields, and offline mode's flagged questions add to the pile. A run where nothing is auto-submittable is normal and is not a malfunction.

What you actually get is the research, the letters, and the typing done, and a morning spent clicking through leftovers instead of starting from a blank form.

Layout

src/internship_agent/
  cli.py            commands
  offline.py        the export/import handoff that removes the API-key need
  pipeline.py       discover -> score -> prepare -> submit
  scorer.py         rule pass + model pass
  generate.py       cover letters, free-text answers, multiple-choice answers
  review.py         the approval gate
  llm.py            Anthropic wrapper (refusal fallbacks, structured output)
  resume.py         PDF/DOCX text extraction
  db.py             sqlite store
  models.py         Job, Score, FilledForm, statuses
  sources/          github_lists, ats (greenhouse/lever/ashby), handshake
  apply/
    browser.py      persistent Playwright session
    fieldmap.py     label -> semantic key -> your data
    filler.py       form enumeration and filling
    runner.py       prepare / submit, and the re-fill verification
scripts/            daily.ps1, install-schedule.ps1, refresh-resume.ps1
tests/              126 tests, no network required

Tests

pytest -q

All offline. Covers the list parsers (both formats), field classification, option matching, scoring rules, resume text repair, and dedup/status handling. The regression tests worth knowing about are the ones asserting it will not put a lookalike university in a school field, a country in a city field, or a university in a "High School Name" field. Each of those was a real bug caught on a live form.

To smoke-test the filler against a real posting without submitting anything, see apply prepare with browser.headless: false and watch it work.

Limitations

  • Workday postings are discovered but not reliably filled. Workday requires an account per employer and uses a multi-page wizard; those get flagged for manual completion. About a third of what the GitHub lists surface is Workday.
  • Captcha forms cannot be submitted by the agent, only prepared.
  • Handshake selectors drift; treat that source as best-effort.
  • Multi-page forms are filled one page deep. If a form paginates, the review screenshot shows page one and submission will abort rather than guess.
  • Scoring quality depends entirely on how specific your profile.yaml is. Vague profile in, vague tailoring out.

© 2026 Arunachalam Kasi. All rights reserved. Public for review purposes. No license granted for reuse or redistribution.

About

Agent that finds internships, tailors materials, fills applications, and submits them behind a human approval gate.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages