Privacy architecture
Matching runs on your machine, and your profile does not cross the wire. You can check that in the engine itself: the plugin ships the bundled CLI, and that bundle is public in the plugin mirror. The website is different — signing in, posting and claiming work, and analytics run on our servers, and the sections below say what each of them collects.
What never leaves your machine
Everything that knows you. The CLI downloads a public job index and derives your skill profile from your public GitHub signal and, optionally, local telemetry you explicitly enable. The matcher, your profile, and your match results never leave your machine.
An automated test suite (the "zero-egress" scan) enforces that the code paths which touch your profile have no network calls — every allowed network touchpoint in the CLI is individually allowlisted and justified, and CI fails if anyone adds one.
What ever crosses the wire
Each one explicit, below.
The authoritative list is not this page — it is
apps/cli/test/zero-egress.test.js,
which fails CI if anyone adds a network call the allowlist does not name. Where that test
and this page disagree, the test is what the software actually does, and this page has the
bug.
That wording is deliberate. This section used to open "these, and only these", and the
sentence was wrong twice — the pulse prompt and the feedback command were both missing
while it read as a guarantee. An absolute maintained by hand degrades every time someone
adds a request and forgets the docs.
So a second test, apps/web/__tests__/privacy-covers-egress.test.ts, now fails the build
if a request is added to that allowlist without someone recording which item below tells
you about it. It checks coverage, not wording: whether a paragraph describes a request
honestly still takes a person, and no test will catch that for us.
A consented lead. If you choose to share your profile with a recruiter or accept an intro, that specific, consented payload is sent — after a typed yes.
Anonymous click analytics. Job links route through a redirect that counts clicks in aggregate. No profile data rides along.
Opt-in connection features. Publishing to the developer directory, requesting intros, and chat are all double-opt-in. Chat is end-to-end encrypted (X25519 + XChaCha20-Poly1305) — the server stores ciphertext it cannot read. Connecting GitHub turns two display settings on by default — listing the individual merged pull requests behind your totals, and building the dossier that describes them. Neither publishes anything on its own: your profile page stays unpublished until you publish it. Turning PR-link display on enables repository names both on your published public résumé and on the private poster decision card when you claim someone's posting. Both cover work that is already public on GitHub, both are on the settings tab to switch off, and turning one off keeps it off — including across a later reconnect. Keeping a GitHub token is not part of that default; it is a separate explicit choice, below, because it is the only one of these that stores a credential.
A stored GitHub token — explicit,
read:useronly. Off until you turn on "live checks" or accept the dashboard's "show my full record" prompt. It lets us read your merged pull requests on your own rate limit.read:useris the only scope Terminalhire ever requests, so the token reads public data and nothing else — it cannot see a private repository. It is encrypted at rest, scoped to your session, and turning the setting off clears it from every session you have.Opt-in click attribution. Off by default. If you turn on "Share job click activity" in the dashboard, and only while you are signed in, a job link you open is also recorded against your GitHub login so you can see your own recent clicks. Anyone signed out, or opted out, stays on the anonymous path above — nothing is attributed. Turning it back off deletes the history it collected, not just stops new rows.
Anonymous install/init counters. Downloads of the install script and completed
terminalhire initruns are counted as anonymous daily aggregates — a date, an optional campaign tag from the link you followed, and a count. The install-script download additionally records a coarse client kind (CLI pipe vs. browser view) — never the raw user-agent string. No account, IP address, or device information is stored or associated; the tag travels only in the URL and carries nothing about you.The "Proof of Work" newsletter — double opt-in. If you sign up, we store your email address — and, only if you choose to add it after confirming, your GitHub username. Nothing else that identifies you (optionally a campaign tag from the link you followed — never an IP address, user-agent, or cookie). We send one confirmation email; you are only a subscriber once you click to confirm. The optional GitHub username is used only to pick issues that match your stack, never linked to your résumé or profile, and deleted with your email. If you add it, we also read your public GitHub following list so we can occasionally pick issues from maintainers you already follow ("From your orbit") — that list is used only to choose picks, never shown to anyone, and deleted with your email. An unconfirmed signup is auto-deleted after 7 days. Every issue carries a one-click unsubscribe that hard-deletes your email — not archived, not suppression-listed, deleted. Delivery is handled by our email processor (Resend); the list itself lives in our own database, disjoint from any profile or résumé data.
"Check against my GitHub" on a project page — public data, nothing stored. On a public project page you can, while signed in, ask whether your work fits its open issues. The check is computed server-side from your public GitHub only — the same public proof-of-work signal that backs your résumé (which domains you have merged pull requests into), matched against that project's issues. It reads nothing from your local machine or profile — your local profile never reaches the server, here or anywhere. The result is computed, returned, and discarded: we write no record keyed to you and that project, and the answer is never shown to anyone but you. If your public credential can't be verified at that moment (GitHub is rate-limiting us, say), you get an honest "can't verify right now" — never a fabricated score.
Contribution-dossier synthesis — envelope-derived facts only, off by default. The Contribution Dossier can optionally use an LLM (Anthropic) to phrase a few narrative sections and a résumé bullet from your merged work. What the model receives is only the identity-free dossier envelope and deterministic counts derived from it — pseudonym labels, enums, sizes, timestamps. No third-party free text is ever sent to the provider: not a comment body, not a review comment, not an issue description — nothing written by another person. Third-party maintainers appear to the model only as stable pseudonyms, never as a name, login, or email. Every generated sentence must cite the specific envelope fact that supports it, a second pass discards any sentence those facts don't back, and anything that slips through those checks is dropped — so the output can't say more than your structured record already proves. The feature is off unless explicitly enabled by the operator, and it runs only for a signed-in developer who has opted the dossier in — which, since connecting GitHub defaults that setting on (see "Opt-in connection features" above), means anyone who has connected and not switched it off. It also needs a stored GitHub token, which is never defaulted on, so no build runs on a connect alone. Deleting your account, or a maintainer's Article-17 request, deletes the synthesized text alongside the record it was derived from.
Poster notification emails — your own public address, nothing stored. If you post work, we email you when a developer claims it, when a patch arrives, when the checks finish, and when work is waiting on your verdict. We send to the public email on your GitHub profile, read at the moment we send and not stored — there is no notification address on file to leak or to erase. No public email on your profile means no email, and nothing else changes. The emails carry no developer's name or identity: they say what happened and link to the row on your own dashboard, which grants nothing to anyone not already signed in as you. Delivery is handled by our email processor (Resend).
A release recall check — the one request that sends nothing.
terminalhire runexecutes a repository's own build and test commands on your machine. Before it does, it asks whether the version you are running has been withdrawn — so a release we discover a containment defect in can be stopped without waiting for you to upgrade. The request carries no query, no body, no cookie, no token and nothing about you, your machine, your profile or the repository. This request does not tell us which version you run: the server returns the full list of withdrawn versions and the comparison happens on your machine. (The pulse prompt below does send your CLI version, if you answer it — so this is a statement about this request, not about the product.) If the check cannot reach us,runproceeds — being offline is not a reason to stop you working. Once your version has been shown to be withdrawn, that answer is remembered locally and keeps applying offline. Once a version has been withdrawn and then cleared again, a reachable check deletes that local record — so an un-recalled build does not keep refusing to run offline.The pulse prompt — one digit, only if you answer it. If you have opted into the beta programme and are signed in, the CLI occasionally asks how things are going and offers a 0–3 rating. Pressing anything else, including Enter, sends nothing — the request is never made. If you do answer, we send exactly four things: that single digit, your CLI version, your operating system name (e.g.
darwin,linux,win32), and your session cookie so it is attributed to your account. No free text is ever sent — there is no comment box, and no field of this request can carry one. Theterminalhire feedbackcommand below is a different thing on the same endpoint, and it is a comment box.terminalhire feedback— free text, by design, and only after you read it back. This one exists to carry your words, so it sends what you typed: a category, whichever of the free-text answers you filled in (what you were trying to do, expected versus actual, what almost made you quit, whether you will keep it installed), any 1–5 ratings you gave, plus your CLI version and operating system name. It is sent with your session cookie, so it is attributed to your account. Before anything is sent, the CLI prints the exact bytes of the request and requires you to typeyes— anything else cancels with zero network calls. Nothing is gathered in the background; every field is one you typed at a prompt.Joining or leaving the beta programme.
terminalhire betasends your choice about whether to be listed publicly, with your session cookie. Re-running it to check your status sends a fixed, empty body carrying nothing you wrote. Without a linked session it sends nothing at all.Telling a poster why you're passing — one of five fixed answers, stored without your name. On the
bountieslist,d<number>sends the posting's id and one value from a fixed list: the price is low, the scope is unclear, there's no way to check the work, not my stack, or something else. There is no free-text box, so there is nothing to send that you did not pick from a menu. Your session cookie authenticates it and then the server throws the identity away: the stored row holds a keyed hash, different on every posting, so your answers cannot be joined into a history of what you turned down. Posters see counts only, and the breakdown on a posting stays hidden until at least three people have answered. The first time, the CLI prints the exact bytes and waits for you to typeyes. This is a separate act from dismissing a job — your local job statuses still never leave your machine, and passing never sends them.terminalhire decline --forgetclears the local consent only: the next time you pass on a posting, the CLI asks again before sending anything. It does not delete answers you already sent, and there is no endpoint that does. That is deliberate, not an omission. The stored row is a keyed hash with no login column and a different value on every posting — it is not attributable to you, cannot be looked up by your name, and cannot be assembled into a history — so it is not personal data we hold about you, and it sits outside the erasure machinery that covers things like intro contact details. What you would be asking us to delete, we cannot find, because we made sure we never could.A note you write to a poster — your words, kept for as long as the message is.
terminalhire claim notedrafts a note to the poster of work you have claimed. The draft is stored so you can read the exact bytes back in your browser before you send them, and it is stored twice once you do: as the message itself, and as the draft that proves what you approved. Both age out together. A draft you never send is deleted after 7 days. A draft you did send keeps its row, because that row is the only link between your approval and the message it produced, but the text inside it is emptied on the same 180-day clock that redacts the message. What survives is a hash, a timestamp and the id of the message, none of which recovers a word you wrote. Deleting your account removes your drafts, sent and unsent, on the same path that clears the rest of your record.Deleting your account — one button, and here is exactly what it does. The control sits under "danger zone" in your dashboard settings, beside the one that ends this browser's session. It asks once to confirm, and there is no undo.
Deleted outright: your connection row, which holds your name, avatar, follower counts, the seniority and skill tags we inferred, your cached résumé and every consent flag on it. Your dossier — the scored contribution records and the synthesized bullets built from them. Note drafts, sent and unsent. Your attributed job-click history. Your trajectory attestation. Push tokens, web-push subscriptions and the delete capability the CLI holds for your pushed claims, so nothing can reach you or act as you afterwards. The claims you pushed from the CLI. Your published profile — the name, addresses, languages and tags you synced. Your beta membership, your place on the waitlist and the feedback you sent us. The record of which repositories you asked us to look at, which we keep only to rate-limit the asking. Intros you requested, and the conversations attached to them.
Every MCP connector you created, so a token minted for one stops working. If you connected an editor or an agent to post work through us, that connection carries a token of its own, and a token is not a session — it does not expire when you sign out, and until now it kept working after the account it acted for was gone. Any approval you had started but not yet finished goes too, so nothing left half-done can turn into a working token afterwards. Anything still holding one will simply be refused.
Every message you sent, on every conversation. Not only the ones on intros you started. A conversation someone else opened with you keeps their request, so the messages on it do not go with it — they are deleted by name instead. Your chat key, your presence row, your read positions, and any block either of you placed go with them: a block on a login that names nobody protects nobody, and all it still records is that the two of you had dealings.
And every session row of your account — the browser you clicked in, your other devices, and any
terminalhire linkbridge — so you are signed out everywhere, not just here.Kept, with your name removed: claims, postings and the events behind them. Those are the record of paid work, and someone else's money moved through them. The rows stay so the accounting still adds up; your login and your numeric GitHub id come out of them, and the free text a poster wrote about you is emptied. Deleting the rows outright is not the safer option — it once stranded a posting so that it could never be withdrawn.
Kept, still naming you, and here is why: an intro someone else requested with you as the target keeps their row and their record of whom they asked. Your contact details and the id we would mail you at are cleared from it. That request is theirs, not yours, and deleting it would erase a person who did not ask to be erased. If you have ever asked us to anonymize you as a repo maintainer, the marker recording that survives too, because it is the thing that stops your name coming back the next time we read that repository.
Not touched, and each for its own reason. A poster's saved card and a developer's payout account are financial records held with Stripe, not ours to drop on the way past. A poster's confirmed contact address stays on file — it is what a posting is reachable at, and it is separate from the notification address in item 10, which we genuinely never store. Your permission to post work, including a revocation if one was ever recorded, stays too: deleting that row would quietly restore an access somebody removed on purpose. Postings you passed on are stored as a keyed hash with no login column, so there is nothing there to find (item 15). And a newsletter subscription is keyed on your email address rather than your account, with its own one-click unsubscribe that hard-deletes it (item 7).
When we refuse instead of trying. Three cases, and in all of them nothing is deleted and you stay signed in. If your session does not carry your numeric GitHub id, we stop: a login on its own is not proof of who you are, since GitHub names can be given up and taken by someone else, and half an erasure reported as a whole one is worse than none. If another live session is signed in under your login with a different id — which is what a name that has changed hands looks like from here — we stop for the same reason, rather than delete a stranger's rows. And if an erasure for your account is already running, the second request waits rather than racing the first. Sign in again, or try again in a few minutes.
If any part of it fails, the whole thing reports a failure and leaves you signed in, so you can try again. We would rather tell you it did not finish than sign you out of the only control that can finish it.
Two limits worth stating plainly. Sessions are matched on your login exactly as it is spelled, so if you renamed your GitHub account by changing only the letter case and a
terminalhire linksession predates that rename, it can outlive the erasure; runterminalhire logouton that machine, or let it expire. And a claim of yours that we release on the way past is not announced to the parts of the product that track whether a posting is still being worked on, so a poster may see it as live for a while. Neither touches what is deleted; both are about how quickly the rest of the system notices.
The surfaces inherit the rules
- The MCP server's match tools answer from the local cache with zero network calls. Claim preview uses governed public reads; claim record writes only the local ledger and cannot start, push, submit, or open a PR. If you configured an Anthropic API key, record applies the same semantic policy-doc reader as the CLI. Message content and consent actions remain excluded (details).
- The editor extension is a read-only renderer of the local cache with no network access at all (details).
- The spinner/statusline surfaces in Claude Code render content computed from a profile that never leaves your machine; supply comes from the public index, not from your data.
- The website counts page views with Vercel Web Analytics — cookieless and anonymous, aggregate numbers only. Your profile never touches the site, so there is nothing for a page view to be tied to.
- The website also records product analytics with PostHog: named steps on the two paths through the site — posting work, and claiming it — so we can see where people get stuck. What each event carries is a step name, a step number, and a handful of tags (which refusal a publish hit, whether a posting is paid, whether a claim was new). While you are signed in, events are tied to your public GitHub login; signed out, each event gets a throwaway id and no profile is built. We never send an email address, a token, a session id, repository contents, a patch, or the text of a posting, and PostHog is loaded with autocapture off — so it records the steps we named, not every click you make. Your locally-matched profile never touches the site, and nothing here changes that.
- The website also sends PostHog masked session recordings, error reports and
server logs. A recording captures the structure of a page and your interactions
with it — clicks, scrolling, navigation — and every piece of text and every form
field is replaced with a placeholder in your browser before the recording leaves it.
A recording can show that you stopped on the payment step and clicked the same button
three times; it cannot show your email address, your posting text, or anything you
typed. It does carry the address of each page, as the analytics events from your
browser do, and some addresses name a public repository (
/projects/<owner>/<repo>). Before it is sent, the token is taken out of every share, claim or draft link (the part after/c/,/t/,/dashboard/work/or/founder/confirm/), and so is the value of every query parameter except which dashboard tab is open. Error reports carry the error type, a stack trace, and the message with addresses and token-shaped strings redacted first. Server logs are lines our own code writes about what it did — never request bodies.
The point
A credential you can trust has to be derived from real work and verifiable — and a matcher you can trust has to prove it isn't quietly a data business. Keeping the profile on your machine is what makes both claims checkable rather than promised.