Scope of engagement
Six areas, and the edge of each.
What follows is the whole of what we do, including where each capability stops. A security engagement whose limits are unstated is not one you can plan around.
Repository audit
We read your repository as one archive and work out which endpoints reach which tables and who can call them. Reasoning about how a stranger or another tenant would get to your users' data runs on your own agent once you connect it; ShipLock makes no AI calls of its own.
- Every route is traced from its exported handler through the code it actually runs: the wrappers it is passed to, the helpers it calls, the module it re-exports. From that we record whether a caller is identified, whether the query is narrowed to that caller, and which tables it touches. Evidence comes from behaviour, never from a type declaration or an import.
- Your agent proposes attack paths over those facts. A deterministic guard then refuses anything that cites an endpoint, file or table we did not observe, and anything that contradicts a fact we read. What survives is reported with the reasoning, line by line, so you can disagree with it.
- Where a check runs somewhere we could not follow, the endpoint is reported as unassessed, with the proportion and a named sample, at the top of the Coverage page. An endpoint we could not judge is never shown as clean.
Boundary
This is static analysis of code as written, and we do not attack your production application or anything you operate. On real codebases a large share of data-touching endpoints stays unjudged: a session resolved behind an option we did not read, an API key checked by a helper we could not classify, a query behind a workspace package we do not follow. We say so on Coverage rather than guess, and a check that could not run is recorded as not run: an absent result is never rendered as a pass. Each finding carries a stable identifier across runs, so one that flaps is not re-reported as new. The audit runs when you start it and when the GitHub App delivers a push or pull-request event; it does not run on a schedule of its own.
Merge check
On every pull request the GitHub App tells us about, the files it touches are checked and a verdict is posted to the pull request before the merge.
- Committed credentials in the diff, an endpoint that reaches data without identifying its caller, and an endpoint that reads a table without narrowing it to the caller when the routes beside it do. The verdict is deterministic; the model can explain a finding and can never create or clear one.
- A route whose gate or scope we could not read never blocks a merge. The check fails closed toward passing, because a wrong block stops your pull request rather than producing a report you can argue with.
Boundary
The check depends on the GitHub App being installed on the repository and its webhook reaching us; we cannot see that configuration from your code. Endpoints that are public by design, such as an SDK ingest route, read as anonymous and unscoped, because that is what they are; whether such a route should block a merge is a policy we have not yet let you set.
Source scanning
Deterministic checks on the source itself, after qualifying pull-request activity and at most every 48 hours from the daily pass.
- Committed and historical credentials in real key shapes, and explicit Next.js environment configuration that makes a value available for client builds. Values whose publicness is unknown are flagged for review. A publishable key in the bundle is not reported, because that is where it belongs; a service-role key is.
- Dependencies matched against public vulnerability advisories, and GitHub Actions workflow structure: a pull_request_target workflow that checks out the pull request's own code, a third-party action pinned to a moveable tag rather than a commit, and a workflow with no stated permissions.
- Two things the file settles on its own: a token or one-time code built from Math.random(), which is predictable, and a credential carried in a URL query string, where it reaches server logs, browser history and the Referer header. Under a test or scaffold path both are reported a step quieter rather than dropped.
- A route that changes data without identifying its caller, and mass assignment: a route that writes a request body to the database wholesale rather than a validated set of fields. These reuse the same route facts the merge check (02) reads, reported here on their own because each stands up without needing a database fact to correlate with.
Boundary
A dependency advisory is matched by package and version; whether the vulnerable path is reachable in your application is not determined. Injection, cross-site scripting and weak-crypto pattern checks were withdrawn: a static per-file pattern cannot see whether a value reaching a dangerous sink is actually attacker-controlled. Reasoning about a specific route's authorization and data access, including whether client input reaches a write unscoped, still happens in the repository audit (01) with the model-hypothesis and deterministic-guard discipline described there.
Fix pull requests
After you approve a fix action, it arrives as a pull request on its own branch off your default branch. We never merge it.
- An audit only records evidence and findings. A branch or pull request is created only after you click its fix action; retries recover the same deterministic ShipLock branch or open pull request when possible.
- A finding closes on evidence: when a later audit no longer reproduces it, it is archived rather than waiting for someone to mark it done.
Boundary
Authorisation rules, ownership checks and injection fixes are never machine-authored. Those pull requests are drafts that mark where the change belongs, for you or your coding agent to complete. A leaked credential is a rotation, and no pull request is offered for it. Nothing here demonstrates a fix by re-running an attack; there is no production proof runner today.
Database connection, optional and unused so far
Row-level security policies live in your database, not your repository. Without a connection we infer them from code and say so; with one we read them.
- If you connect a database, configuration is read through Supabase's Management API in read-only mode or through a Postgres role we verify cannot write, inside a transaction that cannot write either. The exposure model reads column names and row counts and none of your data: never a value, never a row.
- What the database itself can reach: an extension able to open outbound HTTP connections and callable by a public API key, which is server-side request forgery from inside your network, and a scheduled job with a credential written into its own command text.
- No account has connected the Management API or a Postgres role yet, so no policy has been read directly. Every finding that depends on a policy is labelled as inferred.
Boundary
We will not ask you to buy or upgrade a third-party plan to connect. Where a provider gates its configuration behind a paid tier, we read the code instead and say so.
Live site
Give us your app's address and we read it the way a visitor's browser does: the page, up to eight pages it links to on the same site, and up to 48 scripts they load, including your framework's chunks on a CDN.
- In the shipped JavaScript: secret keys that must never reach a browser (a service-role key, not a publishable one) and public source maps that hand out your original source. At fixed paths: a readable .env file, Git metadata, database dumps and credential files, each confirmed by the file's own contents rather than by a 200 status. In the response: whether framing and HTTPS-only are enforced, reported as information.
- Once you confirm the address serves a repository, the two are compared: requests the site makes to its own origin that the repository has no route for, routes the code removed that the site still calls, and what the site started or stopped requesting since the read before. Only complete reads are compared.
Boundary
Every request is an anonymous GET, as a visitor would make it. We do not sign in, submit forms or send attack payloads, and a probe that redirects is not taken as the file. A path assembled at run time from variables is not seen, and anything past eight pages or 48 scripts is reported as unread rather than clean. The read runs every 48 hours, when you ask for a recheck, and after a production deployment your host or the GitHub App reports.
While the product is in its open period, any signed-in account gets everything above and is not charged. Billing is built and switched off; when it is switched on, the price will be on this page before it applies to anyone.
Nothing here writes to your systems. The only thing we create is a pull request on your repository, and you decide whether it merges. Full terms are on the terms page, and what we store is described on the privacy page.