Security
The security controls ZeusQA has, and the ones it does not
ZeusQA drives a real browser from our servers and reads pull requests in your repositories. Every control below is checked against the code by a test that fails if the control is removed, and the list ends with what we do not have.
The short answer
GitHub webhooks are signature-checked, the app cannot push code, and fork pull requests never run on their own. The browser is kept to public addresses: on Cloudflare Browser Run every request is checked, and on ZeusQA's runner machine page loads, frames and fetches are. Neither catches DNS rebinding, a host that gives the check one address and the browser another. On the runner machine, WebSocket and WebRTC connections and redirects of fetches, subresources and form posts are not checked, a name that fails to resolve is let through, and a page load the guard cannot fetch goes to the browser unchecked. These are listed below with the rest of what we do not have. Reports are visible to your workspace, to your API tokens without screenshots, and read-only to the ZeusQA operator for support. Screenshot links in comments and emails need a per-run token, and anyone who can read the comment can open them, so on a public repository they are public. There is no independent audit yet.
Pull requests and the GitHub App
The app reads pull requests, opens a check, keeps one result comment up to date and replies to @zeusqa commands. It cannot push code.
Signed webhooks
Every delivery's
X-Hub-Signature-256is checked with HMAC-SHA256 and a constant-time compare before the body is parsed. An unsigned or mis-signed delivery gets a 401 and nothing runs.Installs belong to the workspace that started them
A webhook only names an installation. The workspace it belongs to comes from a signed link you followed while signed in, valid for 1 hour, and an installation made more than 2 minutes before that link was created cannot be claimed with it.
Code access is read-only
It reads with
contents: read,deployments: readfor the preview URL andmetadata: read. It writes withchecks: writefor the check run,pull_requests: writefor the result comment andissues: writeto read and answer@zeusqacommands, which GitHub files as issue comments. With no write access to contents it cannot push a commit or merge. The vulnerability watch's fix pull requests are the one thing that would need Contents at Read and write; until the App asks for it and an installation accepts, it opens nothing and the advisory's briefing says so.Only owners, members and collaborators start a run
@zeusqa runcounts only when GitHub marks the commenter as the repository's owner, a member of its organization or a collaborator. Everyone else is ignored, and so are bots, ZeusQA's own comments included.Fork pull requests do not run on their own
A pull request from a fork gets a neutral check instead of a run, and a maintainer can start one with
@zeusqa run. Its title and description never reach the agent. A pull request whose fork was deleted counts as a fork.Limits per pull request
Automatic runs: one per commit, unless the preview deployment arrives after a run had to use the project URL. Any run, including
@zeusqa runand a re-requested check: at most one a minute and 6 an hour per pull request. Each one counts against the workspace's plan like a run started by hand.The security review reads the diff as data
On Pro and Team, each new commit's diff goes to claude-sonnet-5-5 through LLM Gateway, or to the newest Claude Sonnet the gateway lists if that one is gone, between markers the diff cannot close, with instructions that text inside it is never a command. Lockfiles, generated, vendored and binary files are left out, the diff is capped at 100k characters, and a finding is kept only when it points at a line the pull request adds. What the model writes is escaped before it reaches the pull request, so it cannot add HTML, links, images or mentions. It is off for each repository until someone turns it on in the repository's settings. Nothing in the repository is run, and it reads the diff with the pull request read access the App already has, so it needs no new permission.
The vulnerability watch reads manifests, never code, and opens pull requests on its own branches
On Pro and Team, a repository with the watch on has package.json, package-lock.json, pnpm-lock.yaml, yarn.lock, requirements.txt, go.mod, Cargo.lock, composer.lock, Gemfile.lock read from the root of its default branch through the GitHub API once a day, and every package version sent to OSV.dev; nothing is cloned or run, and no other file is read. Advisory text is escaped before it reaches a pull request or the app, so it cannot add links, images or mentions, and only https references from the advisory are kept. A pull request is opened only on a
zeusqa/advisory/branch, bumping one manifest to the lowest fixed version, at most 3 a day per repository and 10 open at once; it needs the App's Contents permission at Read and write, which the App does not ask for otherwise. Webhook URLs are accepted only on Slack's and Discord's webhook hosts over https, are stored on the server and shown only in part, and receive only the advisory messages.Secrets in a diff are found by fixed patterns and never shown
Before the review calls the model, every added line of each file GitHub returns a diff for, up to the first 300 files, generated and vendored files and files over the size cap included and lockfiles left out, is checked against fixed patterns: cloud and provider keys, private keys, access tokens, connection strings with a password and random-looking values assigned to secret names. A match is a finding on that line, up to 30 per commit with a count of the rest, critical for a live credential and low in a test, mock or example file, and it is reported even when the model review fails. The check, its annotations and the comment show only a key's public prefix, never the value, and every value the patterns recognise is masked in the diff before the model reads it.
Where the browser can go
A run drives a real browser in its own Cloudflare Container running ZeusQA's runner, or on Cloudflare Browser Run when no container is free. Both keep it to public addresses. Neither catches DNS rebinding, and the runner's check has more gaps, all named below.
Public addresses only
A start URL has to be http or https with no credentials in it. Loopback, private networks, link-local addresses such as cloud metadata, and internal names like
.localor.internalare refused.Names are looked up before the browser connects
Before Cloudflare Browser Run connects, the hostname is resolved over DNS-over-HTTPS and refused if any answer is a private, loopback or link-local address. A lookup that fails is a refusal too. The browser then resolves the name again on its own, so a host that changes its answer in between (DNS rebinding) is not caught.
Every request, not just the first
On Cloudflare Browser Run, redirects, frames, fetches and workers pass the same check while the run is going. A refused request is blocked, and the report lists what was refused.
A guard in ZeusQA's runner, with gaps
In the runner, page loads, frames and fetches are checked against the public-address rule and where the name resolves, and refused ones are aborted. So are the redirect hops of a page load the guard fetches itself, and fetches from scripts the agent runs. WebSocket and WebRTC connections are not checked, a name that fails to resolve is let through, and a page load the guard fails to fetch goes to the browser with its redirects unchecked. Redirects of fetches, subresources and form posts are not checked. The check also resolves the name separately from the browser, so a host that answers the check with a public address and the browser with a private one (DNS rebinding) is not caught.
Preview URLs from webhooks are checked too
A deployment's
environment_urland your preview URL pattern come from webhook data, so they pass a public-address check, and a branch name is cut to letters, digits and dashes before it goes into a hostname.
Reports, accounts and keys
A report shows your app as a signed-in user sees it, so it is treated as private by default.
Runs belong to a workspace
A run's report, screenshots, replay and video are served to members of the workspace that ran it, and to the ZeusQA operator, read-only, for support. Its text, without screenshots, replay or video, is also served to anyone holding one of that workspace's API tokens, through MCP. Screenshots also open from the per-run link described next. There is no public link to a report.
API tokens read runs and nothing else
Owners and admins create up to 20 API tokens per workspace so a coding agent can read its runs over MCP: the run list, a run and its findings. A token cannot start, change or delete anything. Only its SHA-256 hash is stored, so it is shown once. Owners and admins revoke it, and it stops working as soon as the person who made it is no longer an owner or admin. Each token gets 120 calls a minute.
Sign-in is rate limited
Per IP address: 10 sign-in attempts a minute, 5 sign-ups an hour and 3 password reset emails every 10 minutes.
A verified email before any run
An account cannot start a run, from the app or from a pull request, until its email address is verified.
Keys live in Workers secrets
The GitHub App's private key and webhook secret are Cloudflare Workers secrets, never in the repository. A token signed with the key expires 9 minutes after it is made.
What we do not have
- No independent audit or certification yet. If your vendor review needs a third-party report, ZeusQA cannot give you one today.
- The agent is a model, and text on the page it tests can mislead it. A pull request description reaches it marked as data, and the runner tells it page text is data too, but Cloudflare Browser Run does not, and neither stops a determined page.
- There is no choice of region for run data. The web app keeps runs in Cloudflare D1 and KV and screenshots in Cloudflare R2 for 30 days, a Cloudflare Browser Run replay stays in that run's Durable Object storage for 30 days, and a run on ZeusQA's runner keeps its files and video on its own Cloudflare Container's disk, which is erased when the container stops, minutes after the run.
- A run goes through other companies. Its browser runs in a Cloudflare Container with ZeusQA's runner, or on Cloudflare Browser Run when no container is free. Screenshots, page text and your test instructions go through LLM Gateway to the provider of the model you picked, and to Jev's model when Jev is on. On Pro and Team, for each repository with security review turned on, the diff of every new commit goes through LLM Gateway to Anthropic for the review. Run emails, with the summary, findings and last screenshot, are sent through Resend, and the web app sends PostHog product analytics: the pages you visit with their full address, your clicks with the clicked element's text, run reports included, and once you sign in your name, email, user id, workspace and plan.
- The browser guard in ZeusQA's runner has holes. WebSocket and WebRTC connections are not checked there, redirects of fetches, subresources and form posts are not checked, a hostname that fails to resolve is let through, and when the guard cannot fetch a page load itself, the browser loads it and follows its redirects unchecked. Cloudflare Browser Run closes these: it removes WebSocket and WebRTC from the page, checks every redirect, and refuses a failed lookup.
- DNS rebinding is not caught on either runner. The address check and the browser look the hostname up separately, and a verdict is reused for up to a minute, so a host that answers the check with a public address and the browser with 127.0.0.1 or a cloud metadata address such as 169.254.169.254 gets through.
- No single sign-on. Accounts sign in with an email address and a password of at least 10 characters.
- ZeusQA's own pages send no security headers yet: no Content-Security-Policy, nothing that stops another site from framing them (no frame-ancestors or X-Frame-Options), no Strict-Transport-Security, no X-Content-Type-Options: nosniff and no Referrer-Policy.
Try it on your own site
Paste a URL, describe the flow in plain English, and watch a real browser do it. Three runs free, no card.