How Backthread handles your code
Backthread turns your repository into an architecture diagram. To do that we have to read your code — but we don't have to keep it. This page is the long version of what we promise at the connect step. Backthread also keeps a decision log — the why behind your coding-agent sessions, captured by the plugin. That path sends us less than the diagram path, but it does send us something, and we spell out exactly what below.
The tone here is deliberately sober. The brand is irreverent elsewhere; this is the trust moment. Every claim below mirrors what's actually in the implementation. If anything reads as overclaim, write to security@backthread.dev and we'll tighten it.
What we keep
For every repository you connect, the analysis path writes only the derived diagram — the modules, the edges between them, and the per-module changelog "why". We never persist your source code, file contents, or git history beyond what's reflected in the derived view.
That is the analysis path alone. The capture plugin and the lesson flow each store their own things, both described below, and the table at the end of this page lists what this product stores about your work — read that rather than this paragraph. Billing records are covered by the Privacy Policy instead; billing in particular is kept for seven years and outlives account deletion.
What an ingest looks like
- A GitHub webhook (push to your tracked branch, or a merged PR) tells our orchestration Worker that a re-sync is due.
- The Worker spawns a fresh, isolated sandbox for the job — a Cloudflare Container running on a Firecracker microVM — and passes in a short-lived installation token scoped only to your repo.
-
Inside the sandbox we
git clone --depth 1your repo and read its structure statically. The extractor is polyglot — TypeScript, Python, Ruby, Elixir, PHP, Kotlin, Dart/Flutter, Swift, Java and Go — and every language is handled either by an install-free parser or by a hand-written scanner. Nonpm install, nopip install, nobundle, no build step: a language's manifest is scanned as text, never evaluated. We then cluster the import/call graph deterministically (Louvain) and call our LLM only to name and narrate what the structure already says — never to author it. - We write the derived diagram and its per-module changelog to our database. Rows are scoped to your account by Postgres row-level security.
- The sandbox is destroyed. The clone and the installation token die with it.
What is changing: extraction on your side
Not live yet. Everything above is what runs today, and it is what runs until this page says otherwise. This section is here so the direction is on the record rather than a surprise, and so you can hold us to it.
The step we most want to remove is step 3 — us cloning your repository at
all. In the mode we are building, extraction runs on your
CI runner. It checks out the code with the short-lived
GITHUB_TOKEN GitHub already issues to every workflow, which we
never see; it runs
@backthread/extractor,
the same MIT-licensed package our own container runs, so the output is
identical; and it posts back only the derived graph. We validate that
payload before anything is stored, because a payload produced outside our
infrastructure is untrusted input.
Stated precisely, because this is the part that matters: your source would
never leave your infrastructure, and the GitHub App would no longer need
contents: read. It would still need metadata and
pull_requests: read, because that is how we know a decision has
actually merged. So the honest ceiling is
"no permission to read your code", and we will not dress that
up as cutting GitHub off altogether, because that would not be true.
What we don't do
-
We don't execute your code. No
npm install, norequire()of repo modules, noeval. The parser reads source files like text and reasons about them statically. - We don't follow symlinks out of the clone. Any symlink whose target resolves outside the clone root is rejected (path-traversal guard).
- We don't accept pathological inputs. The sandbox enforces a per-file size cap, a total-bytes cap (zip-bomb guard), a file-count cap, and CPU + memory + wall-time caps on the job.
-
We don't ask for org-wide permissions. The GitHub
App grants
contents,metadata, andpull_requestsas read-only on the specific repos you pick. No push, no admin, no secrets access. - We don't train any model on your code. Our LLM providers' commercial terms exclude training on API content (Anthropic's Commercial DPA, and for Google the Data Processing Addendum its Gemini API terms name — not the Google Cloud DPA, which we cited by mistake until 2026-08-21 and never operated under); we are not them, and we do not train any model at all.
The decision log (capture path)
Connecting a repo is one path into Backthread. The other is the
capture plugin (npx backthread), which
records the why behind your coding-agent sessions as a decision
log. It has its own, different boundary — and on the default path it is
a weaker promise than "nothing but the derived result
ever leaves your machine," so here it is, stated plainly.
-
When a session ends, the plugin reads the transcript
locally and drops every tool call and tool result —
the parts that carry your source code and command output — and
replaces fenced code blocks in the remaining prose with
[code redacted]. What survives is natural-language conversation only. -
Before that scrub, the plugin also harvests a little
metadata: the repo-relative file paths
the session touched (e.g.
src/auth/session.ts), plus the session's current branch name and HEAD commit SHA (e.g.feat/authanda3f2c1d). These are directory structure and version-control refs — not file contents — and they're sent alongside the redacted transcript so a derived decision can be anchored to the part of your architecture it's about and held until the work it describes actually merges to your main branch. None of it is source: the never-store-source promise still holds. Absolute paths are normalized to repo-relative first, so nothing about your machine's directory layout outside the repo ever leaves. - On the default (server-inference) path, that redacted transcript is sent to our Worker. The Worker re-runs the code scrub server-side as a fail-closed backstop, derives the decisions, and discards the transcript: processed in memory, never stored.
- Only the derived decisions (a short claim + rationale each) are persisted, scoped to your account by the same row-level security as everything else.
So, said out loud: on the capture path a redacted, natural-language-only transcript does leave your machine, together with the repo-relative file paths the session touched and its branch name + commit SHA (metadata — directory structure and VCS refs, not contents) — but never source code, never tool I/O, never file contents. A bring-your-own-key mode (derivation runs against your own LLM key; nothing but the derived decisions leaves the machine) is designed and coming; until it ships, every capture takes the server path above.
You choose which repositories are captured. The decision log is per-repository: a repo is captured only once you connect it to Backthread, and you can pause any connected repo any time from the Repos page in your account — a per-repo Capturing / Paused switch. Pausing is per person: it stops your sessions on that repo; a teammate who hasn't paused keeps capturing their own. For a repo you never connected, or one you've paused, the plugin now makes that call on your machine, before anything is sent: at the end of a session it asks our server a single question — is this repo being captured, for me? — sending only the repository's owner/name, never the transcript and never your code. If the answer is no, the session is skipped and nothing about it leaves your machine.
One honest caveat, in the spirit of the rest of this page: that pre-send check is fail-open. If it can't reach our server — you're offline, or the endpoint is briefly down — the plugin does not silently drop a real capture; it falls back to the older path, sending the redacted transcript so our server enforces the same rule, deriving the decisions in memory and then dropping them, storing nothing (processed in memory, never written to disk or database). So a left-out repo's source stays on your machine whenever the check succeeds — effectively always — with the server-side drop as the backstop for the rare moment it can't.
Separately, every authenticated request the tool makes carries a little
coarse, non-identifying operational metadata about the tool
itself: your backthread and
@backthread/redact versions, which agent invoked it
(Claude Code / Cursor / Codex / Gemini), your OS platform, and your Node
major version. It rides your existing device token (no new identifier) so we
can see what's running in the field — keep older installs working and prompt
stale ones to update. It describes the tool, never your code or your
conversation, and the exact headers are stamped by the open-source
CLI's versionHeaders().
How the plugin signs in (device-token transit)
The capture plugin authenticates with a capture-scoped, revocable device
token — never your account password, never a full account session. When you
run backthread login, that token reaches your machine
end-to-end encrypted, and our servers never see it in the clear:
-
The CLI generates a one-time session id and an ephemeral keypair,
then opens
app.backthread.devin your browser (it also prints the URL so you can open it on any device). There is no127.0.0.1loopback and no localhost page — the browser stays onapp.backthread.devstart to finish. - After you click Authorize, the token is minted and encrypted in your browser to the CLI's public key (ECDH on P-256 → HKDF → AES-256-GCM). Our server only ever receives and stores the ciphertext — it is never able to decrypt the token.
-
The CLI polls for that ciphertext and decrypts it locally with the
private key that never left your machine, then writes it to
~/.backthread/config.json(owner-only,0600).
The token is capture-scoped — it can write decisions for your own repos and nothing else — and you can revoke it any time under Account → Connected devices.
One honest caveat. For a friction-free login we deliberately skip a device-verification code. That leaves a narrow, capture-scoped vector: someone who tricked you into authorizing their login session on your account would get a capture-only token — never your account, never your source, never read access — which you can revoke instantly. We bound it further with a high-entropy session id, a ~10-minute expiry, and one-time delivery. We think that trade is right for the smoothness; if you'd rather we add a verification code, tell us at security@backthread.dev.
Shared accounts
When more than one person shares an account, changes to it are recorded in an audit log — who invited or removed whom, who changed a role, who connected or transferred a repository. It is on every plan; there is no tier to buy for it. It is readable by the account owner and admins, not by members.
Lessons and coverage
Knowledge coverage has two parts, and only one of them involves you answering anything. The first is inferred from your repository's own git and pull-request history — who committed in, and who reviewed, the work that produced each decision. That part needs no participation from anybody and is capped at half: having worked in an area is not evidence of understanding it, so no amount of committing can push past 50%.
The second part is demonstrated, and it is the only thing that goes above half. Backthread asks you a question about a repo you work on — in your coding agent while it is busy, on demand, or in the browser — and what you type is what that part is measured from. The two are combined by taking the larger, never by adding them. Your answers are personal data about you, so here is what we store and how long we keep it.
We keep what you typed. For each question we store your free-text answer, the verdict (got it / not yet / I disagree / bad question) and a short grader note. We are not going to claim we throw the answer away — we keep it, because grading and re-grading need it.
Coverage itself is never stored. There is no coverage score sitting in a column anywhere — neither half of it. Both are derived on read, the inferred half from git history and the demonstrated half from those answers, which is why deleting the answers deletes your demonstrated contribution exactly, with no counter left behind to disagree with them.
Who has worked where is not a secret. Who has worked in an area — committed there, reviewed there — is read from your git history, which your whole team can already read, and it does carry names. That is ownership, and it is shown with names attached.
Three things here are deliberately not private. Recording an answer as a decision publishes what you wrote, and so does disagreeing with one — both are buttons you press, and both say so. On a public repository a recorded answer is readable by anyone, and so is a disagreement against a decision; a disagreement against a graph anomaly stays with account members. The third is quieter: flagging something as a bad question kills it for the whole repo so it never comes back for anybody, and that record includes who flagged it. A kill is a fact about the question, not about the person — but your name is on it, and you should know that before you use it.
Leaving deletes what you demonstrated. When someone is removed from an account (or leaves), their lessons for that account's repos are deleted, taking the questions and answers with them — so the demonstrated half of team coverage drops, honestly, rather than continuing to claim an area is understood by somebody who no longer works there. The inferred half is computed from the participation record we keep, and that record stays, so that part does not vanish — which is correct: it never claimed understanding in the first place, only involvement. It is scoped to that account: coverage of a different team's repos is untouched, because they have not forgotten that codebase. A personal streak survives too; leaving one team has not broken your habit.
What we keep, in one table
| Category | Where | Retained for |
|---|---|---|
| Your source code | Only inside the ephemeral sandbox — never in our database | Destroyed at end of job (≤ 10 minutes) |
| Derived diagram + changelog | Supabase Postgres, EU region | Until you disconnect AND request deletion |
| Redacted capture transcript (no code, no tool I/O) | Worker memory only — never written to disk, queue, or database | Discarded after extraction (seconds) |
| Repo-relative file paths touched (metadata — directory structure, not contents) | Supabase Postgres, EU region — stored on the derived decision as its module anchor | Until you delete the decision or your account |
| Branch name + HEAD commit SHA at capture (metadata — version-control refs, not contents) | Supabase Postgres, EU region — stored on the derived decision so it stays hidden until that work merges to your main branch | Until you delete the decision or your account |
| Coarse tool metadata (backthread + redact version, agent, OS platform, Node major — metadata about the tool, not contents) | Supabase Postgres, EU region — on the usage event + your device record, keyed to your account, never a new identifier | Until account deletion |
| Derived decisions (claim + rationale, plus the GitHub login of whoever captured it and the coding-agent session id) | Supabase Postgres, EU region — the capturing login is shown in the app and in the daily digest | Until you delete them or your account |
| Your lesson answers (the free text you typed) + verdict + grader note | Supabase Postgres, EU region — these rows — the text, the verdict and the note — are readable only by you, by row-level security. Three exceptions, all of them things you choose to do: recording an answer as a decision, disagreeing with a recorded decision, and flagging a bad question. Each publishes what you typed — see the rows below. Telling us the grade was wrong does not publish your note either — it stays on your own row. It is not silent, though: disputing a grade sends an internal alert to our Telegram, carrying the repository’s identifier, the question’s identifier, the verdict you disputed and the length of your note, never its words. See the Telegram entry below | Until you delete your account, or you leave the team that owns the repo |
| An answer you chose to record as a decision (the text you typed) | Supabase Postgres, EU region — it becomes a decision on your team's log, with your name on it. On a public repository it is readable by anyone | Until you delete the decision or your account |
| A disagreement with something we recorded (the note you typed) | Supabase Postgres, EU region — it renders in full on the decision or the graph anomaly it disputes, for your team. On a public repository a note against a decision is readable by anyone; a note against a graph anomaly stays with account members | Until the decision is deleted, or the account is |
| Telling us a grade was wrong (the note you typed) | Supabase Postgres, EU region — kept on your own answer row and readable only by you. It never rewrites the verdict, never touches your coverage, and is never shown to your team. Pressing the button is not silent, though: it sends an internal Telegram alert carrying the repository and question identifiers, the verdict and your note’s length — never its words. Before 2026-08-19 that alert did carry the words; see the Privacy Policy §10 | Until you delete your account, or you leave the team that owns the repo |
| A bad question kill (which question, who flagged it, and the reason you typed) | Supabase Postgres, EU region — visible to the repo, not private to you. It is a fact about the question, not about you | Kept for the repo, so the question never comes back. It outlives your leaving the team, and is deleted with the account that owns the repo |
| Audit log (changes to a shared account) | Supabase Postgres, EU region — readable by the account owner and admins, not by members | Until account deletion |
| Who worked where (your GitHub login, which pull request, author or reviewer, and when) | Supabase Postgres, EU region — taken from the pull requests we already read, and kept, rather than re-read from git each time. It is what the inferred half of coverage is computed from. Visible to your team, as it already is on GitHub | It outlives your leaving the team — it is a fact about the repository, and it goes when the repository is disconnected and deleted |
| Your knowledge coverage | Never stored — there is no score in a column anywhere. Both halves are computed on read: the demonstrated half from your answers, the inferred half from the participation record in the row above | Your demonstrated half goes the moment the answers do |
| Your daily streak | Supabase Postgres, EU region — readable only by you | Until account deletion |
| Your GitHub OAuth identity | Supabase Auth, EU region | Until account deletion |
| GitHub installation token | Worker memory + sandbox env | Job-scoped, destroyed with the sandbox |
| Device token, in transit (login) | End-to-end encrypted in your browser to the CLI's key; our server stores only ciphertext | Deleted on pickup, or at a ~10-minute TTL |
| Device token, at rest (your machine) | ~/.backthread/config.json, owner-only 0600 |
Until you revoke or re-login |
| Per-IP rate-limit hash (signup endpoint) | Cloudflare KV | 10 minutes |
| Webhook delivery / queue dedupe markers | Cloudflare KV | 10 minutes |
Verify, don't trust
The code that runs on your machine is open source — read it,
run it, and confirm it does exactly what this page claims. The
redaction fence
(@backthread/redact) is the exact code behind the capture boundary
above: it drops every tool call and tool result and redacts fenced code before
anything leaves your machine. The same package's sessionPaths helper
is the exact code that harvests the repo-relative file paths described above —
readable in the open-source @backthread/redact source, so you can
confirm it emits directory structure only, never file contents, and
normalizes everything to repo-relative. The
capture CLI
(npx backthread) is the plugin that applies both — and its
resolveGitContext helper is the exact code that reads the branch
name and HEAD SHA (two git rev-parse calls, nothing more). All MIT, in the public
backthread/backthread repo.
The browser-side encryption of your device token at login is likewise in the open —
the ECDH/HKDF/AES-GCM code the CLI uses to decrypt it lives in the same repo
(cli/src/cliAuthCrypto.ts), so the "servers only ever store ciphertext"
claim above is auditable end to end.
The server-side structural extractor — the deterministic pipeline that runs
against your code inside the ephemeral sandbox — is open source too,
published as
@backthread/extractor
in the same MIT repo. It is the package our container installs, so the
"install-free parsers, we never execute your code" claim above is readable
rather than promised — and it is the same package that would run on your own
runner in the mode described further up. If the implementation drifts from any
claim on this page, write to
security@backthread.dev.
Some claims here are only checkable against a live response, not against our source — and that is exactly where we got it wrong, twice. Until 2026-08-17 our Privacy Policy said we ran no third-party analytics, while every page we serve loaded Cloudflare's cookieless page-view script. Cloudflare injects it at the edge, so it exists in no file we publish and reading our repository could never have found it. Then, fixing that, we wrote that the beacon was the only script Cloudflare injected — and it is not. There are two, and the second one we had also never noticed.
So here is the check, and please run it rather than trusting this
sentence. Fetch https://backthread.dev/privacy and list
every <script> tag in the raw HTML — the
count is the property worth verifying, not any one hostname.
Today you will find exactly one there — the
analytics beacon. The other third-party script named below,
Cloudflare's email obfuscator, is injected only where an address still
needs obfuscating, and since 2026-08-21 the legal pages have none; on
the home page you will still find it. Counting the home page instead
gives three tags, because one of them is a short
inline script of our own that loads nothing and contacts nobody. Both
third-party ones are named in
Privacy Policy §2.7:
- the analytics beacon from
static.cloudflareinsights.com. Cloudflare used to inject this one at the edge too; since 2026-08-21 we carry the tag in our own HTML, so it is in the source we publish as well as in the response you fetch; and - Cloudflare Email Address Obfuscation, served from our own origin at
/cdn-cgi/scripts/…/email-decode.min.js. It rewritesmailto:links and bare addresses into a/cdn-cgi/l/email-protectionlink and decodes them in your browser, to keep our addresses away from scrapers. It sends nothing anywhere. It used to have one dishonest side-effect: with JavaScript disabled our contact addresses did not render at all. Fixed 2026-08-21 — every address on this page, the Privacy Policy, the DPA and the Terms is now excluded from the obfuscator and served as an ordinarymailto:that works with JavaScript off. Because of that exclusion this decoder script is no longer present on those four pages at all, while it still runs elsewhere on the site. Addresses remain written out in plain text in the imprint below.
Beyond those two, the only third party your browser contacts anywhere
on backthread.dev is the youtube-nocookie.com
player behind two home-page FAQ answers, and only if you press play on
one — they are click-to-load, because we measured that
loading="lazy" does not defer an iframe and found both
players were being fetched on every home-page visit.
Fonts are served from our own origin; there are no external
images and no tag manager. The signed-in app additionally talks to our
own worker and to Supabase, and to Stripe when you open billing.
The promise is not that this list can never grow — it is
that it will never grow without being named in
Privacy Policy §2.7 first. If you find
a script there that §2.7 does not name, we have broken that promise
— tell us at
security@backthread.dev
(also reachable as security [at] backthread.dev).
Sub-processors
We are honest about who else touches your data. Our Privacy Policy §5 lists every recipient in full — sub-processors, plus the analytics and video-embed recipients that are not sub-processors — and our DPA §6 lists the sub-processors that touch Customer Personal Data. The short version:
- Supabase (EU
eu-west-1, Ireland) for the database, auth, and realtime. - Cloudflare for hosting, the worker tier, the queue, edge caches, and cookieless page-view analytics — it counts visits to this website and to the app, and sets nothing on your device: no cookie, no cross-site identifier, no fingerprint, and no id that outlives the page load. Privacy Policy §2.7 lists every field it reports.
- Google (US; a Data Transfer Solution — the EU–US Data Privacy Framework — where one exists, EU SCCs as the fallback, per its processor terms) for the LLM that names + narrates the diagram (module labels + per-module changelog), the bulk pass that derives decisions from redacted capture transcripts, and the pass that writes and grades lesson questions. Grading sends the answer you typed; it never sends your identity.
- Anthropic (contracting entity for us is Anthropic Ireland, Limited; processing in the US, EU SCCs) for the tie-break pass when deriving decisions from redacted capture transcripts.
- GitHub (US, EU SCCs) for source-code access via the read-only GitHub App.
- Stripe (US, EU SCCs) for payments and subscription management — Checkout and the billing portal. It sees your billing identity, email, and card details; we never see or store a card number. It sees nothing from your repositories.
- Resend (US, EU SCCs) to deliver the emails we send you — the Decision Digest and trial notices. The digest contains decision titles and summaries derived from your repository, so Resend transmits customer-derived content and is a sub-processor, not merely a recipient.
- YouTube (Google) — a recipient, not a sub-processor: two answers in our home-page FAQ can play videos through the
youtube-nocookie.complayer. Both are click-to-load — the embed is not in the page until you press play, so nothing reaches Google unless you choose to watch. It never touches your data; it is your visit to our page. - Telegram — a recipient, not a sub-processor, and it does see data about you. We push internal operational alerts into five private Telegram channels — errors, signups, product, marketing and ops — where the founder reads them, plus a nudge our own marketing tooling posts when it has work waiting. Not, as this page used to say, a single direct message about a waitlist signup. What reaches it: the email address and the GitHub username (or display name) on a new signup, with the sign-in provider; repository names, including private repositories belonging to our customers, in product and failure alerts — a repository is announced by name on its first successful build — and a repository that never builds is not thereby unnamed, because failure alerts name it precisely when a build goes wrong. A failure alert carries the error text exactly as the failing system handed it back, so it can contain paths, pull-request numbers, and fragments of whatever the failing tool was reading inside that repository; account, installation, organisation-login and user identifiers, counts, and Stripe invoice and event identifiers when we cannot process a payment webhook; and, when you dispute a grade, the repository’s identifier, the question’s identifier, the verdict you disputed and the length of what you wrote. Our daily marketing sweep, weekly ops summary and monthly strategy review also land there, carrying funnel counts, new signups, repositories connected and revenue figures, verbatim quotes from public posts with their authors’ names, and research notes that can name companies and individuals we are talking to. It never receives your captured transcripts, and it never receives the text of a lesson answer — though the text of a disputed answer did reach it before 2026-08-19, which the Privacy Policy §10 records in full. We do not send it your source code either, but we are deliberately not writing never about that: a failure alert carries a tail of the error output from a tool that was reading your code, so we cannot rule out that a line of that code appears in one. We call it a recipient rather than a sub-processor because it is a messaging channel we push notifications into under its own terms, not a service processing your data to deliver the product; it operates under no Data Processing Agreement with us, and we would rather say that than let the label imply otherwise. It also sits outside the EEA, so unlike the sub-processors above — each of which has a transfer mechanism behind it, whether Clauses or the Framework — there is no transfer mechanism at all behind this one. Narrowing what we send there is work we have on the list, not a claim we are making. If you would rather the founder were not notified about your signup, write to hello@backthread.dev and we will turn it off for you.
We will publish at least 30 days' notice on the Privacy Policy before adding a new sub-processor that processes customer data.
What we explicitly haven't promised yet
We don't claim SOC 2 or ISO 27001 today. We'll seek certification when we go after customers who need it; we're not going to claim it before it's true. The same is true of penetration testing reports, bug bounties, and a public security.txt — these are coming, just not yet.
Reporting a security issue
Write to security@backthread.dev. We respond within one business day. We do not yet run a paid bug bounty.
Operator: BACKTHREAD OÜ, Estonia (registration number 17524049, registered office Harju maakond, Tallinn, Lasnamäe linnaosa, Peterburi tee 92g, 13816). Long-form policy detail in the Privacy Policy, the DPA, and the Terms of Service.
Contact, written out so it survives with JavaScript switched off and
needs no script to read: general and privacy enquiries go to
hello [at] backthread.dev, security reports to
security [at] backthread.dev. Both are ordinary
mailboxes; replace [at] with @. The clickable versions
elsewhere on this page used to be rewritten by Cloudflare's
email obfuscator, which is why these two were spelled out here; since
2026-08-21 they are ordinary mailto: links that work with
JavaScript switched off. These plain-text forms stay as a second way in.