This is the abridged developer documentation for zkao
# Build on zkao
> Launch security audits, triage findings, and publish results from your terminal, your CI, your code, or your AI agent.
zkao audits code for security bugs, with a focus on cryptography and zero-knowledge circuits. Everything you can do on a project in the app, you can also do through its API.
```bash
npm install -g @zksecurity/zkao-cli
zkao login
zkao scans launch --repo --budget 500
```
## Pick your tool [Section titled “Pick your tool”](#pick-your-tool) [CLI](/reference/cli/)The zkao command. JSON on stdout, built for shells and scripts. [TypeScript SDK](/reference/sdk/)A typed client generated from the OpenAPI spec, with helpers to wait on scans. [REST API](/api/)Every endpoint, request body, and response shape. [GitHub Action](/guides/github-action/)Scan every pull request's diff, or the whole repository on a schedule. [AI agents](/guides/agents/)A skill that teaches Claude Code and other agents to drive your project. [Authentication](/authentication/)Log in from a browser, or mint a scoped token for CI. ## Made for agents too [Section titled “Made for agents too”](#made-for-agents-too) llms.txt An index of these docs at [`/llms.txt`](/llms.txt), and all of them in one file at [`/llms-full.txt`](/llms-full.txt). Markdown pages Every page is also served as Markdown. Add `.md` to its URL, or use the copy button next to its title. Agent skill The skill lives at [`/skill.md`](/skill.md). Point your agent at it. OpenAPI spec The contract the SDK is generated from, at [`/openapi/v1.yaml`](/openapi/v1.yaml).
# Authentication
> Get a project API token through browser approval or by hand, and understand what its scopes and limits allow.
Every call to the zkao API uses a **project API token**. A token belongs to exactly one project. It carries a set of scopes and can be narrowed further to some repositories, an expiry date, and a spend limit. There are two ways to get one: approve the CLI in your browser, or create a token by hand. ## Browser approval [Section titled “Browser approval”](#browser-approval)
```bash
zkao login
```
The CLI starts a device authorization flow: 1. It prints a URL and a short code, and opens the URL in your browser. 2. On the **Authorize a CLI** page, you confirm that the code matches the one in your terminal. 3. You pick a project, review the requested scopes, and approve. You can also set a repository allowlist, an expiry, and a spend limit there. 4. The CLI receives the token and saves it to `~/.zkao/config.json`. Only approve a login you started yourself. Approving mints a real token for the project you select. By default the CLI requests `read`, `scans:launch`, and `findings:write`. The approval page pre-ticks them, and the approver can change them. To request other scopes, pass `--scope`:
```bash
zkao login --scope read guidance:write
```
`zkao login --project ` preselects a project on the approval page. You can approve several projects in one login. Each gets its own saved token, and the first becomes the active project. Use `--no-browser` to print the URL without opening a browser. ### Agents and CI [Section titled “Agents and CI”](#agents-and-ci) A bare `zkao login` blocks until someone approves in the browser. That can take minutes and trip a command timeout. Non-interactive callers split it in two:
```bash
zkao login --no-wait --no-browser # prints the URL and code, then exits
# a person opens the URL and approves
zkao login --resume # polls once and exits
```
Repeat `zkao login --resume` until it prints `Authorized`. `Still waiting for approval` means nobody has approved yet. `--resume --timeout ` waits up to that long instead of polling once. Several logins can be pending at once. `--resume` finishes whichever is approved first and keeps the rest. A pending login expires after a while. Start a new one when `--resume` reports that it expired. ## Tokens created by hand [Section titled “Tokens created by hand”](#tokens-created-by-hand) A project admin can create a token under **Project Settings → Integrations** in the zkao app. Choose a name, the scopes, and optionally a repository allowlist, an expiry, and a spend limit. The token is shown once, together with the project id. It looks like this:
```plaintext
zkao_proj__
```
Store it as a secret. zkao cannot show it again. Hand it to the CLI or SDK through environment variables:
```bash
export ZKAO_API_TOKEN=zkao_proj_...
export ZKAO_PROJECT_ID=
```
Or save it to the config file:
```bash
zkao config set --token zkao_proj_... --project
```
## Scopes [Section titled “Scopes”](#scopes) A token carries a subset of these scopes: | Scope | Allows | | ---------------- | ------------------------------------------------------------------ | | `read` | List and read repositories, scans, findings, presets, and billing. | | `findings:write` | Comment on findings, change severity and resolution, pin a note. | | `guidance:write` | Update a repository’s guidance and manage its audit areas. | | `scans:launch` | Launch and cancel scans. | | `publish` | Publish a finding or a scan as a public page. | A call that needs a scope the token lacks returns `403 forbidden`. ## Limits on a token [Section titled “Limits on a token”](#limits-on-a-token) * **Repository allowlist.** The token only sees the listed repositories. An empty list means every repository in the project. * **Expiry.** The token stops working after this date. * **Spend limit.** The most credits the token may commit to scans in the current billing period. A launch past the limit returns `429`. `zkao whoami` (or `GET /token`) returns the token’s project, organization, scopes, allowlist, expiry, and spend limit. Any valid token can call it, whatever its scopes. ## Errors [Section titled “Errors”](#errors) | Status | Meaning | | ------------------ | ---------------------------------------------------------------------------------------------------------- | | `401 unauthorized` | The token is missing, malformed, expired, or revoked. The response does not say which. | | `403 forbidden` | The token is valid but lacks the scope this call needs. | | `404 not_found` | The resource is outside the token’s project or repository allowlist. zkao does not confirm that it exists. | A token used against another project’s id gets a `404` whose message names the token’s own project. Send the token only in the `Authorization: Bearer` header. A token passed in the query string is rejected with `400`. ## Where the CLI stores credentials [Section titled “Where the CLI stores credentials”](#where-the-cli-stores-credentials) The CLI writes `~/.zkao/config.json` with owner-only permissions. It keeps one token per project, so logging in to a second project does not drop the first.
```bash
zkao config show # active settings and every saved project (tokens masked)
zkao config use # switch the active project
```
Settings resolve in this order, highest first: 1. Flags: `--token`, `--project`, `--base-url`. 2. Environment: `ZKAO_API_TOKEN`, `ZKAO_PROJECT_ID`, `ZKAO_URL`. 3. The saved config file. ## Revoking a token [Section titled “Revoking a token”](#revoking-a-token) Revoke a token under **Project Settings → Integrations**. It stops working at once. A token also stops working when its project or organization is archived. Caution Never commit a token to a repository or paste it into a log. If one leaks, revoke it and create a new one.
# Environments
> Point the CLI and SDK at production or staging, and install the release channel that matches.
zkao runs two public environments: | Environment | App | API base | Docs | | ----------- | ----------------- | -------------------------------- | ------------------------------------------------------ | | Production | `zkao.io` | `https://zkao.io/api/v1` | [`docs.zkao.io`](https://docs.zkao.io) | | Staging | `staging.zkao.io` | `https://staging.zkao.io/api/v1` | [`docs.staging.zkao.io`](https://docs.staging.zkao.io) | The CLI and SDK target production by default. Staging runs ahead of production and gets new features first. Its data and tokens are separate: a production token does not work on staging. ## `ZKAO_URL` [Section titled “ZKAO_URL”](#zkao_url) One environment variable, `ZKAO_URL`, points the CLI and SDK at an environment. It accepts any of these forms:
```bash
ZKAO_URL=staging.zkao.io
ZKAO_URL=https://staging.zkao.io
ZKAO_URL=https://staging.zkao.io/api/v1
```
All three resolve to `https://staging.zkao.io/api/v1`. A bare host gets `https://`. A value that already ends in `/api/v1` is used as is. The CLI’s `--base-url` flag and the SDK’s `baseUrl` option are used verbatim, with no normalization. They win over `ZKAO_URL`. ## The `next` release channel [Section titled “The next release channel”](#the-next-release-channel) The CLI and SDK ship on two npm dist-tags: * `latest` matches production. * `next` matches staging. Its versions look like `X.Y.Z-next.N`. Install the staging channel when you work against staging:
```bash
npm install -g @zksecurity/zkao-cli@next
```
Or pass `--next` to the install script:
```bash
curl -fsSL https://raw.githubusercontent.com/zksecurity/zkao-sdk/main/install.sh | bash -s -- --next
```
A CLI from the `next` channel checks the `next` tag for updates, and a `latest` CLI checks `latest`. ## Logging in to staging [Section titled “Logging in to staging”](#logging-in-to-staging) Set `ZKAO_URL` when you log in:
```bash
ZKAO_URL=staging.zkao.io zkao login --project
```
Each saved project remembers the base URL it was approved on. After that, you no longer need `ZKAO_URL` for that project. ## Switching between projects and environments [Section titled “Switching between projects and environments”](#switching-between-projects-and-environments)
```bash
zkao config show # every saved project, with its base URL
zkao config use # make another saved project active
```
Switching to a staging project points the CLI at staging. Switching back to a production project points it at production. To log in to the other environment, pass `ZKAO_URL` again. Without it, `zkao login` targets the active project’s environment.
# AI agents
> Give a coding agent the zkao skill so it can launch scans, triage findings, and edit guidance on your behalf.
The zkao skill is a single `SKILL.md` file. It teaches an agent to drive a zkao project through the `zkao` CLI or plain HTTP. It covers login, scopes, launching and waiting for scans, triage, guidance, credits, and publishing. An agent with the skill can answer requests like “triage the findings from the last scan” or “run a diff scan of this change”. ## Install the skill [Section titled “Install the skill”](#install-the-skill) * Claude Code The repository is a Claude Code plugin marketplace. Add it, then install the plugin:
```text
/plugin marketplace add zksecurity/zkao-sdk
/plugin install zkao@zkao
```
* Other agents Save the skill into your agent’s skills directory:
```bash
curl -fsSL https://docs.zkao.io/skill.md -o SKILL.md
```
Agents without a skills system can read the same URL as context. The agent also needs the CLI. See the [Quickstart](/quickstart/). ## Docs for agents [Section titled “Docs for agents”](#docs-for-agents) Every page on this site has a plain Markdown version for agents. | URL | What it holds | | -------------------------------------- | -------------------------------------------------------------------------------- | | [`/llms.txt`](/llms.txt) | An index of the docs, following the [llms.txt](https://llmstxt.org/) convention. | | [`/llms-full.txt`](/llms-full.txt) | Every page in one file. | | [`/llms-small.txt`](/llms-small.txt) | Every page in one file, trimmed for small context windows. | | `.md` | One page as Markdown, such as [`/guides/findings.md`](/guides/findings.md). | | [`/openapi/v1.yaml`](/openapi/v1.yaml) | The full API contract. | Each page also has a **Copy as Markdown** button next to its title. ## Logging in from an agent [Section titled “Logging in from an agent”](#logging-in-from-an-agent) A bare `zkao login` waits for browser approval for minutes. Most agent tool calls time out first. Split the login instead, so no command blocks. 1. Start the login. It prints a URL and a code, then exits.
```bash
zkao login --no-wait --no-browser --project
```
`--project` is optional. It preselects the project on the approval page. 2. The agent gives the URL and code to you. You open the URL, check the code, pick the project, and approve. 3. The agent finishes the login.
```bash
zkao login --resume
```
Each call returns at once. `Still waiting for approval` means try again later. `Authorized` means the token is saved. Pass `--timeout ` to wait up to that long in one call. You can approve several projects in one login. Each one gets its own saved token. `zkao config use ` switches between them. Caution Only approve a login you started. Approving mints a real API token for the project you pick. Revoke it any time under **Project Settings → Integrations**. For unattended agents, create a token by hand instead and pass it through `ZKAO_API_TOKEN` and `ZKAO_PROJECT_ID`. See [Authentication](/authentication/). ## Keeping the skill current [Section titled “Keeping the skill current”](#keeping-the-skill-current) The CLI checks npm for a newer release at most once a day. When one exists, every command prints a notice on stderr:
```text
zkao: version X.Y.Z is available (running A.B.C). Update with `npm install -g @zksecurity/zkao-cli`. If you are an agent working from a zkao skill file, update that too: it may describe fewer commands than the API now offers.
```
The notice never touches stdout, so piping output to `jq` keeps working. An agent that sees it should update the CLI and fetch the skill again. Set `ZKAO_NO_UPDATE_CHECK=1` to turn the check off.
# Credits and usage
> How the prepaid credit balance works, what a scan reserves, and how to read balance, usage, and monthly spend over the API.
zkao is pay as you go. You buy credits, and scans spend them. Credits are the only unit the API uses. ## Where the balance lives [Section titled “Where the balance lives”](#where-the-balance-lives) Every project belongs to an organization. The organization holds one prepaid balance. All of its projects share it. A project token still sees one project. The billing endpoints report the organization’s balance as seen from that project. Usage and monthly spend cover only that project’s activity. Credits are bought in the zkao app, not over the API. ## What a scan reserves [Section titled “What a scan reserves”](#what-a-scan-reserves) A launch reserves the scan’s full budget from the balance. The scan then spends only what its analysis uses, never more than the budget. When it finishes, the unused part is released. A cancelled scan releases its reservation too. So the balance has three numbers: | Field | Meaning | | ------------------ | -------------------------------------------------------------------- | | `balanceCredits` | The organization’s total prepaid balance. | | `reservedCredits` | Credits held by the organization’s queued and running scans. | | `availableCredits` | The balance minus reservations. This is what a new scan can reserve. | Active scans in other projects of the organization reduce `availableCredits` too. ## Check the balance [Section titled “Check the balance”](#check-the-balance) * CLI
```bash
zkao billing balance
```
* TypeScript
```ts
const { availableCredits, organization } = await zkao.getBillingBalance();
```
* curl
```bash
curl -H "Authorization: Bearer $ZKAO_API_TOKEN" \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/billing/balance
```
The response names the organization that owns the balance. Check `availableCredits` before choosing a budget. ## When credits run out [Section titled “When credits run out”](#when-credits-run-out) A launch whose budget the balance cannot cover fails with `402 insufficient_credits`. Add credits in the app, or launch with a smaller budget. Reading the balance and usage keeps working with an empty balance. A token can also carry its own spend limit, set when it was created. `zkao whoami` shows it as `spendLimitCredits`. A launch past that limit fails with `429 rate_limited`. ## Read the usage ledger [Section titled “Read the usage ledger”](#read-the-usage-ledger) The ledger lists every credit movement attributed to the project. Each event has a signed `credits` value. Negative means spent, positive means added or refunded. | Type | Meaning | | ------------------ | -------------------------------------------- | | `SCAN_DEDUCTION` | Credits a scan spent. | | `SCAN_REFUND` | Credits returned to the balance from a scan. | | `PURCHASE` | Credits bought. | | `STRIPE_REFUND` | A purchase refunded. | | `ADMIN_ADJUSTMENT` | A manual adjustment by zkao. | * CLI
```bash
zkao billing usage
zkao billing usage --from 2026-09-01 --to 2026-10-01 --limit 200
```
* TypeScript
```ts
const { from, to, events } = await zkao.getBillingUsage({
from: "2026-09-01T00:00:00Z",
});
```
* curl
```bash
curl -H "Authorization: Bearer $ZKAO_API_TOKEN" \
"https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/billing/usage?from=2026-09-01T00:00:00Z"
```
The default range is the last 30 days. The response echoes the range it used. Events come newest first, up to 1,000. Movements made outside any project, such as an organization top-up, are not listed. ## Monthly summary [Section titled “Monthly summary”](#monthly-summary) The summary gives net credits spent on scans and net credits purchased, per calendar month in UTC. Months come newest first, and quiet months show zeros. Ask for up to 24 months. * CLI
```bash
zkao billing summary --months 6
```
* TypeScript
```ts
const months = await zkao.getBillingSummary({ months: 6 });
```
* curl
```bash
curl -H "Authorization: Bearer $ZKAO_API_TOKEN" \
"https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/billing/summary?months=6"
```
# Diff scans
> Audit only what changed between a base and a head commit, which is cheap enough to run on every pull request.
A diff scan audits one change instead of the whole repository. It reads the code that changed between a base and a head commit. The rest of the repository is context, not a target. That makes it cheap and fast. ## How the change is measured [Section titled “How the change is measured”](#how-the-change-is-measured) * The **head** is the commit you scan. Set it with `commitHash` or `branch`, as for any scan. * The **base** is what the change is measured from. It can be a commit SHA, a branch, or a tag. zkao compares the two on GitHub and audits the change from their merge base to the head. The scan records that merge base SHA as its `baseCommit`. So a base branch that moved on since you branched off does not pull its new commits into the diff. ## Launch a diff scan [Section titled “Launch a diff scan”](#launch-a-diff-scan) 1. Push the head commit to GitHub. zkao reads both commits from there. 2. Launch with the diff scan preset and a base. 3. Wait for it, as for any scan. See [Run scans](/guides/scans/#follow-a-scan). * CLI
```bash
zkao scans launch --repo --preset "builtin:Diff Scan" \
--base main --commit
zkao scans wait
```
* TypeScript
```ts
const { scanId } = await zkao.launchScan({
repositoryId: repoId,
presetRef: "builtin:Diff Scan",
baseCommit: "main",
commitHash: headSha,
});
const scan = await zkao.waitForScan(scanId);
```
* curl
```bash
curl -X POST -H "Authorization: Bearer $ZKAO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repositoryId":"","presetRef":"builtin:Diff Scan","baseCommit":"main","commitHash":""}' \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/scans
```
Confirm the preset ref with `zkao presets`. The budget works as for any scan. Leave it out to use the recommended budget. You can also scope a diff scan to audit areas or give it per-scan guidance. ## Diff scan errors [Section titled “Diff scan errors”](#diff-scan-errors) Only a diff scan preset takes a base, and it always needs one. | Status | Code | Cause | | ------ | ----------------------- | --------------------------------------------------------------------------------- | | `400` | `diff_base_required` | A diff scan was launched without a base. | | `400` | `diff_base_not_allowed` | A base was sent with a preset that is not a diff scan. | | `400` | `diff_base_invalid` | The base is not a valid name, does not exist, or shares no history with the head. | | `422` | `diff_empty` | The head changes nothing since the base. There is nothing to audit. | The message says which case applies. A failed comparison on GitHub’s side returns `400 bad_request`. That one is transient, so retry it later. ## Run diff scans in CI [Section titled “Run diff scans in CI”](#run-diff-scans-in-ci) Diff scans fit pull requests. Use the pull request’s base branch as the base and its head commit as the head. The [zkao GitHub Action](/guides/github-action/) does this for you with `scan: diff`. It picks the pull request’s base, or the commit before a push. It can also fail the job when the scan finds issues above a severity.
# Triage findings
> List a project's findings, read one in full, and record your verdict with comments, severity overrides, and resolution statuses.
A finding is one issue a scan reported. Each finding carries two separate statuses. * `triageStatus` is zkao’s own verdict on the finding. You read it. You cannot set it. * `resolutionStatus` is your team’s decision about it. You set it. Reading findings needs the `read` scope. Every change on this page needs `findings:write`. ## List findings [Section titled “List findings”](#list-findings) The list holds every finding in the project, most severe first. Filter it to one scan with its id. * CLI
```bash
zkao findings list
zkao findings list --scan --limit 100
```
* TypeScript
```ts
const { items, total } = await zkao.listFindings({ scanId, limit: 100 });
```
* curl
```bash
curl -H "Authorization: Bearer $ZKAO_API_TOKEN" \
"https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/findings?scanId=&limit=100"
```
The list paginates with `page` and `limit`. The default limit is 50 and the maximum is 100. Each item has these fields. | Field | Meaning | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `title`, `location`, `category` | What the issue is and where. | | `severity` | The effective severity: your override if set, otherwise zkao’s. | | `triageStatus` | zkao’s verdict, such as `CONFIRMED`, `FALSE_POSITIVE` or `DUPLICATE`. | | `confirmationEvidence` | For a `CONFIRMED` finding, `POC` when a proof of concept ran, or `ANALYSIS` when code analysis alone confirmed it. | | `resolutionStatus` | Your team’s decision. New findings start at `NOT_STARTED`. | | `notesCount` | How many comments the finding has. | See [List findings](/api/operations/listfindings/) for the full schema. ## Read one finding [Section titled “Read one finding”](#read-one-finding) The detail view adds the description, the proof of concept report, the recommended fix, the commit, the repository, and every comment. * CLI
```bash
zkao findings get
```
* TypeScript
```ts
const finding = await zkao.getFinding(findingId);
console.log(finding.description, finding.pocReport, finding.notes);
```
* curl
```bash
curl -H "Authorization: Bearer $ZKAO_API_TOKEN" \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/findings/
```
### `ZK-` labels [Section titled “ZK- labels”](#zk--labels) A finding page on zkao shows a short label such as `ZK-3f9a2c1b`. It is the last eight characters of the finding id. Every finding endpoint accepts it in place of the full id, with or without the `ZK-` prefix. A label is unique within a project in practice, but not by construction. A label that matches two findings returns `409`. Use the full id then. ## Comment [Section titled “Comment”](#comment) A comment is a Markdown note on the finding. Your team sees it on zkao. * CLI
```bash
zkao findings comment "Reproduced on the release branch."
```
* TypeScript
```ts
const { noteId } = await zkao.addFindingNote(findingId, "Reproduced on the release branch.");
```
* curl
```bash
curl -X POST -H "Authorization: Bearer $ZKAO_API_TOKEN" -H "Content-Type: application/json" \
-d '{"content":"Reproduced on the release branch."}' \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/findings//notes
```
The response returns the new `noteId`. You can pin that note as the finding’s resolution note with [Pin a note](/api/operations/pinfindingnote/) or the SDK’s `pinFindingNote`. ## Override severity [Section titled “Override severity”](#override-severity) The levels are `CRITICAL`, `HIGH`, `MEDIUM`, `LOW` and `INFO`. Clearing the override restores zkao’s severity. * CLI
```bash
zkao findings severity LOW
zkao findings severity none # clear the override
```
* TypeScript
```ts
await zkao.setFindingSeverity(findingId, "LOW");
await zkao.setFindingSeverity(findingId, null); // clear the override
```
* curl
```bash
curl -X PATCH -H "Authorization: Bearer $ZKAO_API_TOKEN" -H "Content-Type: application/json" \
-d '{"severity":"LOW"}' \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/findings//severity
```
## Set a resolution [Section titled “Set a resolution”](#set-a-resolution) | Status | Meaning | | ---------------- | ----------------------------------- | | `NOT_STARTED` | Nobody has looked at it yet. | | `IN_PROGRESS` | Someone is working on it. | | `RESOLVED` | Fixed. | | `MITIGATED` | Not fixed, but its risk is reduced. | | `WONT_FIX` | Accepted as is. | | `FALSE_POSITIVE` | Not a real issue. | | `DUPLICATE` | Already covered by another finding. | A status alone records the outcome, not the reason. Attach the reason in the same call, in one of two ways. * `note` is free text. It becomes a comment on the finding. * `reason` is a short code from a fixed catalog. It is also recorded as a comment. Pass one or the other. When both are present, the note wins. Both are ignored for `NOT_STARTED` and `IN_PROGRESS`. * CLI
```bash
zkao findings resolution FALSE_POSITIVE \
--note "The caller checks the length before this point."
zkao findings resolution WONT_FIX --reason risk_accepted
```
* TypeScript
```ts
await zkao.setFindingResolution(findingId, "FALSE_POSITIVE", {
note: { content: "The caller checks the length before this point." },
});
await zkao.setFindingResolution(findingId, "WONT_FIX", { reason: "risk_accepted" });
```
* curl
```bash
curl -X PATCH -H "Authorization: Bearer $ZKAO_API_TOKEN" -H "Content-Type: application/json" \
-d '{"resolutionStatus":"WONT_FIX","reason":"risk_accepted"}' \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/findings//resolution
```
Over HTTP, `note` can also reuse an existing comment: `{"existingNoteId": ""}`. ### Reason codes [Section titled “Reason codes”](#reason-codes) Each status offers its own codes. A code from another status returns `400` with the valid list. | Status | Codes | | ---------------- | ------------------------------------------------------------------------------------------- | | `RESOLVED` | `fixed_in_code`, `fixed_upstream`, `code_removed` | | `MITIGATED` | `compensating_control`, `limited_exposure`, `monitored` | | `FALSE_POSITIVE` | `not_reachable`, `guarded_elsewhere`, `intended_behavior`, `misread_code`, `bad_assumption` | | `WONT_FIX` | `risk_accepted`, `out_of_scope`, `not_worth_fixing`, `code_being_removed` | | `DUPLICATE` | `duplicate_of_finding`, `same_root_cause` | ## A triage pass [Section titled “A triage pass”](#a-triage-pass) 1. Find the latest completed scan.
```bash
zkao scans list
```
2. List its findings.
```bash
zkao findings list --scan
```
3. Read each one in full, including the proof of concept.
```bash
zkao findings get
```
4. Record the verdict, with the reason.
```bash
zkao findings severity MEDIUM
zkao findings resolution RESOLVED --reason fixed_in_code
```
Some knowledge applies to the whole repository, not one finding. A known non-issue is one example. A comment does not carry it into future scans. Put it in the [repository guidance](/guides/guidance/) instead.
# GitHub Action
> Launch a zkao scan from a GitHub workflow, report its findings in the job summary, and gate pull requests on severity.
The [`zksecurity/zkao-action`](https://github.com/zksecurity/zkao-action) action scans the commit a workflow runs on. By default it starts the scan and returns. It can also wait, write the findings to the job summary, and fail the job on severe findings. The action needs no checkout. zkao reads the commit from GitHub itself. ## Before you start [Section titled “Before you start”](#before-you-start) 1. Add the repository to a zkao project. A scan needs at least one project member with GitHub access to the repository. 2. Create a project API token with the `read` and `scans:launch` scopes. See [Authentication](/authentication/). 3. In the GitHub repository settings, store the token as the secret `ZKAO_API_TOKEN`. Store the project id as the variable `ZKAO_PROJECT_ID`. 4. Keep credits on the organization. A scan reserves its budget at launch. See [Credits](/guides/credits/). ## Scan every push to main [Section titled “Scan every push to main”](#scan-every-push-to-main)
```yaml
name: zkao
on:
push:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: zksecurity/zkao-action@v1
with:
token: ${{ secrets.ZKAO_API_TOKEN }}
project: ${{ vars.ZKAO_PROJECT_ID }}
```
This launches a quick look of each push and moves on. The results are on zkao when the scan finishes. ## Gate pull requests on a diff scan [Section titled “Gate pull requests on a diff scan”](#gate-pull-requests-on-a-diff-scan) A diff scan audits only the change a pull request makes. It is cheap and fast. In `gate` mode, the job fails when open findings reach `fail-on`.
```yaml
name: zkao
on:
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: zksecurity/zkao-action@v1
with:
token: ${{ secrets.ZKAO_API_TOKEN }}
project: ${{ vars.ZKAO_PROJECT_ID }}
scan: diff
mode: gate
fail-on: high
```
The diff runs from the merge base of `base` to the scanned commit. `base` defaults to the pull request’s base, or to the commit before a push. The rest of the repository is context, not a target. See [Diff scans](/guides/diff-scans/). On `pull_request` and `pull_request_target` events, the action scans the head of the pull request. On every other event it scans `github.sha`. ## Modes [Section titled “Modes”](#modes) | `mode` | What happens | | ------------------ | ----------------------------------------------------------------------------------------------------------- | | `launch` (default) | Starts the scan and returns. The job never waits or fails on findings. | | `wait` | Waits for the scan and writes the findings to the job summary. The job fails only if the scan itself fails. | | `gate` | Waits, reports, and fails the job when open findings reach `fail-on`. | ## What to run [Section titled “What to run”](#what-to-run) | `scan` | Runs | | ---------------------- | ----------------------------------------------------------------------------------------- | | `quick-look` (default) | The core techniques in one cheap pass. | | `deep-audit` | The full methodology over the whole repository. | | `diff` | Only the change since `base`. | | a preset ref | That preset, such as `builtin:Deep Audit` or a custom one. List refs with `zkao presets`. | ## Inputs [Section titled “Inputs”](#inputs) | Input | Default | What it does | | --------------- | ----------------- | ------------------------------------------------------------------------------------------------------------- | | `token` | required | Project API token. Use a secret. | | `project` | required | The zkao project id. | | `budget` | recommended | Credit budget for the scan. Defaults to what zkao recommends for this scan type on this repository. | | `mode` | `launch` | `launch`, `wait`, or `gate`. | | `scan` | `quick-look` | `quick-look`, `deep-audit`, `diff`, or a preset ref. | | `base` | see above | For `scan: diff`, the commit the change is measured from. | | `fail-on` | `high` | For `mode: gate`, the severity that fails the job: `critical`, `high`, `medium`, `low`, or `info`. | | `repository` | matched by name | The zkao repository id. By default, the project repository whose owner and name match this GitHub repository. | | `commit` | see above | Commit to scan. | | `branch` | workflow branch | Branch the commit is on. | | `areas` | whole repository | Comma-separated audit area keys to scope the scan to. | | `guidance-file` | none | A file whose content replaces the repository’s guidance for this scan. | | `timeout` | `10800` | Seconds to wait in `wait` and `gate` modes. The scan keeps running on zkao after a timeout. | | `summary` | `true` | Write the scan link, and the findings once waited for, to the job summary. | | `base-url` | `https://zkao.io` | The zkao instance. | | `cli-version` | pinned | Version of `@zksecurity/zkao-cli` the action runs. | ## Outputs [Section titled “Outputs”](#outputs) | Output | Meaning | | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `scan-id` | Id of the launched scan. | | `scan-url` | The scan on zkao. | | `status` | `QUEUED` in `launch` mode. `COMPLETED`, `FAILED` or `CANCELLED` once waited for. | | `findings-total` | Open findings, excluding false positives and duplicates. Empty in `launch` mode. | | `findings-critical`, `findings-high`, `findings-medium`, `findings-low`, `findings-info` | Open findings by severity. | Use them in later steps:
```yaml
- uses: zksecurity/zkao-action@v1
id: zkao
with:
token: ${{ secrets.ZKAO_API_TOKEN }}
project: ${{ vars.ZKAO_PROJECT_ID }}
mode: wait
- run: echo "${{ steps.zkao.outputs.findings-total }} open findings at ${{ steps.zkao.outputs.scan-url }}"
```
## Keeping the cost in check [Section titled “Keeping the cost in check”](#keeping-the-cost-in-check) A full scan on every push adds up. These keep it down. * Run on `pull_request`, or on `main` only. * Use `scan: diff` for pull requests. * Pass `areas` to scan only the audit areas a change touches. List area keys with `zkao areas list `. * Set `budget` to cap each scan. A scan of a repository zkao is still analyzing waits for that analysis to finish first. ## How it works [Section titled “How it works”](#how-it-works) The action is a composite step. It runs the published [`@zksecurity/zkao-cli`](https://www.npmjs.com/package/@zksecurity/zkao-cli) against the public API. The runner needs `node` and `jq`, which every GitHub-hosted runner has.
# Repository guidance
> Steer every scan of a repository with standing guidance on scope, threat model, and accepted risks, and keep it current over the API.
Guidance tells zkao what matters in your code. It covers scope, attacker powers, trusted components, security properties, severity expectations, and accepted risks. Good guidance makes scans sharper and cuts false positives. ## Three layers [Section titled “Three layers”](#three-layers) Guidance can come from three places. They stack, and a later layer can refine an earlier one. 1. **A committed `zkao.md`** at the repository root. It ships with your code and is read at the scanned commit. It changes only when you commit a change. 2. **Repository guidance**, stored in zkao. It applies to every scan of the repository without touching the code. This page is about this layer. 3. **Scan guidance**, sent with one launch. It replaces repository guidance for that scan only. See [Guide one scan](/guides/scans/#guide-one-scan). Repository guidance is the right home for a repository you do not control. It also suits knowledge you do not want to commit. Guidance is a strong steer, not an access-control boundary or a guaranteed file filter. To limit what a scan audits, scope it to [audit areas](/guides/scans/#scope-a-scan-to-audit-areas). ## Read guidance [Section titled “Read guidance”](#read-guidance) * CLI
```bash
zkao guidance get
```
* TypeScript
```ts
const { content, updatedAt } = await zkao.getRepositoryGuidance(repoId);
```
* curl
```bash
curl -H "Authorization: Bearer $ZKAO_API_TOKEN" \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/repositories//guidance
```
`content` is `null` when no guidance is set. Reading needs the `read` scope. The committed `zkao.md` is not part of this response. ## Set or clear guidance [Section titled “Set or clear guidance”](#set-or-clear-guidance) Writing needs the `guidance:write` scope. Each change records a revision. Writing the content already stored is a no-op that returns `unchanged: true`. Guidance is limited to 100,000 characters. * CLI
```bash
zkao guidance set guidance.md
cat guidance.md | zkao guidance set -
zkao guidance clear
```
* TypeScript
```ts
const current = await zkao.getRepositoryGuidance(repoId);
await zkao.setRepositoryGuidance(repoId, newContent, {
expectedContent: current.content,
});
// Clear it.
await zkao.setRepositoryGuidance(repoId, null);
```
* curl
```bash
curl -X PUT -H "Authorization: Bearer $ZKAO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content":"# Scope\nAudit src/verifier only.","expectedContent":null}' \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/repositories//guidance
```
### Avoid overwriting a concurrent edit [Section titled “Avoid overwriting a concurrent edit”](#avoid-overwriting-a-concurrent-edit) Teammates, and accepted suggestions in the app, also edit guidance. `expectedContent` protects you from clobbering their change. * Send the content you last read, or `null` if none was set. * If the stored guidance changed since then, the write fails with `409 conflict`. * Read it again, merge your change, and retry. * Leave `expectedContent` out for last writer wins. The CLI does the compare-and-set for you. `zkao guidance set` and `zkao guidance clear` read the current guidance first and send it as `expectedContent`. Pass `--force` to skip that check and overwrite. ## Keep it lean [Section titled “Keep it lean”](#keep-it-lean) Every line is read on every scan. A short file the analysis takes in beats a long one it skims. Use repository-relative paths. Never include credentials or secrets. Put durable facts in repository guidance or `zkao.md`. Put a one-off focus in scan guidance instead. Triage often surfaces knowledge that applies to the whole repository, such as a known non-issue. A comment on one finding does not carry it forward, but guidance does.
# Publish results
> Turn a finding or a completed scan into a public page you can share, optionally behind a password.
Publishing creates a public page on zkao for one finding or one scan. Anyone with the link can read it. Nothing else in the project becomes visible. Publishing needs the `publish` scope. `zkao login` does not request it by default. Ask for it when you log in:
```bash
zkao login --scope read scans:launch findings:write publish
```
## Publish a finding [Section titled “Publish a finding”](#publish-a-finding) A published finding shows its title, severity, repository, commit, description, impact, and recommendation. You can also attach one of its comments as the team’s public response. Pass that comment’s id as the note. * CLI
```bash
zkao findings publish
zkao findings publish --note --password
```
* TypeScript
```ts
const { publicId, accessPassword } = await zkao.publishFinding(findingId, {
noteId,
withPassword: true,
});
```
* curl
```bash
curl -X POST -H "Authorization: Bearer $ZKAO_API_TOKEN" -H "Content-Type: application/json" \
-d '{"noteId":"","withPassword":true}' \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/findings//publish
```
The note must belong to the finding. Another finding’s note returns `400`. The finding id also accepts a [`ZK-` label](/guides/findings/#zk--labels). ## Publish a scan [Section titled “Publish a scan”](#publish-a-scan) A published scan lists the scan’s reported findings, most severe first. It covers the findings zkao confirmed, and those it marked as needing review. Only a `COMPLETED` scan can be published. * CLI
```bash
zkao scans publish
zkao scans publish --password
```
* TypeScript
```ts
const { publicId, accessPassword } = await zkao.publishScan(scanId, { withPassword: true });
```
* curl
```bash
curl -X POST -H "Authorization: Bearer $ZKAO_API_TOKEN" -H "Content-Type: application/json" \
-d '{"withPassword":true}' \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/scans//publish
```
## The result [Section titled “The result”](#the-result) Both calls return the same shape.
```json
{
"artifactId": "…",
"publicId": "…",
"accessPassword": null
}
```
The public page lives at one of these URLs. * `https://zkao.io/public/findings/` * `https://zkao.io/public/scans/` ## Password protection [Section titled “Password protection”](#password-protection) With `withPassword`, zkao generates a password and returns it as `accessPassword`. Visitors must enter it to read the page. Share it separately from the link. Caution Publishing again keeps the same link, but resets the options. A call without `withPassword` removes the password. A call with it generates a new one. For a finding, a call without a note removes the public response. ## Unpublishing [Section titled “Unpublishing”](#unpublishing) The API publishes but does not unpublish. Unpublish a page from the finding or scan on zkao. See [Publish a finding](/api/operations/publishfinding/) and [Publish a scan](/api/operations/publishscan/) for the full schemas.
# Run scans
> Pick a repository and a scan type, launch a scan with a credit budget, follow it to the end, and cancel it if needed.
A scan audits one commit of one repository. You choose the scan type and a credit budget. zkao reserves the budget, runs the analysis, and records findings on the project. The TypeScript examples use a client created as shown in the [SDK reference](/reference/sdk/). The curl examples assume `ZKAO_API_TOKEN` and `ZKAO_PROJECT_ID` are set. ## The flow [Section titled “The flow”](#the-flow) 1. Find the repository and check that it is ready. 2. Pick a scan type (a preset). 3. Launch the scan with a budget. 4. Wait for it to finish. 5. Read its findings. See [Triage findings](/guides/findings/). ## Find a repository [Section titled “Find a repository”](#find-a-repository) List the repositories in the project. A token restricted to some repositories only sees those. * CLI
```bash
zkao repos
```
* TypeScript
```ts
const repositories = await zkao.listRepositories();
```
* curl
```bash
curl -H "Authorization: Bearer $ZKAO_API_TOKEN" \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/repositories
```
Each repository has a `readiness` field. A repository added moments ago is `analyzing`. zkao makes a one-time pass over it first, and that pass costs no credits. A launch during that pass fails with `409 repository_initializing`. Wait for `ready` instead of retrying the launch. * CLI
```bash
zkao repos:wait
```
* TypeScript
```ts
await zkao.waitForRepositoryReady(repoId);
```
* curl
```bash
# Poll until the repository reports "readiness": "ready".
curl -H "Authorization: Bearer $ZKAO_API_TOKEN" \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/repositories
```
The SDK waits up to 30 minutes by default. It backs off between polls and honors `Retry-After`. ## Pick a scan type [Section titled “Pick a scan type”](#pick-a-scan-type) A preset is a scan type. Each one runs a fixed set of analyses. Broader types run more analysis and need a larger budget. * CLI
```bash
zkao presets
```
* TypeScript
```ts
const presets = await zkao.listScanPresets();
```
* curl
```bash
curl -H "Authorization: Bearer $ZKAO_API_TOKEN" \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/scan-presets
```
Each preset has a `ref`, a `name`, a `description`, and a `minCredits` floor. Pass the `ref` when you launch. Without one, the launch uses the first active preset. ## Choose a budget [Section titled “Choose a budget”](#choose-a-budget) The budget is the most a scan may spend, in credits. zkao reserves it at launch and never charges more. Credits the scan does not use go back to the balance. You can leave the budget out. zkao then picks the budget it recommends for this scan type and scope on this repository. The recommendation follows what past scans of the repository spent. The first scan of a repository starts at the scan type’s minimum. The launch response tells you which budget was used. A scan that runs short of budget stops early, so treat its coverage as partial. See [Credits and usage](/guides/credits/) for how reservations affect the balance. ## Launch a scan [Section titled “Launch a scan”](#launch-a-scan) * CLI
```bash
zkao scans launch --repo --preset [ --budget 500
```
* TypeScript
```ts
const { scanId, creditBudget } = await zkao.launchScan({
repositoryId: repoId,
presetRef: ref,
creditBudget: 500,
});
```
* curl
```bash
curl -X POST -H "Authorization: Bearer $ZKAO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repositoryId":"","presetRef":"][","creditBudget":500}' \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/scans
```
The API answers `202` with `scanId`, `queued`, and `creditBudget`. Launching needs the `scans:launch` scope. ### Choose the commit [Section titled “Choose the commit”](#choose-the-commit) By default a scan runs on the head of the repository’s default branch. * `branch` scans the head of that branch, resolved when the scan launches. * `commitHash` pins one commit (7 to 40 hex characters). It wins over `branch`. * `commitMessage` is optional metadata shown with the scan. The CLI flags are `--branch`, `--commit`, and `--message`. The commit must be pushed to GitHub, because zkao reads it from there. ### Scope a scan to audit areas [Section titled “Scope a scan to audit areas”](#scope-a-scan-to-audit-areas) An audit area is a named subsystem of the repository. zkao discovers areas when it maps the code. You can add your own. Scoping a scan to a few areas keeps a large repository affordable. * CLI
```bash
zkao areas list
zkao scans launch --repo --area --area
```
* TypeScript
```ts
const { areas } = await zkao.listAuditAreas(repoId);
await zkao.launchScan({
repositoryId: repoId,
auditAreaKeys: [areas[0].key],
});
```
* curl
```bash
curl -H "Authorization: Bearer $ZKAO_API_TOKEN" \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/repositories//audit-areas
```
Each area reports its size in files and lines from one branch’s map. Pass `--branch` (or `branch`) to read another branch’s sizes. Leave the area list out to scan the whole repository. A key the current map no longer names fails the launch. zkao never drops it silently, so a budget sized for one area never runs against everything. An area is a name and a description, never a file list. Each scan resolves it against the code at its own commit. * CLI
```bash
zkao areas add "Proof verifier" --description "On-chain verifier and its key loading"
zkao areas rm
```
* TypeScript
```ts
const area = await zkao.createAuditArea(
repoId,
"Proof verifier",
"On-chain verifier and its key loading",
);
await zkao.deleteAuditArea(repoId, area.key);
```
* curl
```bash
curl -X POST -H "Authorization: Bearer $ZKAO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Proof verifier","description":"On-chain verifier and its key loading"}' \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/repositories//audit-areas
```
Adding and deleting areas needs the `guidance:write` scope. Only custom areas can be deleted. Deleting a discovered area returns `409`. ### Guide one scan [Section titled “Guide one scan”](#guide-one-scan) Guidance tells a scan what matters: scope, threat model, and accepted risks. A launch can carry guidance for that scan only. It replaces the repository’s stored guidance for this run. A `zkao.md` committed in the repository still applies underneath. * CLI
```bash
zkao scans launch --repo --guidance focus.md
cat focus.md | zkao scans launch --repo --guidance -
```
* TypeScript
```ts
await zkao.launchScan({
repositoryId: repoId,
guidance: "Focus on the proof verifier. The prover is trusted.",
});
```
* curl
```bash
curl -X POST -H "Authorization: Bearer $ZKAO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repositoryId":"","guidance":"Focus on the proof verifier."}' \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/scans
```
In the API, the `guidance` field has three states: * Left out: the scan uses the repository’s stored guidance. * A string: that text replaces it for this scan. * `null` or an empty string: the scan runs with no stored guidance layer. Guidance is limited to 100,000 characters. See [Repository guidance](/guides/guidance/) for the standing layer. ## Follow a scan [Section titled “Follow a scan”](#follow-a-scan) A scan moves through these statuses: | Status | Meaning | | ------------ | ------------------------------ | | `QUEUED` | Accepted and waiting to start. | | `PROCESSING` | Running. | | `COMPLETED` | Finished. Findings are ready. | | `FAILED` | Stopped on an error. | | `CANCELLED` | Cancelled before it finished. | The last three are terminal. While a scan runs, its `progress` field reports `phasesCompleted`, `phasesTotal`, and a weighted `percent`. There is no time estimate. How long a scan takes depends on the repository and its guidance. A finished scan carries a `findingsSummary` with counts by severity. The easiest way to follow a scan is to wait on it. * CLI
```bash
zkao scans wait
zkao scans wait --timeout 7200
```
* TypeScript
```ts
const scan = await zkao.waitForScan(scanId, {
onPoll: (s) => console.log(s.status, s.progress?.percent),
});
```
* curl
```bash
curl -i -H "Authorization: Bearer $ZKAO_API_TOKEN" \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/scans/
```
`zkao scans wait` prints each poll on stderr and the final scan as JSON on stdout. Its `--timeout` is in seconds. The SDK’s `waitForScan` gives up after one hour by default. Set `timeoutMs` for longer scans, and pass a `signal` to abort. ### Poll politely [Section titled “Poll politely”](#poll-politely) If you poll by hand, read the `Retry-After` header on the scan response. It says how many seconds to wait before the next poll. It is present only while the scan is not terminal. A token that polls too fast gets `429 rate_limited`, also with `Retry-After`. Wait that long before the next request. The CLI and SDK do this for you. ## List scans [Section titled “List scans”](#list-scans) Scans are listed newest first. The list is paginated with `page` and `limit` (at most 100, default 50). * CLI
```bash
zkao scans list --limit 20
zkao scans get
```
* TypeScript
```ts
const { items, total } = await zkao.listScans({ limit: 20 });
const detail = await zkao.getScan(items[0].id);
```
* curl
```bash
curl -H "Authorization: Bearer $ZKAO_API_TOKEN" \
"https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/scans?limit=20"
```
## Cancel a scan [Section titled “Cancel a scan”](#cancel-a-scan) You can cancel a queued or running scan. zkao stops its work, marks it `CANCELLED`, and releases its reserved credits. Work already in flight settles what it actually spent as it winds down. * CLI
```bash
zkao scans cancel
```
* TypeScript
```ts
await zkao.cancelScan(scanId);
```
* curl
```bash
curl -X POST -H "Authorization: Bearer $ZKAO_API_TOKEN" \
https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/scans//cancel
```
Cancelling needs the `scans:launch` scope. A scan that already reached `COMPLETED` or `FAILED` cannot be cancelled and returns `400`. ## Launch errors [Section titled “Launch errors”](#launch-errors) | Status | Code | What to do | | ------ | ------------------------- | ----------------------------------------------------------------------------------------- | | `400` | `bad_request` | Fix the request. The message names the problem. | | `400` | `diff_base_*` | See [Diff scans](/guides/diff-scans/). | | `402` | `insufficient_credits` | Add credits, or launch with a smaller budget. | | `403` | `forbidden` | The token lacks `scans:launch`. | | `404` | `not_found` | The repository is outside the token’s project or repository allowlist. | | `409` | `repository_initializing` | Wait for the repository to be `ready`. | | `422` | `diff_empty` | The diff scan has nothing to audit. | | `429` | `rate_limited` | The token’s spend limit is reached, or it sent too many requests. Wait for `Retry-After`. | The full contract is in the API reference for [launchScan](/api/operations/launchscan/) and [getScan](/api/operations/getscan/).
# Quickstart
> Install the zkao CLI, authorize it for a project, launch a scan, and read its findings.
This page takes you from nothing to your first findings in a terminal. You need Node 18 or later and a zkao project with at least one repository. 1. **Install the CLI.**
```bash
npm install -g @zksecurity/zkao-cli
```
This installs the `zkao` command. For a one-off run without installing, prefix commands with `npx @zksecurity/zkao-cli`. You can also use the install script:
```bash
curl -fsSL https://raw.githubusercontent.com/zksecurity/zkao-sdk/main/install.sh | bash
```
2. **Authorize it for a project.**
```bash
zkao login
```
The CLI opens a browser page and prints a short code. Check that the page shows the same code. Pick a project, review the permissions, and approve. The CLI saves the token for that project. There is nothing to copy. Confirm which project you are connected to:
```bash
zkao whoami
```
See [Authentication](/authentication/) for tokens, scopes, and non-interactive logins. 3. **Find a repository.**
```bash
zkao repos
```
Note the `id` of the repository to scan. Its `readiness` must be `ready`. A repository added moments ago is `analyzing` for a short while. `zkao repos:wait ` blocks until it is ready. 4. **Pick a scan preset.**
```bash
zkao presets
```
Each preset is a scan type. Note the `ref` of the one you want. 5. **Launch the scan.**
```bash
zkao scans launch --repo --preset ][
```
The response carries the `scanId` and the budget the scan reserved, in credits. Without `--budget`, zkao picks the budget it recommends for this scan type on this repository. Pass `--budget ` to set your own ceiling. `zkao billing balance` shows the credits available to the project. 6. **Wait for it to finish.**
```bash
zkao scans wait
```
The scan moves from `QUEUED` to `PROCESSING` to `COMPLETED`. Scans take minutes. `wait` polls at the pace the server asks for, so you do not need your own loop. 7. **Read the findings.**
```bash
zkao findings list --scan
zkao findings get
```
`get` returns the full finding, including the description, proof of concept, and recommended fix. Every command prints JSON on stdout, so it pipes cleanly into `jq` or a script. `zkao --help` lists every command. ## Launch without the CLI [Section titled “Launch without the CLI”](#launch-without-the-cli) The same launch over plain HTTP or the TypeScript SDK: * curl
```bash
curl -X POST "https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/scans" \
-H "Authorization: Bearer $ZKAO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"repositoryId":"","presetRef":"]["}'
```
* TypeScript
```ts
import { ZkaoClient } from "@zksecurity/zkao-sdk";
const zkao = new ZkaoClient({
token: process.env.ZKAO_API_TOKEN!,
projectId: process.env.ZKAO_PROJECT_ID!,
});
const { scanId } = await zkao.launchScan({ repositoryId: "", presetRef: "][" });
await zkao.waitForScan(scanId);
const { items } = await zkao.listFindings({ scanId });
```
## Next steps [Section titled “Next steps”](#next-steps) * [Scans](/guides/scans/) covers presets, budgets, branches, areas, and per-scan guidance. * [Triage findings](/guides/findings/) covers severity, resolution, and notes. * [CLI reference](/reference/cli/) lists every command and flag.
# API conventions
> Base URL, authentication, errors, pagination, rate limits, and identifiers shared by every zkao API endpoint.
These rules apply to every endpoint in the [API reference](/api/). The machine-readable contract is the OpenAPI spec at [`/openapi/v1.yaml`](/openapi/v1.yaml). ## Base URL [Section titled “Base URL”](#base-url)
```plaintext
https://zkao.io/api/v1
```
Project resources live under `/projects/{projectId}`, for example `/projects/{projectId}/scans`. The one exception is `GET /token`, which needs no project id. Staging uses `https://staging.zkao.io/api/v1`. See [Environments](/environments/). ## Authentication [Section titled “Authentication”](#authentication) Send a project API token in the `Authorization` header:
```plaintext
Authorization: Bearer zkao_proj__
```
A token in the query string is rejected with `400`. See [Authentication](/authentication/) for how to get a token and what its scopes allow. ## Requests and responses [Section titled “Requests and responses”](#requests-and-responses) Request and response bodies are JSON. Send `Content-Type: application/json` with a body. Timestamps are ISO 8601 strings. ## Errors [Section titled “Errors”](#errors) Every error uses one envelope:
```json
{ "error": { "code": "not_found", "message": "Not found" } }
```
Branch on `code`. The `message` is for people and can change. | Status | Code | Meaning | | ------ | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | | `400` | `bad_request` | The request is invalid. The message says why. | | `400` | `diff_base_required`, `diff_base_not_allowed`, `diff_base_invalid` | A diff scan’s base is missing, given to a preset that refuses it, or malformed. | | `401` | `unauthorized` | The token is missing, malformed, expired, or revoked. | | `402` | `insufficient_credits` | The organization’s credits cannot cover the scan. | | `403` | `forbidden` | The token lacks the scope this call needs. | | `404` | `not_found` | The resource does not exist, or is outside the token’s project or repository allowlist. | | `409` | `conflict` | A concurrent change, such as guidance edited since you read it, or an ambiguous `ZK-` label. | | `409` | `repository_initializing` | The repository is still being analyzed. Retry once its `readiness` is `ready`. | | `422` | `diff_empty` | A diff scan has no change to audit between the base and the head. | | `429` | `rate_limited` | Too many requests, or the token’s spend limit is reached. | | `500` | `internal` | Something failed on zkao’s side. | zkao answers `404`, not `403`, for anything outside a token’s reach. That way a token cannot probe which ids exist. The SDK throws `ZkaoApiError` for any non-2xx response, with `.status`, `.code`, and `.message`. The CLI prints `Error (]): ` on stderr and exits with code 1. ## Pagination [Section titled “Pagination”](#pagination) List endpoints for scans and findings take `page` and `limit` query parameters:
```bash
curl -H "Authorization: Bearer $ZKAO_API_TOKEN" \
"https://zkao.io/api/v1/projects/$ZKAO_PROJECT_ID/findings?page=2&limit=100"
```
* `page` starts at 1. * `limit` defaults to 50 and is capped at 100. They return:
```json
{ "items": [], "page": 2, "limit": 100, "total": 312 }
```
Keep requesting pages until `page * limit >= total`. Other lists, such as repositories and presets, return everything in one response. ## Rate limits and polling [Section titled “Rate limits and polling”](#rate-limits-and-polling) Each token has a request ceiling per time window. Normal use never reaches it. Past it, the API returns `429 rate_limited` with a `Retry-After` header in seconds. Wait that long before retrying. While a scan is still running, `GET /scans/{scanId}` returns an advisory `Retry-After` header. It says how long to wait before polling again. Honor it instead of polling in a tight loop. The SDK’s `waitForScan` and the CLI’s `zkao scans wait` do this for you. A launch that would push a token past its spend limit also returns `429`. ## Identifiers [Section titled “Identifiers”](#identifiers) Ids are opaque strings. Treat them as such. A finding also has a `ZK-` label, shown on its page. The label is the id’s last eight characters. Every endpoint that takes a `findingId` also accepts the label, with or without the `ZK-` prefix. A label that matches more than one finding in the project returns `409 conflict`. Use the full id then. Projects, organizations, and repositories also carry a `slug`. Slugs form the readable app URLs, such as `/orgs//projects//repos/`. API paths use ids, not slugs. ## Credits [Section titled “Credits”](#credits) Every budget, balance, and spend in the API is in credits. Credits are the only unit the API reports. See [Credits and billing](/guides/credits/). ## Versioning [Section titled “Versioning”](#versioning) The major version is part of the path: `/api/v1`. New endpoints and new response fields can appear within v1. Ignore fields your client does not recognize.
# CLI reference
> Every zkao command, argument, and option, with the settings and environment variables the CLI reads.
The `zkao` command ships in the `@zksecurity/zkao-cli` package. It needs Node 18 or later.
```bash
npm install -g @zksecurity/zkao-cli
```
To run a single command without installing, use `npx @zksecurity/zkao-cli `. `zkao --help` and `zkao --help` always list the commands of the version you run. ## Output and exit codes [Section titled “Output and exit codes”](#output-and-exit-codes) * Commands that call the API print their result as JSON on stdout. Pipe it to `jq` or parse it. * Progress lines, warnings, and the update notice go to stderr. They never mix into the JSON. * `login`, `config set`, and `config use` print plain status lines instead of JSON. * The exit code is `0` on success and `1` on any failure. * An API error prints `Error (): ` on stderr. The codes are listed in [API conventions](/reference/api-conventions/). ## Global options [Section titled “Global options”](#global-options) These options work with every command. | Option | Meaning | | ------------------ | ------------------------------------------------------------------------------------------- | | `--token ` | Project API token. Overrides `ZKAO_API_TOKEN` and saved credentials. | | `--project ` | Project id. Overrides `ZKAO_PROJECT_ID` and the active project. | | `--base-url ` | API base URL, used exactly as given. Prefer `ZKAO_URL` unless you need a non-standard base. | | `-V, --version` | Print the CLI version. | | `-h, --help` | Show help for the CLI or a command. | ## Settings and environment [Section titled “Settings and environment”](#settings-and-environment) Each setting is taken from the first source that has it. 1. The command-line flag. 2. The environment variable. 3. The config file, `~/.zkao/config.json`. | Variable | Meaning | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `ZKAO_API_TOKEN` | Project API token (`zkao_proj_...`). | | `ZKAO_PROJECT_ID` | Project id. Picks that project’s saved token when no token is given. | | `ZKAO_URL` | Environment to target. Accepts a host, an origin, or a full API URL, such as `staging.zkao.io`. See [Environments](/environments/). | | `ZKAO_NO_UPDATE_CHECK` | Set to any value to turn off the update notice. | | `ZKAO_DEBUG` | Set to any value to log update-check failures on stderr. | The config file stores one token per project and remembers which project is active. Each saved project keeps its own base URL, so staging and production projects can live side by side. The file is written with owner-only permissions. A login started with `--no-wait` is kept in `~/.zkao/pending-login.json` until it resolves. ## Update notice [Section titled “Update notice”](#update-notice) At most once a day, after a command has printed its output, the CLI asks the npm registry for the newest version. When a newer one exists, later commands print one line on stderr:
```plaintext
zkao: version X.Y.Z is available (running A.B.C). Update with `npm install -g @zksecurity/zkao-cli`. ...
```
A prerelease install (`X.Y.Z-next.N`) follows the `next` channel and suggests `@zksecurity/zkao-cli@next`. The check never delays a command or changes its output. ## Authentication [Section titled “Authentication”](#authentication) ### `zkao login` [Section titled “zkao login”](#zkao-login) Authorize the CLI in your browser. The CLI prints a URL and a short code. You open the URL, confirm the code matches, pick one or more projects, and approve. The CLI saves a token for each approved project and makes the first one active. | Option | Meaning | | --------------------- | --------------------------------------------------------------------------------------------------- | | `--no-browser` | Print the URL instead of opening a browser. | | `--no-wait` | Start the login, print the URL and code, and exit. Finish it with `--resume`. | | `--resume` | Poll a login started with `--no-wait`. Polls once by default. | | `--timeout ` | With `--resume`, keep polling up to this long. | | `--scope ` | Scopes to request. The default is `read scans:launch findings:write`. The approver can change them. | The global `--project ` preselects that project on the approval page. For agents and CI, split the login so no call blocks:
```bash
zkao login --no-wait --no-browser # prints the URL and code, then exits
zkao login --resume # repeat until it prints "Authorized"
```
`--resume` prints `Still waiting for approval` while the login is pending. Several logins can be pending at once, and `--resume` finishes whichever is approved. See [Authentication](/authentication/). ### `zkao whoami` [Section titled “zkao whoami”](#zkao-whoami) Print the token’s id, name, scopes, repository allowlist, expiry, and spend limit, plus the project and organization it belongs to. Warns on stderr when the token belongs to a different project than the configured one. ## Config [Section titled “Config”](#config) ### `zkao config set` [Section titled “zkao config set”](#zkao-config-set) Save settings to `~/.zkao/config.json`. | Option | Meaning | | ------------------ | -------------------------------- | | `--token ` | Token to save. | | `--project ` | Project id to save or switch to. | | `--base-url ` | API base URL to save. | A token given without `--project` is looked up with the API and saved under the project it belongs to. A project id given alone switches to that project’s saved token. ### `zkao config show` [Section titled “zkao config show”](#zkao-config-show) Print the resolved settings with the token masked. Also lists every saved project with its name, organization, base URL, and whether it is active. ### `zkao config use ` [Section titled “zkao config use \”](#zkao-config-use-projectid) Make a project with saved credentials the active one. Fails when no token is saved for it. ## Repositories [Section titled “Repositories”](#repositories) ### `zkao repos` [Section titled “zkao repos”](#zkao-repos) List the project’s repositories. Each has an `id` and a `readiness` of `ready` or `analyzing`. ### `zkao repos:wait ` [Section titled “zkao repos:wait \”](#zkao-reposwait-repositoryid) Wait until a repository is ready to scan, then print it. A newly added repository is `analyzing` for a while, and a scan launched on it fails with `repository_initializing`. | Option | Meaning | | ---------------- | ---------------------------------------------------------------- | | `--timeout ` | Give up after this many milliseconds. The default is 30 minutes. | ## Presets [Section titled “Presets”](#presets) ### `zkao presets` [Section titled “zkao presets”](#zkao-presets) List the scan presets the project can launch. Pass a preset’s `ref` to `zkao scans launch --preset`. ## Scans [Section titled “Scans”](#scans) ### `zkao scans launch` [Section titled “zkao scans launch”](#zkao-scans-launch) Launch a scan and print its id. Needs the `scans:launch` scope. | Option | Meaning | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `--repo ` | Repository id. Required. | | `--budget ` | Maximum budget in credits, reserved at launch. Omit it to use the budget zkao recommends for this scan type on this repository. | | `--preset [` | Scan preset ref from `zkao presets`. Defaults to the first active preset. | | `--branch ` | Branch to scan, resolved to its head commit. Ignored when `--commit` is set. | | `--commit ` | Commit to scan. | | `--base ][` | Base commit, branch, or tag of a diff scan. The scan audits only the change from it. Requires a diff preset. | | `--message ` | Commit message to record with the scan. | | `--guidance ` | Guidance for this scan only, from a file or `-` for stdin. Replaces the repository’s guidance for this run. | | `--area ` | Audit area to scope the scan to. Repeat it for several. See `zkao areas list`. | See [Scans](/guides/scans/) and [Diff scans](/guides/diff-scans/). ### `zkao scans list` [Section titled “zkao scans list”](#zkao-scans-list) List scans, most recent first. | Option | Meaning | | ------------- | --------------------- | | `--page ` | Page number. | | `--limit ` | Page size, up to 100. | ### `zkao scans get ` [Section titled “zkao scans get \”](#zkao-scans-get-scanid) Print one scan’s status and detail. The status is `QUEUED`, `PROCESSING`, `COMPLETED`, `FAILED`, or `CANCELLED`. ### `zkao scans wait ` [Section titled “zkao scans wait \”](#zkao-scans-wait-scanid) Poll a scan until it is `COMPLETED`, `FAILED`, or `CANCELLED`, then print the final detail. It backs off between polls and honors the server’s `Retry-After`. Each poll prints a progress line on stderr. Ctrl-C stops waiting. The command exits `0` whichever final status the scan reaches. Read `status` in the output. | Option | Meaning | | --------------------- | --------------------------------------------------------- | | `--timeout ` | Give up after this many seconds. The default is one hour. | ### `zkao scans cancel ` [Section titled “zkao scans cancel \”](#zkao-scans-cancel-scanid) Cancel a running or queued scan. Needs the `scans:launch` scope. The reserved credits not yet spent are released. A completed or failed scan cannot be cancelled. ### `zkao scans publish ` [Section titled “zkao scans publish \”](#zkao-scans-publish-scanid) Publish a completed scan as a public page. Needs the `publish` scope. The output’s `publicId` names the page, at `https://zkao.io/public/scans/`. | Option | Meaning | | ------------ | ------------------------------------------------------------------------- | | `--password` | Protect the page with a generated password, returned as `accessPassword`. | See [Publishing](/guides/publishing/). ## Findings [Section titled “Findings”](#findings) The read commands need the `read` scope. Commands that change a finding say which scope they need. `` accepts a finding’s full id or the `ZK-` label shown on its page. A label that matches more than one finding is refused. Use the full id then. ### `zkao findings list` [Section titled “zkao findings list”](#zkao-findings-list) List findings across the project, or for one scan. | Option | Meaning | | ------------- | ----------------------------- | | `--scan ` | Only findings from this scan. | | `--page ` | Page number. | | `--limit ` | Page size, up to 100. | ### `zkao findings get ` [Section titled “zkao findings get \”](#zkao-findings-get-findingid) Print a finding’s full detail, including its proof of concept and notes. ### `zkao findings comment ` [Section titled “zkao findings comment \ \”](#zkao-findings-comment-findingid-text) Add a comment to a finding. Needs the `findings:write` scope. ### `zkao findings severity ` [Section titled “zkao findings severity \ \”](#zkao-findings-severity-findingid-level) Override a finding’s severity with `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`, or `INFO`. Pass `none` to clear the override. Case does not matter. Needs the `findings:write` scope. ### `zkao findings resolution ` [Section titled “zkao findings resolution \ \”](#zkao-findings-resolution-findingid-status) Set a finding’s resolution status: `NOT_STARTED`, `IN_PROGRESS`, `RESOLVED`, `WONT_FIX`, `MITIGATED`, `FALSE_POSITIVE`, or `DUPLICATE`. Needs the `findings:write` scope. | Option | Meaning | | ----------------- | --------------------------------------------------------------------------------------------------------------------------- | | `--note ` | Record the reason as a comment in the same call. | | `--reason ]` | Record the reason as a catalog code instead. Valid codes depend on the status. A wrong one is rejected with the valid list. | `--note` wins when both are given. Both are ignored for `NOT_STARTED` and `IN_PROGRESS`. See [Triage findings](/guides/findings/) for the reason codes. ### `zkao findings publish ` [Section titled “zkao findings publish \”](#zkao-findings-publish-findingid) Publish a finding as a public page. Needs the `publish` scope. The output’s `publicId` names the page, at `https://zkao.io/public/findings/`. | Option | Meaning | | ----------------- | ------------------------------------------------------------------------- | | `--note ` | Note to show as the published note. | | `--password` | Protect the page with a generated password, returned as `accessPassword`. | ## Guidance [Section titled “Guidance”](#guidance) Guidance is text that every scan of a repository reads. See [Repository guidance](/guides/guidance/). ### `zkao guidance get ` [Section titled “zkao guidance get \”](#zkao-guidance-get-repoid) Print a repository’s guidance. ### `zkao guidance set ` [Section titled “zkao guidance set \ \”](#zkao-guidance-set-repoid-file-) Set a repository’s guidance from a file, or from stdin with `-`. Needs the `guidance:write` scope. By default the CLI reads the current guidance first and sends it along. If someone changed it in between, the write fails with a conflict instead of overwriting their edit. | Option | Meaning | | --------- | ---------------------------------------------- | | `--force` | Overwrite without the concurrent-change check. | ### `zkao guidance clear ` [Section titled “zkao guidance clear \”](#zkao-guidance-clear-repoid) Remove a repository’s guidance. It takes the same check and `--force` option as `set`. ## Audit areas [Section titled “Audit areas”](#audit-areas) An audit area is a named subsystem of a repository. A scan can be scoped to one or more areas instead of the whole repository. ### `zkao areas list ` [Section titled “zkao areas list \”](#zkao-areas-list-repoid) List a repository’s audit areas and their keys. | Option | Meaning | | ------------------- | ------------------------------------------------------------------------------------ | | `--branch ` | Read area sizes from this branch’s map. Defaults to the repository’s default branch. | ### `zkao areas add ` [Section titled “zkao areas add \ \”](#zkao-areas-add-repoid-name) Add a custom area. Its key is derived from the name. Needs the `guidance:write` scope. | Option | Meaning | | ---------------------- | -------------------------------- | | `--description ` | What this part of the code does. | ### `zkao areas rm ` [Section titled “zkao areas rm \ \”](#zkao-areas-rm-repoid-areakey) Delete a custom area. An area that a scan’s map named is refused. Needs the `guidance:write` scope. ## Billing [Section titled “Billing”](#billing) The balance belongs to the project’s organization and is shared by all its projects. Usage and the monthly summary count only movements attributed to this project. See [Credits](/guides/credits/). ### `zkao billing balance` [Section titled “zkao billing balance”](#zkao-billing-balance) Print the organization’s balance, the credits active scans hold, and the credits available for new scans. ### `zkao billing usage` [Section titled “zkao billing usage”](#zkao-billing-usage) List the credit ledger movements attributed to this project. Each movement’s `credits` is signed. Negative means spent. | Option | Meaning | | --------------- | ------------------------------------ | | `--from ` | ISO date or timestamp to start from. | | `--to ` | ISO date or timestamp to stop at. | | `--limit ` | Maximum number of movements. | Without `--from` and `--to`, it covers the last 30 days. ### `zkao billing summary` [Section titled “zkao billing summary”](#zkao-billing-summary) Print credits spent and purchased per calendar month (UTC), newest first. | Option | Meaning | | -------------- | -------------------------- | | `--months ` | How many months to return. |
# TypeScript SDK
> The typed @zksecurity/zkao-sdk client, its options, every method, the polling helpers, and error handling.
`@zksecurity/zkao-sdk` is a typed client for the zkao API. Its types are generated from the [OpenAPI spec](/openapi/v1.yaml), so every request and response is checked at compile time. It runs on Node 18 or later and in any runtime with `fetch`.
```bash
npm install @zksecurity/zkao-sdk
```
## Create a client [Section titled “Create a client”](#create-a-client)
```ts
import { ZkaoClient } from "@zksecurity/zkao-sdk";
const zkao = new ZkaoClient({
token: process.env.ZKAO_API_TOKEN!,
projectId: process.env.ZKAO_PROJECT_ID!,
});
```
A client is bound to one project. Every call is scoped to it. | Option | Type | Meaning | | ----------- | -------------- | ---------------------------------------------------------- | | `token` | `string` | Project API token, `zkao_proj__`. Required. | | `projectId` | `string` | Id of the project the token belongs to. Required. | | `baseUrl` | `string` | API base URL, used exactly as given. Optional. | | `fetch` | `typeof fetch` | Custom `fetch`, for tests or proxies. Optional. | ### Base URL [Section titled “Base URL”](#base-url) The client picks its base URL in this order: 1. The `baseUrl` option, used verbatim. 2. The `ZKAO_URL` environment variable, normalized. 3. Production, `https://zkao.io/api/v1` (exported as `DEFAULT_BASE_URL`). `ZKAO_URL` accepts a bare host, an origin, or a full API URL. A bare host gets `https://`, except localhost, which gets `http://`. `/api/v1` is appended unless the value already ends in `/api/v`. So `staging.zkao.io` becomes `https://staging.zkao.io/api/v1`. A plain `http://` URL to a non-local host logs a warning, since the token would travel unencrypted. Two helpers expose this logic: | Function | Meaning | | -------------------------------------------------- | ------------------------------------------------------------------------------------------- | | `normalizeBaseUrl(value: string): string` | Turn a host, origin, or URL into a full API base URL. Throws on an empty or invalid value. | | `resolveBaseUrlFromEnv(env?): string \| undefined` | Read and normalize `ZKAO_URL`, from `env` or `process.env`. Returns `undefined` when unset. | ## Methods [Section titled “Methods”](#methods) Every method returns a promise. A non-2xx response rejects with [`ZkaoApiError`](#errors). The required token scope is noted where a method needs more than `read`. Each method links to its endpoint in the API reference. ### Token [Section titled “Token”](#token) | Method | Returns | Meaning | | ---------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `getTokenInfo()` | `TokenInfo` | The token’s scopes, repository allowlist, expiry, spend limit, and its project and organization. [API](/api/operations/gettokeninfo/) | The standalone `getTokenInfo({ token, baseUrl?, fetch? })` does the same without a project id. Use it to find which project a token belongs to. ### Repositories [Section titled “Repositories”](#repositories) | Method | Returns | Meaning | | --------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------- | | `listRepositories()` | `Repository[]` | The project’s repositories, or the token’s allowlisted subset. [API](/api/operations/listrepositories/) | | `waitForRepositoryReady(repositoryId, opts?)` | `Repository` | Block until the repository’s `readiness` is `ready`. See [Polling helpers](#polling-helpers). | ### Guidance [Section titled “Guidance”](#guidance) | Method | Returns | Meaning | | ----------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------ | | `getRepositoryGuidance(repositoryId)` | `RepositoryGuidance` | Read a repository’s guidance. [API](/api/operations/getrepositoryguidance/) | | `setRepositoryGuidance(repositoryId, content, opts?)` | `SetGuidanceResult` | Set guidance, or clear it with `null`. Needs `guidance:write`. [API](/api/operations/setrepositoryguidance/) | `setRepositoryGuidance` takes `{ expectedContent?: string | null }`. Pass the content you last read, or `null` if it was cleared. The write then fails with `409 conflict` if someone changed it in between. Writing the content already stored is a no-op that returns `unchanged: true`.
```ts
const current = await zkao.getRepositoryGuidance(repoId);
await zkao.setRepositoryGuidance(repoId, `${current.content ?? ""}\nNew note.`, {
expectedContent: current.content,
});
```
### Audit areas [Section titled “Audit areas”](#audit-areas) | Method | Returns | Meaning | | --------------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `listAuditAreas(repositoryId, opts?)` | `AuditAreaList` | A repository’s audit areas. `opts.branch` picks the branch whose map gives sizes. [API](/api/operations/listauditareas/) | | `createAuditArea(repositoryId, name, description?)` | `AuditArea` | Add a custom area. The key is derived from the name. Needs `guidance:write`. [API](/api/operations/createauditarea/) | | `deleteAuditArea(repositoryId, areaKey)` | `DeleteAuditAreaResult` | Delete a custom area. An area a scan’s map named is refused with `409`. Needs `guidance:write`. [API](/api/operations/deleteauditarea/) | ### Scans [Section titled “Scans”](#scans) | Method | Returns | Meaning | | ---------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `listScans(opts?)` | `Paginated` | Scans, most recent first. `opts` takes `page` and `limit` (max 100). [API](/api/operations/listscans/) | | `getScan(scanId)` | `ScanDetail` | One scan’s status and detail. [API](/api/operations/getscan/) | | `launchScan(body)` | `LaunchScanResult` | Launch a scan. Needs `scans:launch`. [API](/api/operations/launchscan/) | | `waitForScan(scanId, opts?)` | `ScanDetail` | Block until the scan is `COMPLETED`, `FAILED`, or `CANCELLED`. See [Polling helpers](#polling-helpers). | | `cancelScan(scanId)` | `CancelScanResult` | Cancel a running or queued scan and release its unspent credits. Needs `scans:launch`. [API](/api/operations/cancelscan/) | | `publishScan(scanId, opts?)` | `PublishArtifactResult` | Publish a completed scan as a public page. `opts.withPassword` adds a generated password. Needs `publish`. [API](/api/operations/publishscan/) | `launchScan` takes a `LaunchScanRequest`: | Field | Meaning | | --------------- | ----------------------------------------------------------------------------------------- | | `repositoryId` | Repository to scan. Required. | | `creditBudget` | Maximum budget in credits. Omit it to use the budget zkao recommends. | | `presetRef` | Preset ref from `listScanPresets()`. Defaults to the first active preset. | | `branch` | Branch to scan. Ignored when `commitHash` is set. | | `commitHash` | Commit to scan. | | `commitMessage` | Commit message to record. | | `baseCommit` | Base commit, branch, or tag of a diff scan. Required by a diff preset, refused by others. | | `auditAreaKeys` | Area keys to scope the scan to. Omit to scan the whole repository. | | `guidance` | Guidance for this scan only, replacing the repository’s. | It resolves to `{ scanId, queued, creditBudget }`. See [Scans](/guides/scans/) and [Diff scans](/guides/diff-scans/). ### Findings [Section titled “Findings”](#findings) `findingId` accepts the full id or the `ZK-` label shown on the finding page. | Method | Returns | Meaning | | ------------------------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `listFindings(opts?)` | `Paginated` | Findings across the project. `opts` takes `scanId`, `page`, and `limit`. [API](/api/operations/listfindings/) | | `getFinding(findingId)` | `FindingDetail` | Full detail, including the proof of concept and notes. [API](/api/operations/getfinding/) | | `addFindingNote(findingId, content)` | `{ noteId, findingId }` | Add a comment. Needs `findings:write`. [API](/api/operations/addfindingnote/) | | `pinFindingNote(findingId, noteId, kind?)` | `{ findingId, noteId }` | Pin a note as the finding’s resolution note. `kind` is `"resolution"`. Needs `findings:write`. [API](/api/operations/pinfindingnote/) | | `setFindingSeverity(findingId, severity)` | `{ findingId, userSeverity, effectiveSeverity }` | Override severity, or clear the override with `null`. Needs `findings:write`. [API](/api/operations/updatefindingseverity/) | | `setFindingResolution(findingId, status, opts?)` | `{ findingId, resolutionStatus }` | Change the resolution status. Needs `findings:write`. [API](/api/operations/updatefindingresolution/) | | `publishFinding(findingId, opts?)` | `PublishArtifactResult` | Publish a finding as a public page. `opts` takes `noteId` and `withPassword`. Needs `publish`. [API](/api/operations/publishfinding/) | `setFindingResolution` takes `{ note?: ChangeNote; reason?: ResolutionReason }`. `note` is `{ content }` for a new comment or `{ existingNoteId }` to reuse one. `reason` is a catalog code valid for the status. `note` wins when both are given. Both are ignored for `NOT_STARTED` and `IN_PROGRESS`. See [Triage findings](/guides/findings/) for the codes.
```ts
await zkao.setFindingResolution(findingId, "WONT_FIX", { reason: "risk_accepted" });
```
### Presets [Section titled “Presets”](#presets) | Method | Returns | Meaning | | ------------------- | -------------- | ---------------------------------------------------------------------------------------------------- | | `listScanPresets()` | `ScanPreset[]` | Presets the project can launch. Pass a `ref` as `presetRef`. [API](/api/operations/listscanpresets/) | ### Billing [Section titled “Billing”](#billing) The balance belongs to the project’s organization and is shared by all its projects. Usage and the monthly summary count only movements attributed to this project. See [Credits](/guides/credits/). | Method | Returns | Meaning | | -------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `getBillingBalance()` | `BillingBalance` | `balanceCredits`, `reservedCredits` held by active scans, and `availableCredits` for new scans. [API](/api/operations/getbillingbalance/) | | `getBillingUsage(opts?)` | `BillingUsage` | Ledger movements with signed `credits`. `opts` takes `from`, `to`, and `limit`. Defaults to the last 30 days. [API](/api/operations/getbillingusage/) | | `getBillingSummary(opts?)` | `UsageMonthSummary[]` | Credits spent and purchased per UTC month, newest first. `opts.months` sets how many. [API](/api/operations/getbillingsummary/) | ## Polling helpers [Section titled “Polling helpers”](#polling-helpers) Scans take minutes, and a tight polling loop is rejected with `429`. The helpers back off exponentially with jitter, honor the server’s `Retry-After`, and treat a `429` as a signal to slow down.
```ts
const { scanId } = await zkao.launchScan({ repositoryId, presetRef });
const scan = await zkao.waitForScan(scanId, {
onPoll: (s) => console.log(s.status),
});
if (scan.status !== "COMPLETED") throw new Error(`Scan ended ${scan.status}`);
```
`waitForScan(scanId, opts)` resolves with the final detail whatever the final status is. | Option | Default | Meaning | | --------------- | ------- | ------------------------------------------------------------------- | | `intervalMs` | `5000` | First delay between polls. Grows by 1.5x each poll. | | `maxIntervalMs` | `30000` | Cap on the delay. | | `timeoutMs` | 1 hour | Give up and throw after this long. | | `signal` | none | `AbortSignal` that stops the wait. Rejects with the abort reason. | | `onPoll` | none | Called with the latest `ScanDetail` on each poll that is not final. | `waitForRepositoryReady(repositoryId, opts)` takes the same options except `onPoll`, with a default timeout of 30 minutes. It throws if the repository is not in the project or the token’s allowlist. Call it between adding a repository and launching its first scan. `isTerminalScanStatus(status)` and `TERMINAL_SCAN_STATUSES` tell whether a status is final, for your own loops. ## Errors [Section titled “Errors”](#errors) Every non-2xx response throws `ZkaoApiError`. | Property | Meaning | | --------- | ------------------------------------------------------------------------------ | | `status` | HTTP status. | | `code` | Machine-readable error code from the response, such as `insufficient_credits`. | | `message` | Human-readable message. | Branch on `code`, not on the message.
```ts
import { ZkaoApiError } from "@zksecurity/zkao-sdk";
try {
await zkao.launchScan({ repositoryId, presetRef });
} catch (err) {
if (err instanceof ZkaoApiError && err.code === "repository_initializing") {
await zkao.waitForRepositoryReady(repositoryId);
} else if (err instanceof ZkaoApiError && err.code === "insufficient_credits") {
console.error("Not enough credits for this budget.");
} else {
throw err;
}
}
```
The full list of codes is in [API conventions](/reference/api-conventions/). Timeouts and aborts in the polling helpers throw a plain `Error`, not `ZkaoApiError`. ## Types [Section titled “Types”](#types) The package exports a named type for each API object. | Type | Meaning | | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | `Repository`, `RepositoryGuidance`, `SetGuidanceResult` | Repositories and their guidance. | | `Scan`, `ScanDetail`, `ScanStatus`, `ScanPreset` | Scans, their status, and presets. | | `LaunchScanRequest`, `LaunchScanResult`, `CancelScanResult` | Launch and cancel bodies and results. | | `Finding`, `FindingDetail`, `FindingNote`, `ChangeNote` | Findings and their notes. | | `Severity`, `ResolutionStatus`, `ResolutionReason`, `TriageStatus` | Finding enums. | | `PublishArtifactResult` | Result of publishing a scan or finding. | | `TokenInfo` | The calling token and its project. | | `BillingBalance`, `BillingUsage`, `UsageEvent`, `UsageEventType`, `BillingSummary`, `UsageMonthSummary` | Credit balance and ledger. | | `Paginated` | `{ items, page, limit, total }` for list endpoints. | | `ZkaoClientOptions`, `WaitForScanOptions`, `WaitForRepositoryReadyOptions` | Option objects. | The raw generated types are exported as `components` and `paths`. Any schema without a named export is available as `components["schemas"][""]`, for example `components["schemas"]["AuditArea"]`.