Security
This page is written to be forwarded unedited to a security team. Every claim on it is either a permission GitHub shows you on its own consent screen, or a property a build gate in this repository fails on. Where something is a promise rather than a mechanism, it says so — and §"What a build gate holds us to" names the gate behind each claim, so your own engineers can read the check rather than trust the sentence.
The asset table and the permission table below are the same ones in our internal threat model, parsed out of it by a gate: this page cannot quietly say something kinder than the document our engineers work from.
What an attacker would be after
| Asset | Why it is wanted | Where it lives |
|---|---|---|
| Customer source | it is the company | your repository. On the CLI path it never leaves your machine; on the App path it is unpacked onto our analysis worker's disk for one analysis, read inside a sandbox with no network, and deleted when that analysis ends. One exception, and it is the only one: where a scan finds something to optimise, the few lines it is about — the loop itself, three lines either side, forty at most — are kept so the Optimize screen can show you the code the suggestion is about. They are encrypted at rest, they are never shown to anybody at MLReps, and they are deleted the moment a later scan finds the case fixed, the moment you dismiss it, when you remove the organisation, or after thirty days without a scan — whichever comes first |
| Installation tokens | read access to every repository in an installation | minted per request, valid one hour, never persisted, never logged — no column capable of holding one exists, and a build gate fails if one appears |
| Our publishing keys | ship code into every customer's build | hardware-backed, CI-only, never on a laptop |
| The admin account | one login that sees every tenant's business | aggregates only. No screen and no query reads one customer's data; the timed, customer-issued grant that would allow it during a support case is designed and not built, so today the answer is that the path does not exist |
| The knowledge graph | an architecture diagram of a private system | identifiers, spans, metrics and findings — never source text. We hold the latest scan of each repository's default branch, per organisation, from your pipeline's publish or from the App. A pull request's scan is posted to that pull request as a check run and kept nowhere |
| Findings metadata | a map of where a company is weakest | stored — the one asset we genuinely hold at rest |
| Credentials we encounter in their code | direct access to their systems | removed before anything leaves the machine — before a check-run annotation is posted, and before anything is written down |
The row worth reading twice is findings metadata. "These twelve files are load-bearing, untested and changed last week" contains none of your source and is still a target list. It is why tenant isolation is treated as a correctness property here rather than a configuration setting.
The permissions we ask for, and the ones we refuse
contents write the tree and the diff — and one file, in a pull request
pull_requests write our own comment, and the one pull request below
checks write our own check run. Never anybody else's
metadata read unavoidableRefused permanently: workflow, administration, secrets, actions, members.
contents: write is new, and it is a real reduction in what we can promise you. Until 2026-10-29 this said read, and said that we could not modify your code even if we were fully compromised, because the permission did not exist. That sentence is gone and we are not going to dress up what replaced it.
What the permission is for: one file, instrumentation.ts, on a branch of its own, in a pull request you review and merge. It turns on OpenTelemetry so the runtime half of this product works without you running anything. Nothing else is written, ever.
What stops us writing more is our own code and your review — not GitHub. That is weaker than a permission that does not exist, and the difference is the honest cost of the feature. What still holds:
- we never push to your default branch; every write is a pull request with a diff
workflowis still refused, so we cannot change what your CI runssecretsis still refused, so we cannot read or write your secrets- every write appears in your history with our name on it, and is revertible
- removing the installation removes the access, instantly
If you would rather we never wrote to your repository, install the App and decline the pull request — the code half of MLReps works exactly the same, and you can turn telemetry on yourself with one variable.
Every token we mint asks for less than the installation allows. The worker that reads your repository asks for contents: read and cannot write however wide the installation is; the token that opens the pull request asks to write, names your one repository, and is handed back the moment the commit lands. A permission is what the App may have; a capability is what a given token carries, and those are not the same thing.
What that does not protect against, because you should hear it from us: whoever holds our App key can mint any token the installation allows. The arrangement above defends against a compromise of the sandbox that runs other people's code — the part most likely to be attacked — and not against a compromise of the host that holds the key, which already holds our database. The independent penetration test in §9 has still not been run.
A check run never decides whether your code merges. The type in our code admits only a neutral conclusion; there is no branch that can fail your build.
What we do not do
- We do not write to your repository. We produce a patch; you apply it, your tests run it, you commit it under your name.
- We do not store your diffs, or anything a model said about your code. Your source: one exception, and it is written out above — the few lines an optimisation case is about, kept only while that case is open.
- We do not record who dismissed which finding. The table has no column for a person, so that feature cannot be added without changing a schema in public.
- Your code goes to a model in exactly one place, and only when you press the button that says so. On the Optimize screen you can ask Gemini to show what a change looks like; that sends the few lines of the case you have open — the same lines drawn on the left of that panel, never the file, never another case — and draws what comes back as a suggestion, which is not stored. Nothing else on any screen sends your code anywhere, and nothing sends it while a page is merely loading.
Reporting a vulnerability
Where: connect@mlreps.com — also listed at /.well-known/security.txt
What to expect
- We acknowledge your report within two business days.
- We tell you whether it is in scope, and our assessment, within ten business days.
- We tell you when it is fixed, and we credit you unless you would rather we did not.
Safe harbour. If you make a good-faith effort to follow this policy, we will not pursue or support legal action against you for your research, and we will treat what you did as authorised. Good faith means you do not access or alter data belonging to anybody else, you do not degrade the service for other people, you give us reasonable time to fix the issue before telling anybody else, and you do not attach a payment demand to the report.
In scope: this repository, the hosted analysis service, the GitHub App's permission surface, and the analysis sandbox.
Out of scope: anything that requires an already-compromised developer machine, denial of service by volume, and scanner output with no demonstrated impact.
What a build gate holds us to
Every line below is a property a check in this repository fails on, not a policy somebody is trusted to follow. The suite runs on every commit, and the whole of it has to be green before anything ships.
The analysis container. It has no network, a read-only filesystem, every Linux capability dropped, no-new-privileges, a pids ceiling and a memory ceiling, a seccomp profile, and it cannot start a second process at all. Every line of that specification is asserted against the arguments the running container is actually launched with — 71 checks — so the document and the container cannot drift apart. We also run an adversarial suite of sixteen checks against the running container, covering network egress, the read-only filesystem, the dropped capabilities, no-new-privileges, both ceilings, the single-process limit and hostile input to the analyser. All sixteen hold.
Your code does not run in our process. Fetched code is read inside the container and nowhere else, and 114 checks cover the queue, a worker dying mid-analysis, and the absence of any flag that could change it.
One column can hold your source, and a gate counts it. 540 fields across 49 models and 434 columns across 48 migrations are checked against what we are allowed to store. Exactly one of them holds any of your code — the optimisation snippet described above — and the gate that checks this keeps a list of such columns with one entry on it, so a second one fails the build until somebody writes down why. No column for a diff, a patch or a token exists at all. A further 20 deliberately poisoned schemas are refused by the same gate.
Credentials in your code never leave the machine. They are removed before a check-run annotation is posted and before anything is written down, tested against 13 real credential formats and 14 strings that only look like one — and nothing that is merely code is destroyed in the process.
Nothing that can carry your source reaches a log. 104 log calls across 610 files are checked against 8 source-carrying types and 12 telltale properties.
Tenant isolation is a correctness property. 193 queries against 25 customer-owned tables across 562 files are each proved to be scoped to one organisation or to declare why they are not. The admin plane is held to aggregates only: 6 exported functions and 2268 type shapes, none of which can name a customer.
We can write exactly one file, and only as a pull request you review. Until 2026-10-29 this paragraph said we could not modify your code at all, because the permission did not exist; that is no longer true and §3 above does not soften it. What is held, and checked on every build over the token's every escape route, a widened installation, the webhook endpoint and a check run that cannot fail your pull request: the write never reaches your default branch, the token that carries it names one repository and is handed back when the commit lands, and the process that reads your code asks for contents: read and cannot write however wide the installation is.
Our dependencies are accounted for. 539 packages, 17 of them direct, with every one of the 4 install-time scripts carrying a written reason; a new one fails the build until somebody explains it.
Reports arriving from outside are treated as hostile. 147 checks cover what an incoming report is refused for and a publish that cannot turn your build red.