What happens to your code
No company wants to hand its codebase to a third party. This page is the mechanism, in detail, including the parts that are not reassuring.
A private repository stays with you
Stated first, because everything else on this page depends on it. A private repository is surveyed by the collector, which runs on your machine. Borehole never clones or holds a private repository. What crosses from it: never whole files; facts and short excerpts of up to 120 characters where a check matched. A public repository is different: it is cloned on our servers for the survey and deleted after, as Public repositories are different says.
The rule covers machines we cannot read too. A confidential VM in our own cloud project would stop us looking at your code, and your code would still be on our infrastructure. We do not run one. "It stays on your machine" is a simpler promise than "we hold it and cannot read it", and it is the one we make.
pip install borehole export BOREHOLE_TOKEN=bh_… from your account page borehole collect . --dry-run prints the entire bundle, sends nothing about it borehole collect . sends it, returns a report
What does cross
Facts about the repository, computed where it sits. The whole list:
- Commit hashes, dates, subjects, and insertion and deletion counts. A subject crosses as written, with secret-named values blanked from collector 0.7.9, so a package or host it names crosses with it.
- Which files changed in which commit (and, from collector 0.4.0, whether each was a binary file), the list of tracked file paths, and tags with the commit each points at and its date.
- From collector 0.4.0: the name and version of each dependency your lockfiles pin from a public registry, its ecosystem, and whether the lockfile marks it as shipping. Packages from a private registry, a git URL or a local path are left out, because their names can describe your own code. Borehole accepts collector 0.7.10 or later, and a package crosses only when its lockfile shows a public registry served it. Every other one stays on your machine, including those whose lockfile does not say where they came from, as pnpm and Yarn 2 and later do not for the registry you configured; the bundle carries how many, and the report says they were not looked up. NuGet and Gradle files never say which registry a package came from, so from collector 0.7.4 none of their names is sent, and the report says those ecosystems were not assessed. Your project's own package name crosses to our server as its manifest declares it, and when you run the collector our server sends that name to no one: it looks up a project's own package, in OSV or anywhere else, only for a public repository we survey ourselves. Our server looks each locked package's ecosystem, name and version up in OSV, a public vulnerability database. OSV receives nothing else: no repository name, no path, no code.
- The remote's host and path, or the directory name if there is no remote.
- Where each detection pattern matched — a path and a line number —
and up to 120 characters of the matched text. Except
a credential: a match that looks like a key or token crosses
as its location and kind only, never its text. From collector 0.7.1
the collector enforces this itself, for every pattern the server
sends, whatever it is called. From collector 0.7.2 it also sends a
12-character fingerprint of each match, salted with a random value
made for that one survey and never sent or kept. It says only that two
matches in the same survey are the same key, so one test key found in
three files counts once. Without the salt, nobody, Borehole included,
can check a guessed key against it or match a key across two
surveys. From collector 0.7.3, three more yes-or-no facts about a
credential, worked out on your machine: whether it sits in a test (a
test-named setting, or a Rust
#[cfg(test)]block), and, for a signed token, whether it has expired, whether its own header calls it a licence, and whether it was issued for a local host. No claim, name or host crosses. From collector 0.7.4, one more: whether the value is placeholder text where a key would go, such asyour-token-here, or a private key block holding no key material. - From collector 0.5.0, committed
.envfiles, notebooks and app manifests are read too. From a.envfile what crosses is where a key-shaped value sits and what kind it is, and the bare names of settings a public prefix exposes to the browser (such asNEXT_PUBLIC_…). Never a value: from collector 0.7.1 the collector blanks any matched text from a.envfile that goes past a setting's name. From collector 0.7.5 that covers any file named*.envtoo, and nothing matched in a key or credential file crosses at all:.pem,.key,.p8,.p12, SSH keys, a service account's or OAuth client's JSON,.npmrc,.pypirc,.netrcand git credentials. - Secrets committed at any point in the history. The collector walks every added line on your machine and sends, for each credential-shaped match, the commit, the path and the kind of pattern; from collector 0.7.0, also whether the line was a comment, named a local or example host, or was an environment fallback; from 0.7.2, the survey's salted fingerprint of the match, as above. Never the text.
- From collector 0.6.0: names found in lines the history added or removed, such as a feature-flag key, an API version, a dependency or a CI platform, with the dates each was first and last added and removed. A name, never the line. From collector 0.7.9 a dependency name crosses readable only when a lockfile pins it from a public registry or it is a common development tool; any other crosses as a fingerprint salted for that one survey, which no one can reverse.
- From collector 0.6.1: which packages of your published Go dependencies the code imports, so an advisory for a package nothing imports can be set aside. Never your own package paths, and from collector 0.7.9 nothing under a module from a host that does not serve modules to the public.
- Two kinds of name cross as your files write them, whatever host
they name. A Go module's own path, from
go.mod, names the host its code lives on. And from collector 0.7.9 the telemetry checks send the host of an address in your code or configuration that is named for telemetry, usage statistics or crash reports, such ascrash-reports.example.internal, private or not, and never its path or query. - From collector 0.6.2: which vendored files open with a licence notice, and which phrase (such as "Licensed under"), and which vendored components a notices file names. Never the notice or the notices file's text.
- From collector 0.7.0: a status line on the README's first screen, such as "no longer maintained", without addresses or handles; where a contributing, security or governance file states a policy (a mirror, no pull requests, one maintainer, an AI policy, a named foundation), never the line; the licence identifier of each past version of the root licence file, with its commit and date; and submodule paths.
- Two counts about the tree: how many files, how many lines.
- Author names and email addresses, hashed on your machine. From collector 0.7.12 the hash is keyed with a random value made for that one survey and never sent or kept. The same person is one identity within a survey and not across surveys.
Whole files do not cross. Not compressed, not sampled, not hashed.
What crosses from inside a file is the matched text above, up to
120 characters a match. The collector's source is readable,
and one file in it, extract.py, lists everything it can
compute.
What this rests on
The detection patterns come from our server on each run, so what
crosses also depends on what the server asks for. From collector 0.6.1
the collector does not take that on trust. It refuses a pattern that
matches ordinary text rather than something a check looks for. It
refuses to send a bundle carrying more than 6,000
characters of matched text from any one file, or
1,000,000 in all. A history pattern can send back
only a short name. The connection uses TLS; the collector does not pin
our certificate. And --dry-run, below, shows you the whole
bundle before anything is sent.
Check it before you send it
The dry run needs no purchase, no account and no token. In the repository's folder:
pip install -U borehole
borehole collect . --dry-run # prints what a survey would send, and sends nothing
Without a token it leaves out the short quoted lines a survey sends, because the rules that find them are fetched for each run rather than shipped.
Run --dry-run with your token set to
see those too. It
prints the exact bundle a real run would send, including the quoted
lines, and sends nothing about your repository. The detection patterns
are not in the package; they are fetched for each run, and that fetch is
the only request a dry run makes. Its body is empty.
You should not have to take our word for what leaves your machine, and with that flag you do not have to.
Or run it in your own cloud
The collector can also run inside a Confidential VM in your own Google Cloud project. Google Cloud attests which collector image ran, and the token it signs names the exact facts that image sent. That proves the facts were not edited on the way, which the ordinary collector cannot. Google does not review the report. Your repository still never leaves your project, and we never receive access to it. How to run it, and what it does not prove.
What happens to the facts
The bundle is assessed on our servers: the thresholds, the bands and the wording of every finding live there and not in the package. The report is stored. It holds pointers to your code (commit, path, line range) and never a whole file. It also holds the lines that four checks quote as their evidence, as the bundle carried them: up to five TODO-style comments, up to four commit subjects, up to five unpinned dependency lines and one status line from the README's first screen, such as “no longer maintained”. You can remove those lines, as What you control says. The report records that it came from the collector, and which version.
Public repositories are different
A public repository is cloned onto our infrastructure and read there, because it is already readable by anyone. The clone uses no credential, and the working copy is deleted when the scan ends, including when it fails. The hosted scanner holds no credential for a private repository and refuses any job that carries one.
A paid survey of a public repository is reviewed by a language model.
The review reads the findings and the code it needs at the surveyed
commit, which are sent to Anthropic for that review. Commit
authors from the history are never shown to it, but a file it reads
reaches it as written, including any names the file holds, such as an
AUTHORS file or a copyright line. It withdraws a finding it
judges the code contradicts, and the report lists each withdrawal with
the model's reason. The review is a language model's reading of the findings against the code, and it can be wrong. It never adds a finding, never raises a band, and never removes a finding from a proven check or an advisory that affects the surveyed version. The review runs after a paid survey of a public repository, before the report is stored. It never runs on a private repository, a collected survey, a free reading or a re-read; a re-read keeps an earlier review's withdrawals only where the finding and its evidence are unchanged. If the review is switched off, fails or runs out of time, the report arrives as the checks wrote it. A private
repository or a collected survey never reaches the worker that runs
it.
What we structurally cannot do
These are not promises. They are consequences of how the thing is built, and each one is asserted by a test that fails the build.
- The hosted worker refuses a private repository. A job for one is failed before anything is cloned, however it was queued, and any access token it carries is destroyed unused.
- The web tier cannot clone. There is no
gitbinary in its image. Continuous integration checks this on every build. - The worker runs as nobody. Non-root, read-only filesystem, every Linux capability dropped.
- A public working copy is deleted in a
finally. It goes when the scan raises, not only when it succeeds. - Reports do not record commit authors' names or email addresses. Authorship is counted. A quoted line can contain a name someone wrote in it, and whoever controls the repository can remove it.
What you control
- Who can read it. A private report is readable by the account that ran the survey and by anyone GitHub says controls the repository. Borehole's administrators can also open it, to run the service and answer support. Nobody else can read it unless you share it. It is never listed, never generates a shareable card, and never generates a badge.
- Whether anyone else can. You can mint a share link for a counterparty with no GitHub account. Anyone holding it can read the report — the page says so — and you can revoke it or replace it.
- The quoted lines. Four checks quote text because in each case the text is the evidence: up to five TODO-style comments, up to four commit subjects, up to five unpinned dependency lines and one status line from the README's first screen, such as “no longer maintained”. One click removes them from the stored report. The pointers stay, so you can still find the line yourself.
What we still cannot prove
That the facts were not edited. A report is built from what your machine sent. We cannot tell a bundle the collector produced from one somebody changed afterwards, so a collected report is as good as the hands that ran it — unless the collector ran in Confidential Compute in your own cloud, where Google Cloud attests which collector image ran and which facts it sent.
That a hashed address stays anonymous. From collector 0.7.12, without the survey's key nobody, Borehole included, can test whether a guessed address belongs to an author. Earlier collectors salted the hash with the repository's head commit, which is in the bundle, so anyone holding one of their bundles can. Either way it is a pseudonym, not anonymity: each commit's id, date and subject cross beside it, and anyone who can read your repository can look its author up. A report stores authorship only as counts.
What a quoted line says. Four checks quote the line they matched, because the line is the evidence. If somebody wrote a colleague's name in a TODO, that text crosses. The dry run shows it first, and redaction removes it afterwards.
If a page about trust will not say that plainly, nothing else on it is worth much.
If that is not enough
For some codebases even facts should not leave, and we would rather say so than win the argument. Tell us what you need — we would rather route you somewhere honest than sell you something that does not fit.
See also the Privacy Policy, which lists every processor and what each one holds, and the Terms.