security model

crosscode moves uncommitted file contents between machines. three things carry that: a per-checkout daemon, a coordination service, and the local clients that talk to the daemon. this page is what defends each, and what does not.

authentication

sign-in is github oauth, and there is no other path. members authenticate through the website; the cli obtains the resulting session by a device-code handshake and stores it mode-0600 in <git-dir>/crosscode/config.json, preferring the os keychain for the refresh token where one exists (macos security, linux secret-tool).

the cli asks the service for a pair of codes, prints a url and the short userCode, and polls. the user signs in on the website and types the code, which binds it to their session; the next poll returns the session. what that shape buys, and costs:

the service verifies supabase access tokens against jwks and takes the github identity from the verified claims, never from anything the client asserts. device sign-in only produces such a token; it does not widen what one means.

every service request re-derives project membership server-side for the authenticated user rather than trusting a scope in the token, so a removed member loses access on their next request instead of when a token expires.

invite redemption

an invite code is a bearer secret, so the interesting check is the one after it: redemption verifies through github that the redeeming account actually has access to the repository. a valid code from someone with no repo access is refused. this is the reason identity is github-only.

codes are stored hashed, single-use, and expiring. unknown, expired, and already-consumed codes are answered identically so the endpoint is not an oracle, and redemption is rate-limited by ip.

the daemon's local api

the daemon binds loopback only and writes a mode-0600 connection descriptor (<git-dir>/crosscode/daemon.json) holding its port and a random secret. every local request carries that secret as a bearer token. clients refuse a descriptor whose permissions are wider than owner-only or which is owned by another user, so a shared machine cannot hand one user's descriptor to another.

the mcp server and the pre-edit hook hold no state and no credentials of their own; they read that descriptor and forward.

the daemon ↔ filesystem boundary is fully trusted. the daemon reads and writes the checkout it manages, with no sandbox between the two.

what is never sent

enforced before a change is captured, not filtered afterwards:

integrity on receive

the receiver verifies every change before applying it: the content must hash to the contentHash on the wire, and the merge base must resolve to the baseHash the sender recorded. a change that fails either is not applied.

that check has to be the receiver's rather than the service's. a service willing to substitute content would substitute the hash beside it, so a server-side check is a garbage-in filter and nothing more.

encryption

tls in transit. at rest, the data sits in supabase postgres and on vercel, encrypted by those providers under keys they hold; crosscode adds no encryption layer of its own. there is no end-to-end encryption, no device pairing, and no key exchange. the service can read the file contents it stores; privacy.md says so in the same words.

we built e2e once and removed it. it bought a real property and cost a device-pairing ceremony, a keyring, epochs, rotation, and recovery paths, for a product whose durable artifact is a git repository the user already has. the honest trade for this product is fewer moving parts and a plain statement of what we hold.

service ↔ database

row level security is on from the first migration. the service connects with a least-privilege role that cannot rewrite history: file_versions has no update or delete policy at all, so no request-handling path can erase or alter a change. any retention sweep runs as the owner on a separate connection, never through the runtime role.

what a malicious member can do

given a valid session and project membership:

what they cannot do:

threat model, stated plainly

holds against: an attacker on the network (tls); another local user on a shared machine (loopback binding, 0600 descriptors and config); a member removed from a project (access ends on their next request); a service that corrupts stored content (receiver-side hash verification); an invite forwarded to someone without repo access (github check).

does not hold against: a compromised member device, which holds the checkout and the session; a member who leaves with what they already synced; anyone with production access to our service, who can read stored file contents; traffic analysis of when you work and how large your changes are.

report vulnerabilities per SECURITY.md, never in a public issue.

breach response runbook

crosscode stores plaintext customer source code. a breach here is severe by definition: there is no "only metadata was exposed" outcome available. assume the worst category until the evidence rules it out.

the gdpr clock is 72 hours to notify a supervisory authority, and crosscode's dpa promises controllers notice within 48 hours. both clocks start at awareness, not at containment.

who decides: the project owner. one person, named in the privacy policy, owns every call below: severity, notification, disclosure. there is no committee to convene and no escalation path. if the owner is unreachable, the breach waits, which is a real risk of a one-person project and is stated rather than papered over.

1. detection

where a breach realistically surfaces:

awareness starts when you have a reasonable degree of certainty that a security incident occurred and it involved personal data. not when you finish investigating. write the timestamp down the moment you reach it, because the 72 hours is measured from there and you will be asked to evidence it.

2. first 60 minutes: contain, then preserve

