npm.io
0.42.11 • Published 8h agoCLI

terminalhire

Licence
MIT
Version
0.42.11
Deps
0
Size
14.3 MB
Vulns
0
Weekly
0
Stars
2
Install scriptsThis package runs scripts during installation (preinstall/install/postinstall)

terminalhire — your code is your résumé

Job + bounty matching in the terminal, and a Proof of Work credential earned from PRs that maintainers who aren't you merged into established repos — checkable on GitHub, impossible to self-report. Claim a scoped bounty and get paid when the poster accepts it.

Domain: terminalhire.com

Install

# one-off — no global install needed
npx terminalhire help

# global install
npm i -g terminalhire

# wire up the Claude Code statusLine nudge (optional, recommended)
node $(npm root -g)/terminalhire/install.js

The installer prints a full v3.1 disclosure, asks for explicit "yes", then offers GitHub sign-in (recommended), and finally writes one key to ~/.claude/settings.json. Install is NOT consent to share your profile — that is a separate, per-role decision.

Uninstall

node $(npm root -g)/terminalhire/install.js --uninstall
terminalhire logout             # clear GitHub token (if connected)
terminalhire profile --delete   # also wipe local profile
rm -rf ~/.terminalhire          # wipe everything

To disable only the spinner job surface without uninstalling:

terminalhire spinner --off

Commands

terminalhire login              # sign in with GitHub — enriches profile instantly (recommended)
terminalhire logout             # clear stored GitHub token from ~/.terminalhire/github-token.enc

terminalhire jobs               # fetch index, browse ranked roles matched to your profile
terminalhire jobs --limit 20    # show top 20 results (default: 10)
terminalhire jobs --remote-only # filter to remote roles only
terminalhire jobs --all         # show all matches above zero score

terminalhire bounties           # day-sized paid tasks you can knock out today
terminalhire bounties --priced  # only bounties with a known $ amount
terminalhire contribute         # credential-building issues matched to your stack — merges grow your Proof of Work

terminalhire claim record <id|issueUrl>  # claim a bounty locally + print the executor brief
terminalhire claim list --active         # list your active claims + accepted-PR rate
terminalhire claim status <id>           # poll source PR merge state (updates the metric)

terminalhire trajectory         # trajectory from your local Claude Code corpus
terminalhire trajectory --export  # write a derived score + Markdown to ~/.terminalhire/
terminalhire trajectory --inward  # also show private rework/recovery (never exported)

terminalhire devs               # opted-in peers & founders — matched on your machine
terminalhire intro <login>      # request a double-opt-in intro (typed-yes consent)
terminalhire inbox              # unread chats, connections & pending intros in one place
terminalhire mcp --print-config # match + claim-record MCP config for AI hosts

terminalhire profile --show     # inspect your encrypted local profile (incl. GitHub fields)
terminalhire profile --edit     # set displayName, contactEmail, prefs
terminalhire profile --delete   # wipe profile and encryption key from disk

GitHub is the primary enrichment signal for Terminalhire. Signing in solves the cold-start problem — you get accurate matches on the very first run instead of waiting for tag accumulation.

terminalhire login

