Read-only, never cloned,
and no token in the database.
- Repository access
- Read-only
- Repositories cloned
- None, ever
- Tokens in the database
- None from GitHub
- Markdown sanitized
- Before storage
- Key material stored
- SHA-256 only
Three kinds, and what happens to each
The rule underneath all of them: we hold the least durable form of a credential that still works, and where we must hold one we say where and under what key.
GitHub
A GitHub App, read-only, on the repositories you pick.
- Revocable from GitHub at any moment, without asking us.
- Installation tokens are minted on demand from the App's private key and held in memory for their hour. Nothing token-shaped is ever written to the database.
- The private key is the only long-lived secret and belongs in a secret store, not beside the data.
- Connecting a repository asks GitHub as *you*, not as the installation — so nobody can connect a repository they cannot personally read.
GitLab
A project or group access token you paste, encrypted at rest.
- Encrypted under its own key, deliberately not the session secret: rotating the session secret signs everybody out and is routine, rotating this one means re-encrypting every credential.
- A token carrying a write scope is refused at connect, by name.
- A token with no expiry is refused outright.
- The consequence is stated rather than worked around: because we will not hold a write-capable token, we cannot create GitLab webhooks, and freshness is a poller instead.
API keys
For agents and CI. The organization's, not yours.
- Only a SHA-256 of the key is stored. The value is shown once, at mint, and there is no recovery path — because a recovery path would mean it was kept somewhere.
- A key belongs to the organization rather than its author, so a CI job does not break the day that person changes team.
- One key names exactly one project. There is no organization-wide key to mint any more.
- Revoking bites on the next request. Nothing caches the lookup.
Markdown permits raw HTML, and that is the highest-severity risk in a product like this
A <script> tag in somebody’s README would otherwise execute in our origin, with the reader’s session. Every document is sanitized server-side against an allowlist before it is stored as HTML — not filtered at render time, where one path that forgets is one path that ships an exploit.
The raw markdown stays the source of truth. Rendered HTML is only a cache, keyed by the content's hash and a renderer version, so hardening the renderer invalidates everything at once and nothing has to be migrated.
The repository is never cloned, and nothing outside your docs is ever fetched
One API call returns the folder structure plus a content hash for every file. There is no git binary, no working copy and nothing written to disk. Only markdown blobs are fetched, and only from inside the docs root — a source file is a name we may see in a path, never a body we retrieve.
Images inside a private repository are served through an authenticated proxy rather than hotlinked, so they load for the people allowed to see them and for nobody else.
Session cookies are encrypted, not merely signed
An OAuth session carries the user's GitHub token, so a signed cookie would be a readable one. Cookies are encrypted; passwords are hashed with Argon2 behind a port, never a library whose dependency Python removed.
Revocation is real rather than nominal. Every authenticated request compares an epoch on the account row, which is the honest price of withdrawing a credential that carries its own validity — a decrypted cookie is not an authenticated one. Signing out every device is one action, and disconnecting GitHub invalidates the sessions that carried its token rather than leaving them working until they expire.
There is a session list: a coarse platform label and an address truncated to a /24. No city, no country, and no guess about whether a sign-in was unusual — those need a geolocation database or a model of usual, and inventing either is how a device list starts reporting incidents that never happened.
Nothing is readable by default, and the browser never decides
Joining an organization hands out no documents. A member reads a project through a team grant or a direct one; somebody in no team with no grant reads nothing at all. There is no visibility setting that quietly confers access to everybody.
Access resolves server-side, in the one query every reader route already flows through. A repository you have lost access to answers 404 — identically to one that never existed, so the response cannot be used to learn what exists.
No role comparison happens in TypeScript. Even the per-row 'may this person be removed' answers the Members screen renders are computed on the server, so no copy of the hierarchy reaches the client to drift.
A URL you supply is treated as an attack surface
A self-managed GitLab host is a URL a customer types and the server then fetches, which is an SSRF primitive. The checks are pure predicates: https only, every resolved address checked against private, loopback, link-local and reserved ranges, and redirects refused outright rather than followed and re-checked.
Webhook order is fixed and never rearranged: raw body, then the signature compared in constant time, then parse. Reading a payload before verifying it is parsing something a stranger sent.
The client address is configuration rather than a header. At the default, forwarded headers are ignored entirely; where proxies are counted, the address is taken from the end a caller cannot extend by prepending.
A machine credential is narrower than the person who minted it, on purpose
A key can carry a minimum review state, and documents below it answer 404 on every door — the reader routes included, because a floor that stops at one surface is a floor with a second front door. It lives on the credential rather than in a tool argument: a filter the agent passes is a filter the agent can be prompted out of passing.
No tool on that surface mutates a repository document, closes a reviewer's note, deletes an artifact, or asks a model a free-text question. Those are absences rather than settings — there is no flag an administrator can grant, which is the point.
Document text leaves the box only for an organization that said so
This is the one outbound path carrying customer content, and the switch is off by default — defaulting it on would have started every existing organization sending documents to a third party because a deploy happened. Turning it on is written to the audit log.
An empty document makes no call at all: prompting a model not to invent is a request, whereas not calling it is a guarantee. A provider named without its key refuses rather than returning placeholder text, because a fabricated summary stored against real content is served as real until that content changes.
An audit row is written inside the same transaction as the change
Never after the commit, never best-effort, never from a background job. If the change rolls back the record goes with it, and if the record cannot be written the change does not land. The actor's name and the subject's label are copied onto the row, because a deleted team still has to read as the team it was.
The trail is filterable and exportable, and kept for the window your plan sets. Nothing can delete a single row from it — a caller able to remove one row is a caller able to remove the interesting one.
What you operate yourself
Two of these are plan-gated and the page says which, because finding that out at the point of need is worse than reading it here.
IP allowlist
Refuse every request from an address you did not name. It reaches API keys as well as browsers — a machine credential is checked at the door before the request is dispatched.
Required GitHub sign-in
Withdraw documents from anybody without a GitHub identity, so access follows an account your organization can see.
Session policy
Set how long a session lives, and sign every member out at once when you need to.
Audit retention
Choose how long the trail is kept. A longer window is the benefit; the purge runs on a schedule rather than on demand.
What we have not done
A security page with no gaps on it is a security page nobody believes. These are real and they are current.
- SSO and SAML are not built
- The card exists in the organization's security settings and it is static markup. It is part of what an Enterprise agreement covers and is not something you can turn on today. We would rather say that here than let you find it after signing.
- No SOC 2, no ISO 27001, no penetration test
- None of these have been done. If your procurement process requires one, we are not ready for you yet, and that is a real answer rather than a roadmap promise.
- DNS rebinding is closed; connection pinning is not
- The address checks run at resolve time. A host that answers differently on the second lookup is handled, but we do not pin the connection to the address we validated.
- No customer-managed keys on a deployment we run
- Encryption keys belong to the deployment. Holding your own means running your own: an install inside your network is an Enterprise term, and on the deployment we operate the keys stay ours.
Found something wrong with any of this? [email protected]. We would rather hear it from you than read it somewhere else.
Give it read access to one repository and take it away in a click.
The smallest possible trial of any of this is a public repository and five minutes.
Start free