in this order:

  1. stop the bleeding. rotate what is exposed: supabase service role key and database password, SENTRY_DSN, POSTHOG_KEY, and any github oauth app secret. if a deployment is the cause, promote the last known good one from the vercel dashboard. a promotion takes seconds and a diagnosis does not.
  2. do not delete anything. not logs, not rows, not the bad deployment. evidence first; see step 3.
  3. revoke sessions if account takeover is plausible: supabase auth, sign out all users on the affected accounts.
  4. write down the clock: when it started, and how you know.

3. preserve evidence

before anything ages out. vercel and supabase log retention is days, not months, so this is genuinely urgent:

put all of it in one dated folder outside the repository. do not commit evidence containing personal data to a public repo.

4. assess severity

level what it means examples
p1, file contents exposed someone who should not have been able to read stored file contents did, or plausibly could have database credential leak, rls bypass, a route serving another project's changes, backup left readable
p2, account or auth compromise identity or session data exposed, without confirmed content access device-code or session token leak, oauth secret exposure, account takeover
p3, metadata exposure records about accounts and projects, no file contents email addresses, github logins, owner/repo names, project membership
p4, no personal data a security issue with no personal data involved a vulnerability reported and fixed before exploitation, with logs showing no access

p1 and p2 are notifiable. p3 usually is, because repository names alone can be commercially sensitive, and the honest reading is that owner/repo plus membership tells an attacker who works on what. p4 is not notifiable, but write it up anyway.

the three questions that decide it: was personal data involved? could someone unauthorised have accessed it? is there a risk to those people's rights and freedoms? if you cannot answer the second one "no" with evidence, treat it as yes.

5. notify

supervisory authority: within 72 hours of awareness. late is better than never: a late notification must explain the delay, so file within the window even if the picture is incomplete. art. 33(4) explicitly allows notifying in phases.

Subject: Personal data breach notification, Crosscode

1. Nature of the breach
   What happened, in two sentences. When it started, when it was discovered, whether it is
   contained now.

2. Categories and approximate number of data subjects
   Account holders affected: N.
   Third parties whose personal data was inside the affected repositories: unknown.
   Crosscode does not inspect file contents and cannot enumerate them. See the privacy
   policy, §3.3.

3. Categories and approximate number of records
   Which of: file contents and paths; account records (email, GitHub identity);
   authentication records; project and membership records; analytics or error reports.
   Number of file versions, projects and accounts, as best established.

4. Likely consequences
   For a P1: source code, and any personal data inside it, was readable by an unauthorised
   party. Downstream risk to the affected users' own customers, which those users must
   assess as controllers.

5. Measures taken and proposed
   Containment already done, credentials rotated, fix deployed, notifications sent, changes
   to prevent recurrence.

6. Contact
   {{PROVIDER_NAME}}, privacy@getcrosscode.dev

7. What is not yet known
   State it explicitly, with the date by which you expect to know.

affected users: without undue delay, where there is high risk to their rights and freedoms. for a p1, assume there is. send from security@getcrosscode.dev:

Subject: Security incident affecting your Crosscode data, action needed

What happened
  On <date> <one sentence: what an unauthorised party was able to do>. We discovered it on
  <date> at <time UTC>.

What of yours was involved
  <Specific to this user: which projects, which repositories, whether file contents were
  readable, whether their email address or GitHub identity was exposed.>

What this means for you
  Crosscode stores the contents of files you synced. Treat any secret that was in a tracked
  file in <repos> as exposed, and rotate it. Untracked files never left your machine, and
  .env files and key material are on a hard denylist and were never stored.

  If those repositories contain other people's personal data (names in fixtures, customer
  records in seed data) you are the controller for it and may have your own notification
  obligation. Our data processing agreement is at /docs/dpa.html.

What we have done
  <Containment, rotation, fix, deployment.> The service is <state> as of <time UTC>.

What we are asking you to do
  1. Rotate any credential that was in a tracked file in the affected repositories.
  2. Review <repo> for personal data you may need to notify people about.
  3. <Anything user-specific: re-authenticate, revoke a token.>

We are sorry. Questions to security@getcrosscode.dev; we will answer every one.

{{PROVIDER_NAME}}, Crosscode

do not minimise, do not lead with what was not affected, and do not say "out of an abundance of caution" about something you know happened. the candour in privacy.md is a legal asset only if it survives contact with a bad day.

also notify: any affected user's own supervisory authority is their call, not yours. give them what they need and say so explicitly.

6. post-mortem

within 10 working days, in the repository under docs/incidents/, public unless publishing would expose an unfixed vulnerability or another user's data:

keep the art. 33(5) internal record for every breach including the ones you decide are not notifiable: the facts, the effects, and the remedial action. the regulator can ask for it, and "we assessed it and decided not to notify" is only a defence if the assessment was written down at the time.

7. what this runbook does not have

before this takes effect

view raw markdown · generated from docs/security.md at build time, do not hand-edit this page.