Browse docs
Docs /install/cli
Install

CLI

@izri/cli is the terminal surface for Izri, and the foundation every other integration shells out to — the GitHub Action installs this CLI and calls it.

Install

# One-shot, no install
npx @izri/cli@latest --version

# Global
npm install -g @izri/cli
izri --version

Requires Node.js 18 or newer.

Configure

Three environment variables:

Variable Required Example
IZRI_API_URL yes https://api.izri.ai
IZRI_API_TOKEN yes izri_xxxxxxxxxxxxxxxx
IZRI_PROJECT_ID no 01HK0... (ULID)

Mint the token from your organization settings with the tests:run scope (izri upload, izri verify, izri run) and tests:read (izri status, --watch); a token without them gets 403. It must start with izri_ — the API short-circuits to bearer validation on that prefix, so a token without it produces an opaque 401 rather than a useful message.

How the project gets resolved

IZRI_PROJECT_ID is optional. Resolution order:

  1. --project <id> on the command
  2. IZRI_PROJECT_ID
  3. auto-match — inside a git checkout with a GitHub origin remote, the CLI matches owner/repo against the projects your token can see

Auto-match uses the same comparison the GitHub App webhook does, so the CLI and your PR checks always agree about which project a repository belongs to.

Commands

izri run

Trigger a test run for the current repository.

izri run                        # fire and forget
izri run --watch                # poll until terminal
izri run --project 01HK...      # explicit project
Flag Description
-p, --project <id> Project ULID. Otherwise auto-resolved from the git remote.
--watch Poll until the run reaches a terminal status.

The current HEAD SHA and branch are sent automatically.

--watch polls every 3 seconds until the run reaches PASSED, FAILED, ERROR, or CANCELLED, with a 30-minute hard cap. It exits non-zero on any terminal status except PASSED, so CI scripts can branch on it directly.

izri upload

Send your CI's own test results — and optionally a coverage report — to Izri as the evidence for izri/tests.

izri upload --junit junit.xml                                   # results only
izri upload --junit junit.xml --coverage coverage/lcov.info     # plus diff coverage
izri upload --skipped "docs-only change"                        # nothing ran, say so
Flag Description
--junit <path> JUnit XML from your test runner. Required unless --skipped.
--coverage <path> LCOV or Cobertura report. Format is inferred from the extension; override with --coverage-format.
--skipped <reason> Record an explicit empty run. Always upload something: silence reads as a broken step.
--head-sha, --base-sha Default from the GitHub Actions environment (the PR head, not the merge commit), then from git.
-p, --project <id> Project ULID. Otherwise auto-resolved from the git remote.

In GitHub Actions the CLI requests the job's id-token (the job needs permissions: id-token: write) and sends it with the upload; its signed claims are what let Izri trust the evidence, as izri/tests explains. Without that permission, or outside Actions, the upload still goes through and is recorded as untrusted, with a line on stderr saying so. A token Izri refuses is different: that is a configuration error (usually the id-token's audience, the origin of IZRI_API_URL, not matching the Izri deployment's API_URL or APP_URL), and the command fails with a message naming it rather than quietly uploading untrusted evidence. A 409 (Izri has not seen the commit yet) is retried three times, then warned about — it is never your build's failure.

izri verify

Run the revert check: the tests that depend on the change, once at HEAD and once with the change's source files restored to the merge base, and upload both results.

izri verify --test-command "npx vitest run {files} --reporter=junit --outputFile={junit}"
izri verify --base origin/develop --max-test-files 20 --test-command "…"
Flag Default Description
--test-command <cmd> (required) Shell command run in each worktree. {files} becomes the selected test files, each shell-quoted; {junit} becomes a path the CLI owns, where the command must write JUnit XML.
-b, --base <ref> origin/$GITHUB_BASE_REF, else the merge base with origin/main Base of the change.
--max-test-files <n> 50 Budget. Over it, only the test files the PR changed run; still over, nothing runs and a budget skip is uploaded.
--head-sha <sha> (as izri upload) Commit the evidence is about.
-p, --project <id> (auto) Project ULID.

What it selects: the test files the PR added or changed, plus every tracked test file whose imports suffix-match a changed source file — the same rule Izri's static linkage uses. Only TypeScript/JavaScript files count, as sources and as tests: a changed fixture or snapshot under a test directory is not run. Imports are read from the first megabyte of each regular (non-symlinked) test file. With no changed source file, or no selected test, it uploads an explicit empty check rather than nothing. Like izri upload it needs the base fetched (actions/checkout with fetch-depth: 0).

Both runs happen in throwaway git worktrees, so your tracked files are never modified; the worktrees are removed when the command ends, including when it is interrupted (SIGINT, SIGTERM; a second signal kills a test command that ignores the first) — only a SIGKILL of the CLI itself leaves them behind. Each node_modules directory your checkout has is linked into the worktrees, so install dependencies before running it; a runner that writes caches into node_modules writes them into your checkout's. Imports that resolve through a node_modules link into another workspace package see that package as it is in the checkout, not reverted. A selected test file whose path contains a newline aborts the command; a path starting with - is passed as ./-…, never as an option. The test command's exit status is ignored — the reverted run is expected to fail — and its output goes to stderr; there is no timeout of its own, so rely on the job's. It runs without IZRI_API_TOKEN and the ACTIONS_ID_TOKEN_REQUEST_* variables, so the PR's tests cannot use Izri's credentials; IZRI_VERIFY_RUN is head or reverted. It needs a POSIX sh.

The upload is judged against izri/tests evidence for the same commit: run izri upload (or the Izri runner) too, in either order. The first stdout line is <verificationRunId> JUDGED or NOT_JUDGED, followed by the reason. Like izri upload it sends the job's id-token (an id-token Izri refuses is a configuration error and fails the command) and retries a 409, then only warns. It also fails on a malformed invocation, a base it cannot resolve, a project it cannot resolve, a git worktree it cannot create, a test command that writes no JUnit at head, and an upload Izri rejects as 400, 401, 403, 404 or 413. An unreachable Izri, or one answering 5xx, only warns.

izri check-scope

Run scope analysis on your local diff without triggering a test run. Deterministic and fast.

izri check-scope
izri check-scope --base origin/develop
izri check-scope --semantic
izri check-scope --json
Flag Default Description
-p, --project <id> (auto) Project ULID.
-b, --base <ref> origin/main Base ref to diff against.
--semantic off Opt into the LLM semantic layer. Tier-gated server-side.
--json off Emit raw JSON instead of the formatted summary.

The default base is origin/main. If your trunk is named anything else, pass --base or the diff will be wrong rather than empty — worth wiring into a shell alias.

--semantic costs AI tokens and is gated by your plan. Without it you still get the full deterministic rule layer: path categorization, commit-vs-files alignment, sensitive-path hints, and scale anomalies.

izri status

Fetch the latest delta report for the current HEAD.

izri status
izri status --json
Flag Default Description
-p, --project <id> (auto) Project ULID.
--json off Emit raw JSON instead of the formatted summary.

Use this after a push to read the verdict without opening GitHub.

Scripting against it

--json plus a non-zero exit on failure is the whole contract:

if ! izri run --watch; then
  echo "Izri verdict is not passing"
  izri status --json > izri-report.json
  exit 1
fi

See verdicts and exit codes for the exact status-to-exit-code mapping.

  • GitHub Action — the same CLI, wired into pull requests.
  • MCP server — the same signals, exposed to coding agents.

Reading this with an agent? /docs/install/cli.md serves the raw markdown.

Edit this page on GitHub →All docs