This script lists job openings from GeekHunter vagas, filters them by keywords and remote work, and optionally applies to each job (either by clicking “Tenho interesse nessa vaga” or by filling and submitting the “Candidatar para a vaga” form).
- What it does
- Prerequisites
- Cookies (authentication)
- How to run
- Environment variables
- Filter configuration
- Apply flows
- Form config (for “Candidatar” jobs)
- State file (avoid re-applying)
- File layout
- Tests
- Troubleshooting
- Security and ignored files
-
List jobs
Opens the vagas listing in a headless browser (with your cookies), fetches one or more pages, and parses each job card. For each job it extracts:- URL (job detail page)
- Title (when present in the card)
- Remote (
trueif the card contains “Remoto”)
-
Filter
Keeps only jobs that:- Match at least one keyword in title or description (e.g. Full Stack, Backend, TypeScript, Node, React)
- Are remote when “remote only” is enabled
- Respects a maximum number of jobs per run (e.g. 10)
-
Dry-run or apply
- Dry-run: Prints the filtered job URLs. Does not open job pages or click anything. Does not use the “applied” state file.
- Apply: For each filtered job (excluding already-applied URLs):
- Opens the job page
- If “Tenho interesse nessa vaga” is visible → clicks it
- Else if “Candidatar para a vaga” and a form are visible → fills the form (name, LinkedIn, CV file, terms) and submits
- Else → skips and logs the reason
- Waits 2 seconds between jobs and records each applied URL in the state file
- Node.js (v18+ recommended)
- npm (to install dependencies and run scripts)
- Playwright (installed as a project dependency; browsers are downloaded on first use)
From the repository root:
npm installNo separate install step is needed for the GeekHunter script; it uses the project’s existing Playwright dependency.
The script needs your GeekHunter session cookies so the site sees you as logged in. Without them, the listing may show a login page and apply will fail.
A JSON file containing an array of cookie objects. Each object should have at least:
name(string)value(string)domain(e.g.".geekhunter.com.br"or"www.geekhunter.com.br")path(e.g."/")
Optional but useful: expires, httpOnly, secure, sameSite.
Example (minimal):
[
{
"name": "remember_candidate_token",
"value": "YOUR_TOKEN_VALUE",
"domain": "www.geekhunter.com.br",
"path": "/"
},
{
"name": "_geekhunter_session",
"value": "YOUR_SESSION_VALUE",
"domain": "www.geekhunter.com.br",
"path": "/"
}
]In practice you need at least the session-related cookies (e.g. remember_candidate_token, _geekhunter_session, auth_token, XSRF-TOKEN, user_type) for the logged-in listing and apply to work.
- Default: In the repository root, name the file
geekhunter-cookies.json. - Custom path: Set the env var
GEEKHUNTER_COOKIESto the path (relative to the current working directory or absolute). Example:GEEKHUNTER_COOKIES=./my-cookies.json.
- Log in to GeekHunter in your normal browser.
- Open DevTools (F12) → Application (Chrome) or Storage (Firefox) → Cookies →
https://www.geekhunter.com.br. - Copy the relevant cookies (name, value, domain, path) into a JSON array and save as
geekhunter-cookies.jsonin the repo root (or the path you set inGEEKHUNTER_COOKIES).
Important: This file is in .gitignore. Do not commit it; it contains session data.
All commands are run from the repository root (JamDev0).
Only lists and filters; prints URLs. Does not open job pages or apply. Cookies are optional (listing may show login if missing).
npm run geekhunter -- --dry-runLists, filters, then applies to each job (interest button or form). Requires cookies. Skips URLs already in the state file.
npm run geekhunternode scripts/geekhunter/run.mjs --dry-run
node scripts/geekhunter/run.mjs| Variable | Description | Default |
|---|---|---|
GEEKHUNTER_COOKIES |
Path to the JSON cookie file | geekhunter-cookies.json (in cwd) |
GEEKHUNTER_APPLIED_FILE |
Path to the “applied URLs” state file | geekhunter-applied.json (in cwd) |
GEEKHUNTER_MAX_PAGES |
Max listing pages to fetch (1–77) | 77 (all pages) |
Examples:
GEEKHUNTER_MAX_PAGES=10 npm run geekhunter -- --dry-run
GEEKHUNTER_COOKIES=./cookies/gh.json npm run geekhunter
GEEKHUNTER_APPLIED_FILE=./data/applied.json npm run geekhunterThe filter is defined in scripts/geekhunter/run.mjs in DEFAULT_FILTER_CONFIG:
- keywords — Array of strings; job title/description must contain at least one (case-insensitive).
- remoteOnly — If
true, only jobs withremote === true(card contains “Remoto”) are kept. - maxJobs — Maximum number of jobs to consider after keyword and remote filter (e.g. 10).
To change behavior, edit that object in run.mjs (or later refactor to a config.json / env and load it there).
GeekHunter job pages can show two different flows:
-
“Tenho interesse nessa vaga”
One click to signal interest. No form. The script clicks the button and records the URL as applied. -
“Candidatar para a vaga”
A form with Nome completo, Email (often pre-filled), LinkedIn, file upload (CV), and terms checkbox. The script fills what it can fromformConfig, checks the terms box, and clicks the button.
If neither is present (e.g. already applied or different page layout), the job is skipped and the reason is logged.
When the script submits the “Candidatar para a vaga” form, it can fill:
- Nome completo — from
formConfig.fullName - LinkedIn — from
formConfig.linkedInUrl - CV file — from
formConfig.cvPath(absolute or relative path to a file)
Currently run.mjs passes an empty object {} to applyToJob, so those fields are left blank unless you change the code. To personalize:
- Open
scripts/geekhunter/run.mjs. - Find the call:
applyToJob(page, job.url, {}). - Replace with something like:
You can also read these from env (e.g.
applyToJob(page, job.url, { fullName: 'Your Full Name', linkedInUrl: 'https://linkedin.com/in/your-profile', cvPath: resolve(process.cwd(), 'resumes', 'your-cv.pdf'), });
process.env.GEEKHUNTER_FULL_NAME) if you prefer.
When you run apply (without --dry-run), the script keeps a list of job URLs it has already applied to. Before applying, it loads this list and skips those URLs. After each successful apply, it appends the new URL and writes the file.
- Default path:
geekhunter-applied.jsonin the current working directory (repo root when you runnpm run geekhunter). - Override: Set
GEEKHUNTER_APPLIED_FILEto another path.
Format:
{
"applied": [
"https://www.geekhunter.com.br/company-a/jobs/slug-one",
"https://www.geekhunter.com.br/company-b/jobs/slug-two"
]
}- Dry-run does not read or write this file.
- The file is in
.gitignore; do not commit it if it contains your data.
scripts/geekhunter/
├── README.md # This file
├── run.mjs # CLI entry: list → filter → apply
├── list-vagas.mjs # Fetches listing pages, parses with parse-listing
├── parse-listing.mjs # Pure: HTML → job list (url, title, remote)
├── filter.mjs # Pure: filter jobs by keywords, remote, maxJobs
├── apply-decision.mjs # Pure: page snapshot → action (click_interest | submit_form | skip)
├── apply.mjs # Per-job: navigate, decide, click or submit form
└── __tests__/
├── filter.test.mjs
├── parse-listing.test.mjs
└── apply-decision.test.mjs
- Pure modules (filter, parse-listing, apply-decision): no browser; easy to unit test.
- Browser modules (list-vagas, apply): take a Playwright
page; used byrun.mjs, which launches the browser and loads cookies.
Unit tests cover the pure logic: filter, parse-listing, and apply-decision.
From the repository root:
npm run test -- --runOr to watch:
npm run testTests are under scripts/geekhunter/__tests__/. No browser is started; no live requests to GeekHunter.
-
On GeekHunter (source of truth)
Log in at geekhunter.com.br, open your profile/dashboard, and check for any section that lists vagas you’ve shown interest in or candidaturas. That list is the only definitive proof that the platform registered your action. -
Spot-check a few job pages
Open 2–3 URLs fromgeekhunter-applied.jsonin your browser while logged in. If the apply really went through, the page will usually show something like “Você já demonstrou interesse” or the “Tenho interesse” / “Candidatar” button will be gone or disabled. If you still see the normal apply button, that one may not have been recorded. -
What the script does
After each click (“Tenho interesse” or “Candidatar”), the script waits ~3 seconds and checks whether the apply button disappeared or success-style text appeared. When that happens it logsApplied: <url> (verified on page). If you see(verified on page)for a run, that application was confirmed by the page state. If you don’t see it, the click was performed but the success check didn’t match (e.g. different wording on the site); you can still confirm manually with steps 1–2.
-
“Cookies required for apply”
You ran without--dry-runand the script could not load cookies. Creategeekhunter-cookies.json(or setGEEKHUNTER_COOKIES) with a valid cookie array. -
Dry-run prints “(none matched filter)”
The listing was fetched but no job passed the filter (keywords and/or remote). CheckDEFAULT_FILTER_CONFIGinrun.mjs. If the site’s HTML changed, title/remote might not be parsed; checkparse-listing.mjsand the structure of the listing page. -
Apply always skips with “no interest or candidatar form on page”
The job page may have a different layout, or your profile may not be approved (some jobs only show “Tenho interesse” for approved profiles). Try opening the same URL in your browser and see which buttons/forms appear. -
Playwright “browser not found”
Runnpx playwright install chromiumfrom the repo root (or install the browser(s) you use). -
Rate limiting / blocking
The script waits 2 seconds between applies. If you still get blocked, increase the delay inrun.mjs(DELAY_BETWEEN_JOBS_MS) or reducemaxJobs/GEEKHUNTER_MAX_PAGES.
The following are in .gitignore and should not be committed:
geekhunter-cookies.json— Session cookies; sensitive.geekhunter-applied.json— List of URLs you applied to; optional privacy.playwright-state.json— Playwright storage state if you add it later.
Keep cookie and applied files only on your machine or in a secure, private store.