This runs the GitHub OAuth Device Flow:

  1. A code like XXXX-XXXX and a URL (https://github.com/login/device) are displayed.
  2. You open the URL, enter the code, and click "Authorize".
  3. Terminalhire polls until you authorize, then stores the token encrypted at ~/.terminalhire/github-token.enc.
  4. Your public GitHub profile is fetched, processed, and merged into your local profile.
What GitHub enriches
Signal Source
Skill tags Repo languages + repo topics, filtered through vocabulary.normalize()
Seniority estimate Account age × (repos + followers) per documented thresholds
Display name user.name if not already set in profile
Contact email user.email if public and not already set in profile
Public scope guarantee

Scope requested: read:user — public profile + public repos only.

Private-repo scopes are NEVER requested. This means:

  • No employer-IP risk from private repos.
  • No code, secrets, or private file access.
  • Only what is already publicly visible on your GitHub profile.
Data residency
  • Token: encrypted at ~/.terminalhire/github-token.enc (AES-256-GCM, same scheme as local profile).
  • GitHub data stays on your machine — it enriches your local profile.
  • GitHub fields cross the wire only in a consented LeadPayload when you type "yes".
  • GitHub data is never sent silently.
Seniority thresholds
junior : age < 2 yr  OR  (repos < 5 AND followers < 10)
mid    : age 2–5 yr  AND repos >= 5
senior : age 5–9 yr  AND (repos >= 20 OR followers >= 100)
staff  : age >= 9 yr AND (repos >= 40 OR followers >= 500)

When signals conflict → conservative (lower) estimate. Unresolvable → undefined.

Mock mode (dev / CI)

No GitHub OAuth App registered yet? Set TERMINALHIRE_GITHUB_MOCK=1:

TERMINALHIRE_GITHUB_MOCK=1 terminalhire login

Uses the fixture at fixtures/github-sample.json instead of a live OAuth flow. No client ID needed. Safe for local development and CI.

Registering a GitHub OAuth App
  1. Go to github.com → Settings → Developer settings → OAuth Apps → New OAuth App
  2. Set "Authorization callback URL" to http://localhost (not used by device flow)
  3. Enable Device Authorization in the OAuth App settings
  4. Copy "Client ID" → set as GITHUB_CLIENT_ID in your environment
  5. For public (CLI-only) OAuth Apps, the client secret is optional. If you set one, add it as GITHUB_CLIENT_SECRET.
export GITHUB_CLIENT_ID=Iv1.your_app_client_id
# optional:
export GITHUB_CLIENT_SECRET=your_client_secret

Status bar nudge

After terminalhire jobs runs and finds matches, the Claude Code status bar shows a once-per-session discovery nudge:

✦ N roles match your current work — run: terminalhire jobs

The nudge is printed at most once per Claude Code session. It reads only ~/.terminalhire/index-cache.json (a matchCount written by the last terminalhire jobs run). It makes zero network calls.

Spinner job surface

While you work inside Claude Code, terminalhire surfaces your top local job matches directly in the spinner — the ambient status indicator that appears while Claude is thinking.

  • Enabled at install. No additional setup required.
  • Profile never leaves your machine. Match scoring runs against your encrypted local profile. No network call is made for the spinner display.
  • Zero egress. The spinner reads only ~/.terminalhire/index-cache.json (written by the last terminalhire jobs run). Nothing leaves your device.
  • Reversible. Disable at any time without uninstalling:
terminalhire spinner --off

Re-enable with:

terminalhire spinner --on

The spinner surface shows a short ranked summary (e.g. ✦ 3 matches — run: terminalhire jobs). It does not send your profile, fingerprint, or any data to any server.

Architecture (v3.1 hybrid)

Server                              Client (your machine)
──────────────────────────────      ─────────────────────────────────────────
Broad public pool:                  ~/.terminalhire/profile.enc  (AES-256-GCM encrypted)
  Greenhouse + Ashby (ATS)          ~/.terminalhire/github-token.enc  (AES-256-GCM)
  Himalayas + WWR + HN         GET  Fingerprint built from profile
  Coastal buyer-lead roles    ←─── /api/index (anonymous, no dev data)
                                    Local match() from @jpi/core
                                    Ranked list printed to terminal
                                    ↓
                               POST /api/lead  ←── ONLY on explicit "yes"
                                    Named-entity consent per role
                                    (GitHub fields included only if present AND consented)

GitHub (optional enrichment):
  github.com/login/device/code ←── Device flow (read:user scope only)
  api.github.com/users/<login>      Token + data stay on machine
  (public repos, topics, langs)

Zero dev-side egress during matching. The index download is anonymous. GitHub enrichment stays local. The only outbound payload with developer data is a consented LeadPayload, for buyer-lead roles only.

Privacy

What happens locally What crosses the wire
Profile accumulated from personal project sessions GET /api/index — anonymous, no dev data
Closed-vocab skill tags + seniority (encrypted at rest) POST /api/lead — ONLY after explicit per-role "yes" consent
GitHub token + public profile data (encrypted at rest) GitHub fields in lead — ONLY after consent AND GitHub is connected
Employer-repo sessions: language tags only Nothing else
git email domain + remote host (employer detection only)

Employer-repo exclusion (default on): if git config user.email is a corporate domain or git remote points to a non-personal host, only language-level tags (typescript, python, …) accumulate. Fine-grained framework/infra tags are excluded. The flag hasEmployerSessions is recorded locally but never emitted.

Local profile encryption

  • Algorithm: AES-256-GCM via Node built-in crypto (no external deps).
  • Key: stored at ~/.terminalhire/key with 0600 permissions (a plaintext key file). In a compiled build the OS keychain is not used for these stores — the cross-platform plugin ships without node_modules and can't reach one. See docs/adr-002 (keychain promotion is tracked as TERM-9).
  • Profile file: ~/.terminalhire/profile.enc — JSON blob { iv, tag, ciphertext } (all hex-encoded).
  • GitHub token file: ~/.terminalhire/github-token.enc — same format, same key.

Lead payload shape

The exact object sent to /api/lead when you consent (nothing else is sent):

{
  "opportunityId": "coastal:senior-fullstack-001",
  "buyerId": "coastal",
  "buyerLegalName": "Coastal Recruiting LLC",
  "approvedFields": {
    "skillTags": ["typescript", "react", "postgresql"],
    "seniorityBand": "senior",
    "displayName": "Your Name",
    "contactEmail": "you@example.com",
    "note": "Optional note typed at consent time",
    "github": {
      "login": "yourlogin",
      "profileUrl": "https://github.com/yourlogin",
      "topLanguages": ["typescript", "python", "go"],
      "publicRepos": 42
    }
  },
  "consentText": "You are about to share the following information with Coastal Recruiting LLC\n...",
  "createdAt": "2026-06-14T00:00:00.000Z"
}
  • displayName, contactEmail, and note are included only if set.
  • github is included only when: (a) you have run terminalhire login AND (b) you typed "yes" at the consent prompt.
  • skillTags is always the full list from your profile.

The exact text shown before any lead is sent (GitHub profile connected):

You are about to share the following information with Coastal Recruiting LLC
for opportunity: <title> at <company> (<jobId>)

Fields that will be sent:
  • skillTags: ["typescript","react","postgresql"]
  • seniorityBand: "senior"
  • displayName: "Your Name"        ← only if set in profile
  • contactEmail: "you@example.com" ← only if set in profile
  • github.login: "yourlogin"
  • github.profileUrl: "https://github.com/yourlogin"
  • github.topLanguages: ["typescript","python","go"]
  • github.publicRepos: 42

GitHub fields above are public data only (scope: read:user). No private repos.

Nothing else leaves your machine. This action is specific to this role.
Coastal Recruiting LLC will use this to evaluate you for the role.
You can delete your profile at any time with: terminalhire profile --delete

Share your profile with Coastal Recruiting LLC for this role? [y/N]

Without GitHub: the github.* fields and the note about public scope are omitted.

Typing anything other than y or yes aborts with no network call.

Apply modes

applyMode What happens when you select a role
direct Opens the employer's public URL in your terminal. No data sent.
buyer-lead Triggers the named-entity consent flow above. Nothing sent without "yes".

The two modes are never silently conflated.

Zero-egress test (M-5)

node test/zero-egress.test.js

Environment variables

Variable Default Description
TERMINALHIRE_API_URL https://terminalhire.com Base URL for every server call — reads, login/OAuth and background syncs alike (also accepts legacy JPI_API_URL). Allowlisted hosts only — see below
GITHUB_CLIENT_ID (required for real OAuth) GitHub OAuth App client ID (device flow)
GITHUB_CLIENT_SECRET (optional) GitHub OAuth App client secret (public apps omit this)
GITHUB_DEVICE_CLIENT_ID (falls back to GITHUB_CLIENT_ID) Device-flow specific client ID if different
TERMINALHIRE_GITHUB_MOCK 0 Set to 1 to skip real OAuth and use the fixture (dev/CI); also accepts legacy JPI_GITHUB_MOCK
Pointing the CLI at a non-production server

TERMINALHIRE_API_URL is resolved once, in src/api-base.ts, and every call site uses that one answer — so reads and writes can no longer disagree about which environment you are on.

The override is an allowlist, not a free-form URL, because this same base decides where your push token is presented. Only these are accepted:

Host Scheme Extra flag
terminalhire.com, www.terminalhire.com* https
dev.terminalhire.com https
localhost, 127.0.0.1 (any port) http TERMINALHIRE_ALLOW_LOCAL_API=1

* www is rewritten to the apex — its 301 strips the session cookie.

Anything else — an unknown host, the wrong scheme, embedded credentials, a string that will not parse — makes the command refuse to run. It does not quietly fall back to production: a typo'd override that resolved to prod is how a developer ends up pushing claims at the real database while believing they are in a sandbox. It never falls back to the host you named either, so a poisoned shell profile cannot redirect you. Loopback without the opt-in is refused for the same reason: the URL alone must not be enough to make ambient refresh present the persisted prod session to a local listener.

Browser sign-in uses a narrower allowlist. login, link, sync and the claim verification flows accept only https://terminalhire.com and https://dev.terminalhire.com. Loopback OAuth requires both TERMINALHIRE_ALLOW_LOCAL_API=1 and TERMINALHIRE_ALLOW_LOCAL_OAUTH=1. Point at https://dev.terminalhire.com for an end-to-end login against the shared-dev stack.

Note that ~/.terminalhire is keyed per machine, not per origin: the web session, push token and consent markers are shared with production. The credential-bearing commands warn about this on stderr when a non-prod base is active.

When a non-production base is active, every command prints one line to stderr:

[dev → dev.terminalhire.com]

No marker means you are talking to production.

File layout

apps/cli/
  bin/
    jpi.js          — statusLine nudge (once-per-session, zero egress)
    jpi-dispatch.js — 'terminalhire' bin entrypoint, routes subcommands
    jpi-jobs.js     — 'terminalhire jobs': fetch index, local match, consent flow
    jpi-login.js    — 'terminalhire login' / 'terminalhire logout': GitHub device flow + enrichment
    jpi-profile.js  — 'terminalhire profile': show/edit/delete encrypted profile
    refresh.js      — TOMBSTONE (v1 removed; exits non-zero)
  src/
    signal.ts       — local fingerprint extractor (reads dep files + extensions)
    profile.ts      — encrypted profile: read/write/accumulate/delete
    github-auth.ts  — GitHub device flow: token I/O + encrypted storage
  test/
    zero-egress.test.js  — M-5 zero-egress assertion suite
  install.js        — v3.1 installer with GitHub onboarding
  package.json

How data is used: local default vs consented lead

Data Default (local-enrichment) Opt-in (consented lead)
Skill tags Stored encrypted locally, used for local matching only Included in approvedFields.skillTags on "yes"
Seniority Stored encrypted locally Included in approvedFields.seniorityBand on "yes"
Display name Stored encrypted locally (pre-filled from GitHub if public) Included if set and "yes"
Contact email Stored encrypted locally (pre-filled from GitHub public email) Included if set and "yes"
GitHub login / profileUrl Stored in profile.github on terminalhire login Included in approvedFields.github ONLY on "yes"
GitHub topLanguages Used locally for tag inference (closed vocab) Included in approvedFields.github ONLY on "yes"
GitHub token Encrypted at ~/.terminalhire/github-token.enc Never sent
Private repos Never accessed (scope: read:user) N/A

Keywords