# hushrun docs, every page # Skip checks this code already passed Source: https://hushrun.com/docs hushrun runs a check only when the code in front of it hasn't passed it yet. Put `hushrun run --` in front of a test, lint or typecheck command once, and it skips the same way on your laptop, in CI and in a coding agent's sandbox. ```console $ hushrun run e2e -- playwright test --project site hushrun: e2e running: no pass yet for this exact code … hushrun: e2e passed in 16s; it won't run again on this code ``` A minute later, a coding agent runs the same check on the same files, on another machine: ```console $ hushrun run e2e -- playwright test --project site hushrun: e2e skipped: this exact code passed 1 min ago (saves 16s) ``` ## How it decides Before it runs a check, hushrun looks for an earlier result for the same check on the same code: - **the repository's files as they are now**, committed or not, with ignored files left out; - **the check's name, its exact command, and the directory it runs in.** The machine, the branch, the commit and the CI provider aren't part of it, so a pass on your laptop skips the check in CI, and the other way round. When hushrun can't tell whether this code passed, it runs the check: a doubt costs a run, never a skip that should have been a run. [How skipping works](https://hushrun.com/docs/run.md) has the details. ## Where it works - **On your laptop**, once you sign in with `npx hushrun login`. - **In GitHub Actions**, with the job's ID token and no secret to store. Other CI uses a token. See [In CI](https://hushrun.com/docs/ci.md). - **For coding agents.** An agent on your machine uses your sign-in. An agent in a sandbox uses an agent token and the key it comes with. See [Agents and sandboxes](https://hushrun.com/docs/agents.md). Results go to your organization's backend on hushrun's hosted service, where everyone who works on the repository shares them. Without one, they stay on your machine. [Where results go](https://hushrun.com/docs/results.md) explains the choice, and how to run your own server. ## Set it up At the root of a repository whose `package.json` runs your checks: ```bash npx hushrun init ``` It finds the checks, signs you in, shows every change it would make, and makes them once you agree. It takes about three minutes; the [Quickstart](https://hushrun.com/docs/quickstart.md) walks through it. A coding agent can do it for you: [For coding agents](https://hushrun.com/docs/for-agents.md) has the prompt to give it. ## Promotion hushrun also runs a whole delivery pipeline: CI builds and validates each change once, and every later stage deploys the exact artifact that was tested, by digest, with signed evidence of what ran. You don't need it to skip checks. [Promotion](https://hushrun.com/docs/promotion.md) explains it. ## Reading these docs as an agent Every page is also Markdown: add `.md` to its address, such as `https://hushrun.com/docs/quickstart.md`, or ask for `text/markdown` in the `Accept` header. [`/llms.txt`](https://hushrun.com/llms.txt) lists every page, and [`/llms-full.txt`](https://hushrun.com/llms-full.txt) has all of them in one file. # Quickstart Source: https://hushrun.com/docs/quickstart `npx hushrun init` sets hushrun up in a repository in about three minutes. It wraps your checks, signs you in, lets your CI share results with no secret, and shows every change before it makes it. ## Before you start - A git repository with a `package.json` at its root whose scripts run your checks: `test`, `lint`, `typecheck` and the like. Without one, see [By hand](#by-hand). - Node.js, for `npx`. - A GitHub or Google account to sign in with. - Optional: the GitHub CLI, `gh`, signed in. While you sign in, init uses it to count how often CI ran again on code it had already passed in the last 30 days. ## Run init At the repository's root: ```bash npx hushrun init ``` npx asks to install the `hushrun` package the first time. init then looks at the repository, opens your browser to sign in, and lists what it will change. `--dry-run` shows the same list without signing in or changing anything: ```console $ npx hushrun init --dry-run … hushrun init will change: package.json "lint": "eslint ." → "hushrun run -- eslint ." "test": "vitest" → "hushrun run -- vitest run" (vitest alone watches on a terminal) "typecheck": "tsc --noEmit" → "hushrun run -- tsc --noEmit" "hushrun": {"backend": ""}, where CI, teammates and agents share results devDependencies: "hushrun": "^0.1.0" .github/workflows/ci.yml job test: adds permissions contents: read and id-token: write. The job's token then has only these two: add any other it needs your org connects github.com/acme/shop, so that its CI records results with its own ID token adds github.com/acme/shop to your org, for promoting its builds, unless it is there already hushrun.yaml a new file: repository github.com/acme/shop, the URL the org gives it, and its log key Nothing changed (--dry-run). ``` Without `--dry-run`, it signs you in first, so the list names your organization and its URL, and it ends with `Apply? [Y/n]`. ## Approve the sign-in init prints a code and opens a page where you approve it. Pick the organization the repository belongs to. With no organization yet, you create one on that page, or join one you were invited to. If the repository already belongs to an organization you aren't in, init offers to ask its admins to let you in. Signing in saves a token that only you can read, and registers this machine with your organization; `hushrun logout` revokes both. To sign in from a terminal with no browser, add `--no-browser`: init prints the page's address instead of opening it. ## What init changes Once you agree, init makes exactly the changes it listed and keeps every other byte of each file as it was: - **Your checks' scripts** get `hushrun run --` in front. Each check takes its script's name. A bare `vitest`, which watches at a terminal, becomes `vitest run`. Scripts that watch, serve, fix or write files, or that use shell operators such as `||` or `;`, stay as they are, and init says why. - **`"hushrun": {"backend": …}`** in `package.json` names your organization's URL on hushrun. Committed with the code, it tells CI, your teammates and their agents where results go, with no variable to set. - **`hushrun`** joins `devDependencies`, installed with your package manager (npm, pnpm, Yarn or Bun), so `npm ci` installs hushrun in CI too. - **GitHub Actions jobs** that run a wrapped check get `id-token: write`, so CI records results with its own short-lived token. There is no secret to store. - **Your organization** connects the repository, so its CI may record results. - **`hushrun.yaml`**, a small file for [promotion](https://hushrun.com/docs/promotion.md), names the repository and its URL on hushrun. init writes it when an admin or owner of the organization runs it, since that adds the repository to the dashboard. Skipping checks doesn't need it. Then init runs one wrapped check twice, through your package manager, to show the skip: ```console $ npm run lint > shop@1.0.0 lint > hushrun run -- eslint . hushrun: lint running: no pass yet for this exact code hushrun: lint passed in 0.2s; it won't run again on this code $ npm run lint > shop@1.0.0 lint > hushrun run -- eslint . hushrun: lint skipped: this exact code passed just now (saves 0.2s) ``` ## Commit and push Review the changes with `git diff`, then commit and push them. CI records its first results, and from then on skips any check this exact code already passed, wherever it passed: on a laptop, in CI or in an agent's sandbox. In the dashboard, at https://app.hushrun.com, the repository's page lists its checks: each one's newest run, its skips and the time they saved, and which checks are flaky. Teammates get hushrun with the commit. Until one of them signs in, their checks run as usual, and at a terminal hushrun ends with: ```text hushrun: sign in to share results with acme: npx hushrun login ``` ## With a coding agent A coding agent can run init for you. Give it this prompt, word for word: ```markdown Add hushrun to this repository. Run `npx hushrun init --yes` at the repository root; it prints a sign-in link, which I'll approve in my browser. Then show me the diff it made, run the checks it wrapped once, and commit the change on a new branch. ``` `--yes` applies the changes without asking. The agent shows you the sign-in link, and you approve it in your browser. [For coding agents](https://hushrun.com/docs/for-agents.md) has the rest of what an agent should know. ## By hand hushrun doesn't need `package.json` or init. Install it with npm: ```bash npm install --global hushrun ``` Then put `hushrun run NAME --` in front of any check, in any language: ```bash hushrun run unit -- cargo test hushrun run lint -- golangci-lint run ``` `unit` names the check, and everything after `--` is its command. Results stay on your machine until you sign in with `hushrun login`, or until a backend is named, as [Where results go](https://hushrun.com/docs/results.md) explains. Without a global install, `npx hushrun run unit -- cargo test` works the same way. ## Next - [How skipping works](https://hushrun.com/docs/run.md): what counts as the same code, and when hushrun runs a check anyway. - [In CI](https://hushrun.com/docs/ci.md): GitHub Actions, and CI elsewhere. - [Agents and sandboxes](https://hushrun.com/docs/agents.md): agents that can't open a browser. # For coding agents Source: https://hushrun.com/docs/for-agents This page is written for a coding agent: one asked to add hushrun to a repository, or working in a repository that already uses it. People can read it as the rules their agents follow. ## Is hushrun here? A repository uses hushrun when its `package.json` has scripts that start with `hushrun run`, or a `"hushrun"` key that names a backend: ```json title="package.json" "scripts": { "test": "hushrun run -- vitest run" }, "hushrun": { "backend": "https://api.hushrun.com/o/acme" } ``` Other repositories call `hushrun run NAME -- COMMAND` from their CI files or scripts. ## Setting hushrun up When a person asks you to add hushrun to a repository: 1. At the repository's root, run: ```bash npx hushrun init --yes ``` `--yes` applies the changes without asking. Without `--yes`, and without a terminal, init prints its plan and exits with status 2. 2. It prints a sign-in link and a code. Show both to the person and wait: they approve it in their browser, and you can't do it for them. If they don't have an organization yet, they create one on that page. 3. When init finishes, show the person the diff, run each check it wrapped once, and commit the change on a new branch. `npx hushrun init --dry-run` shows the plan and changes nothing: no sign-in, no files, no install. The prompt people give agents for this, word for word: ```markdown Add hushrun to this repository. Run `npx hushrun init --yes` at the repository root; it prints a sign-in link, which I'll approve in my browser. Then show me the diff it made, run the checks it wrapped once, and commit the change on a new branch. ``` ## Running checks - **Run checks the usual way:** `npm test`, `pnpm lint`, `yarn typecheck`. Don't call the tools behind them directly to get around hushrun. - **A skip is a pass.** `hushrun: test skipped: this exact code passed …` means the same check, with the same command, passed on exactly these files: here, in CI or in another agent's sandbox. The exit code is 0. Running it again would give the same answer. - **A failure is real.** `hushrun: test failed in 2s (exit 1)` keeps the check's own exit code and output. Fix the cause; the next run runs again. - **Changing files runs checks again.** Editing a file changes the code, so checks run; going back to files that passed skips them. - **Partial runs are separate checks.** `vitest run src/cart.test.ts` never reuses the full run's pass, and its pass never stands in for the full run. Use them freely while you work, then run the full check. - **To see a skipped check's output,** such as a log or a coverage report, run it again with `--force`, giving the script's name and its exact command so the result counts for the same check: `npx hushrun run --force test -- vitest run`. - **hushrun's lines go to stderr** and start with `hushrun: `. Everything else is the check's own output. ## Lines that need you | Line | What to do | |---|---| | `… files changed while it ran, so it wasn't recorded …` | The check writes files that aren't ignored. Tell the person, and suggest adding them to `.gitignore` | | `hushrun: sign in to share results with acme: npx hushrun login` | You run where nobody has signed in. Checks still run; tell the person, who signs in once | | `hushrun: results aren't shared with acme: …` | No credential reached the backend, as in a sandbox without `HUSHRUN_TOKEN`. Checks still run; tell the person | | `… refused HUSHRUN_TOKEN: … expired on …` or `… was revoked …` | The sandbox's token no longer works; the person makes a new one | | ``… not recorded: set `HUSHRUN_MACHINE_KEY` to the key `hushrun token create --agent` printed`` | The sandbox has the agent token but not the key it came with. Checks still run; tell the person, who sets `HUSHRUN_MACHINE_KEY` | | ``… not recorded: this machine isn't registered: run `hushrun login` `` | This machine's sign-in isn't registered, or lapsed after 90 days unused. Checks still run; tell the person, who signs in again | | `… not recorded: …; upgrade: https://…` | The organization's plan recorded all the checks it includes this period. Checks still run; tell the person | | `… running without skipping: can't tell which code this is …` | `hushrun.yaml` has several scopes: the script needs `--scope NAME` | None of these fail a check: hushrun runs the command and keeps its exit code. `hushrun whoami` shows where results go and which credential is used. ## In a sandbox Without a browser, hushrun needs an agent token and the key it came with, from a person: `HUSHRUN_TOKEN=hra_…`, `HUSHRUN_MACHINE_KEY` (the key's PEM block), and `HUSHRUN_PROVIDER=local` where the sandbox sets `CI=true`. If `hushrun whoami` shows no token, or a run says its result wasn't recorded for want of the key, checks still run but share nothing: say so instead of trying to sign in. [Agents and sandboxes](https://hushrun.com/docs/agents.md#sandboxes) is the page to point the person to. ## Never - Commit a token (`hra_…`, `hru_…`) or a machine key, or print one in a log or a pull request. - Edit the `"hushrun"` key in `package.json`, a `HUSHRUN_*` variable, or `id-token: write` in a workflow, unless the person asks. - Remove `hushrun run --` from a script to make a check run. Use `--force` once instead. - Run `hushrun login`, `hushrun logout` or `hushrun token` on a person's behalf without being asked. ## A snippet for AGENTS.md Add this to the repository's `AGENTS.md`, `CLAUDE.md` or your agent's instructions file: ```markdown title="AGENTS.md" ## Checks run through hushrun This repository's checks run through hushrun (`hushrun run -- …` in package.json's scripts). A check skips when this exact code already passed it, here, in CI or in another agent's sandbox. - Run checks as usual (`npm test`, `npm run lint`); don't call the tools behind them directly. - A skip is a pass, with exit code 0. A failure is real: fix it. - To see a skipped check's output, run it with `--force`, the script's name and its command, such as `npx hushrun run --force test -- vitest run`. - A check that writes files must list them in .gitignore, or its result isn't recorded. - Never commit tokens (hra_…, hru_…) or machine keys, and never edit package.json's "hushrun" key. - More: https://hushrun.com/docs/for-agents.md ``` ## Reading these docs Every page of these docs is Markdown at its address with `.md` added, such as `https://hushrun.com/docs/run.md`. A request whose `Accept` header asks for `text/markdown` gets the Markdown at the page's own address. [`/llms.txt`](https://hushrun.com/llms.txt) lists every page with a line on each, and [`/llms-full.txt`](https://hushrun.com/llms-full.txt) has all of them in one file. # How skipping works Source: https://hushrun.com/docs/run `hushrun run -- COMMAND` runs COMMAND unless this exact code already passed it, and records the result where your laptop, CI and your agents find it. ## The command ```bash hushrun run [NAME] -- COMMAND ``` Everything after `--` is the check's command, run as it is. In `package.json`, it wraps a script once, and `npm test` then skips the same way everywhere: ```json title="package.json" "scripts": { "lint": "hushrun run -- eslint .", "test": "hushrun run -- vitest run" } ``` - hushrun exits with the command's exit code, or 0 when it skips. - Its own lines go to stderr and start with `hushrun: `. `-q` silences them. Stdout is the command's. - It needs no configuration file, key or account. Without a backend, results stay on this machine. ## What counts as the same code hushrun skips when it finds a pass recorded for the same key. The key is made of: - **The repository's files as they are now:** tracked and untracked, committed or not, with the files `.gitignore` ignores left out. Change a file and the check runs again; go back to files that passed and it skips. - **The check's name.** - **The exact command, argument by argument.** `vitest run` and `vitest run src/cart.test.ts` are two checks, so a partial run never stands in for a full one. - **The directory it runs in,** relative to the repository's root. Nothing else is part of it: not the machine, the operating system, the CI provider, the environment variables, the branch, the commit or the time. A pass on your laptop skips the check in CI, and the other way round. Committing changes nothing. ```console $ npm test > shop@1.0.0 test > hushrun run -- vitest run hushrun: test running: no pass yet for this exact code … hushrun: test passed in 0.6s; it won't run again on this code ``` A change to `cart.js` that breaks a test runs it again, and the failure keeps vitest's exit code: ```console $ npm test > shop@1.0.0 test > hushrun run -- vitest run hushrun: test running: no pass yet for this exact code … Test Files 1 failed (1) Tests 1 failed | 1 passed (2) … hushrun: test failed in 0.3s (exit 1) ``` Undo the change and the earlier pass counts again: ```console $ npm test > shop@1.0.0 test > hushrun run -- vitest run hushrun: test skipped: this exact code passed just now (saves 0.6s) ``` When a check depends on something outside the files, such as an environment variable, the Node.js version or the operating system, give each variant its own name, such as `unit-node22` and `unit-node24`. ## Naming checks The name labels a check's history and is part of its key. Without NAME: - run through a `package.json` script, the check takes the script's name: `"test": "hushrun run -- vitest run"` is `test`, with npm, pnpm, Yarn and Bun alike; - otherwise it takes the program's name: `hushrun run -- vitest run` at a prompt is `vitest`. A name starts with a lowercase letter, followed by lowercase letters, digits, `_` and `-`, 63 characters at most, and hushrun makes one from a script's or a program's name: `test:unit` names its check `test-unit`, and `format:check` names it `format-check`. Name a check yourself when you want another name in its history: ```json title="package.json" "scripts": { "test:unit": "hushrun run unit -- vitest run --project unit", "test:e2e": "hushrun run e2e -- playwright test" } ``` ## What it prints | Line | What happened | |---|---| | `test running: no pass yet for this exact code` | No result for this key: the command runs | | `test passed in 41s; it won't run again on this code` | It passed and was recorded | | `test skipped: this exact code passed 3 min ago (saves 41s)` | A pass was found: the command didn't run, and hushrun exited 0 | | `test skipped: this exact code passed in CI 3 min ago (saves 41s)` | The same, for a pass CI recorded | | `test running: it failed on this exact code last time` | The newest result for this key is a fail: it runs again | | `test failed in 2s (exit 1)` | It failed; the failure was recorded and the exit code kept | | `test running (--force)` | `--force` runs it even after a pass | | `test running: can't read earlier results (…)` | The backend couldn't be reached: it runs, and hushrun still tries to record | | `test stopped by a signal; nothing recorded` | Interrupted, for example with Ctrl-C | Lines about credentials, tokens and plan limits are in [Troubleshooting](https://hushrun.com/docs/troubleshooting.md). ## When hushrun runs anyway Whenever hushrun can't tell whether this code passed, it runs the command. At worst a doubt costs a run; it never skips a check it shouldn't. - **Outside a git repository**, it runs and records nothing. - **With results turned off** (`HUSHRUN_BACKEND=` set to nothing), it runs, records nothing and prints nothing. - **When the backend can't be reached** within 10 seconds, or answers something unexpected, it runs, and still tries to record. - **When the backend refuses the credential**, it says so in one line, runs, and doesn't record. - **Without the sign-in a backend needs**, it runs, and at a terminal ends with `hushrun: sign in to share results with acme: npx hushrun login`. - **When `hushrun.yaml` has several scopes** and no `--scope` says which one, it can't tell which files the check covers, and says so (see [Several projects in one repository](#monorepos)). ## Files that change while it runs hushrun checks the files again when the command ends. If they changed, the result belongs to neither version, so it isn't recorded: ```text hushrun: test passed in 12s, but files changed while it ran, so it wasn't recorded. If test writes files, list them in .gitignore ``` List what your checks write, such as `coverage/`, `test-results/` or build output, in `.gitignore`. ## Failures and flaky checks A failure is recorded too, and the newest result for a key decides the next run, so a fail always runs again. A pass and a fail on the same key means the check is flaky: it gave two answers on the same code. The repository's page in the dashboard marks those checks. ## Running a check again `--force` runs a check after a pass, and records the new result: ```bash npx hushrun run --force test -- vitest run ``` To run a `package.json` script's check again, give its name and its exact command, from the same directory, so the key is the same. `npx hushrun` finds the `hushrun` package that init added to `devDependencies`. ## Several projects in one repository By default a check's key covers every file in the repository, so a change anywhere runs every check again. `hushrun.yaml` at the repository's root narrows it in two ways. Leave out files that never change a check's result: ```yaml title="hushrun.yaml" version: 1 repository: github.com/acme/shop digest: exclude: ["**/*.md"] ``` Split the repository into scopes, and give each check its scope: ```yaml title="hushrun.yaml" version: 1 repository: github.com/acme/shop scopes: web: include: ["web/**", "package.json", "pnpm-lock.yaml"] api: include: ["api/**", "go.mod", "go.sum"] ``` ```json title="web/package.json" "scripts": { "test": "hushrun run --scope web -- vitest run" } ``` A change under `api/` no longer runs `web`'s checks again. Once `hushrun.yaml` has more than one scope, every `hushrun run` needs `--scope`; without it, the check runs every time, and hushrun says why: ```text hushrun: test running without skipping: can't tell which code this is (this policy has several scopes (api, web); pass --scope NAME or --all-scopes) ``` [Configuration](https://hushrun.com/docs/reference/configuration.md) has the whole of `hushrun.yaml`. ## Next - [Where results go](https://hushrun.com/docs/results.md): your machine, your organization, or your own server. - [In CI](https://hushrun.com/docs/ci.md) and [Agents and sandboxes](https://hushrun.com/docs/agents.md). # Where results go Source: https://hushrun.com/docs/results hushrun reads earlier results from one place and records new ones there: your organization's backend, shared by everyone who works on the repository, or this machine when there is none. ## The order hushrun takes the first of these: 1. **`HUSHRUN_BACKEND`**, when it is set. Set to nothing (`HUSHRUN_BACKEND=`), it turns results off: every check runs, and nothing is recorded. 2. **The repository's backend**, which `package.json` at the repository's root names. `npx hushrun init` adds it: ```json title="package.json" "hushrun": { "backend": "https://api.hushrun.com/o/acme" } ``` Committed with the code, it tells CI, your teammates and their agents where results go, with no variable to set. 3. **The backend you signed in to** with `hushrun login`, for repositories that name none. 4. **This machine**, with no setup: hushrun's cache directory, `~/.cache/hushrun/results` on Linux, `~/Library/Caches/hushrun/results` on macOS and `%LOCALAPPDATA%\hushrun\results` on Windows. `HUSHRUN_CACHE_DIR` moves it. A backend's URL is an organization's address on a server, `https:///o/`. It must use `https://`; `http://` works only on `localhost`, for a server in development. `hushrun whoami` shows which backend applies here, what named it, and the credential that goes with it: ```console $ hushrun whoami backend https://api.hushrun.com/o/acme (this repository's package.json names it) token none: sign in with hushrun login, or set HUSHRUN_TOKEN … ``` It exits 0 when the backend answered, 1 when it refused the credential, and 6 when it couldn't be reached. ## Signing in On a laptop, a backend needs your sign-in: ```bash npx hushrun login ``` hushrun shows a code and opens a page where you approve it, for one of your organizations; `--no-browser` prints the page's address instead. Once you approve, hushrun keeps a token that only you can read, and the organization's URL becomes your backend for repositories that name none. In a repository that names a backend, `login` signs in there. - **Another server:** `hushrun login https://hushrun.example/o/acme` signs in to an organization on a server you run. - **This machine:** signing in registers the machine. hushrun makes a key the first time, kept beside the token and never in a repository, and signs your results with it: the server records none without that signature. The dashboard lists your machines; once you revoke one there, the server records nothing it signs. A machine unused for 90 days is revoked too, until you sign in on it again, and you can have 20 in an organization. - **Signing out:** `hushrun logout` revokes the token and this machine's registration. Results then stay on this machine again. The token and the machine key live in hushrun's configuration directory: `~/.config/hushrun/` on Linux, `~/Library/Application Support/hushrun/` on macOS and `%APPDATA%\hushrun\` on Windows. `HUSHRUN_CREDENTIALS` names another file. Until you sign in, checks in a repository that names a backend still run, and at a terminal hushrun ends with one line: ```text hushrun: sign in to share results with acme: npx hushrun login ``` ## Which credential goes where hushrun sends the first of these that applies: 1. **`HUSHRUN_TOKEN`**, but only to the backend `HUSHRUN_BACKEND` names. An agent token, which starts with `hra_`, also goes to the repository's or your login's backend when it is on hushrun's hosted service, `https://api.hushrun.com`, and its results carry the signature of the key it came with, `HUSHRUN_MACHINE_KEY` ([Agents and sandboxes](https://hushrun.com/docs/agents.md#sandboxes)). 2. **Your login's token**, only to the server you signed in to. 3. **In GitHub Actions**, when the job has `id-token: write`: an ID token made for that backend alone. A repository's `package.json` is part of the code, so anyone who changes it could point it at another server. That is why a CI secret in `HUSHRUN_TOKEN` never follows it, and hushrun says so instead: ```text hushrun: HUSHRUN_TOKEN isn't sent to https://hushrun.example/o/acme: it goes only to the backend HUSHRUN_BACKEND names, and an agent token (hra_…) to hushrun's host, https://api.hushrun.com ``` Set `HUSHRUN_BACKEND` alongside the token when the backend is somewhere else. ## On this machine only With no backend, results stay in hushrun's cache directory, where nothing leaves the machine and only it reads them. A pass still skips the next run on the same files, which is useful for an agent working alone. `hushrun repository stats --local` sums up this machine's results and sends nothing. ## Turning results off ```bash HUSHRUN_BACKEND= npm test ``` Every check runs, and nothing is read or recorded. An empty `HUSHRUN_CACHE_DIR` turns off the cache on this machine in the same way. ## Your own server Any server that speaks hushrun's results protocol, `hushrun-results/v0`, can be a backend: name its organization URL in `package.json` or `HUSHRUN_BACKEND`. It is three requests under the organization's URL, `{base}`, each with `Authorization: Bearer `: | Request | Answer | |---|---| | `GET {base}/v0` | `200 {"protocol": "hushrun-results/v0", "org": "acme", "as": ""}`. `hushrun login` checks access with it, and `hushrun whoami` shows it | | `GET {base}/v0/results/{key}` | `200 {"result": }`, or `{"result": null}` | | `POST {base}/v0/results/{key}` | `2xx` once it keeps the result in the body | - `{key}` is 64 lowercase hex characters, the run key hushrun computes. The server keeps results by key; it never computes one. - Each result is a JSON object. hushrun decides what it means; the server keeps it, and who sent it: ```json {"schema": "hushrun.run-result/v0", "check": "unit", "result": "pass", "exitCode": 0, "command": ["vitest", "run"], "cwd": "", "scope": "default", "digest": "sha256:…", "revision": "", "repository": "github.com/acme/shop", "startedAt": "2026-10-16T14:05:10.123Z", "durationMs": 41000, "provider": "github", "runUrl": "https://github.com/acme/shop/actions/runs/1", "tool": "hushrun 0.1.0"} ``` - hushrun also records skips, `"result": "skip"`, with the time each saved in `savedMs`. A server keeps them for its own counts, and never answers a lookup with one. - Errors are `{"error": {"code": "…", "message": "…"}}`, and hushrun shows the message. - hushrun sends requests to that origin only, follows no redirect, and gives all of a run's requests 10 seconds. Anything else it gets back means it runs the check. Which tokens a server accepts, and whose results count, is the server's to decide. A token from `HUSHRUN_TOKEN` and `HUSHRUN_BACKEND` works with any server; `hushrun login` needs the server to offer its sign-in at `{origin}/cli/login`. # In CI Source: https://hushrun.com/docs/ci In CI, hushrun skips the checks this exact code already passed anywhere, on a laptop, in an agent's sandbox or in an earlier run, and records the rest for everyone else to reuse. ## GitHub Actions CI needs no secret. When `package.json` names the backend, as `npx hushrun init` writes it, it needs no variable either. Give each job that runs a wrapped check an ID token: ```yaml title=".github/workflows/ci.yml" jobs: test: runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npm run lint - run: npm test ``` `npm ci` installs hushrun with your other `devDependencies`, and `npm test` goes through `hushrun run` as it does everywhere else. init adds `id-token: write` for you, to the job's `permissions:` or the workflow's. When neither has one, it adds a block with `contents: read` too, since a `permissions:` block replaces the token's defaults: add any other permission the job needs. ### How CI is recognized hushrun asks GitHub for an ID token made for your organization's backend, and the backend checks which repository the job runs for. hushrun's hosted service accepts a repository that your organization connected, which init does with your sign-in. The first result it accepts pins that repository's GitHub id. A repository deleted and created again under the same name has another id, so it is refused, with a message that says why; a renamed repository keeps working. - **Pull requests from forks** get no ID token, so their checks run and nothing is recorded. - **A job without `id-token: write`** has no credential either. Its checks run, and its log ends with `hushrun: results aren't shared with acme: …` and the backend's reason. ### Without package.json For a repository without `package.json`, or a job that doesn't install your Node.js dependencies, install hushrun in the job and name the backend there: ```yaml title=".github/workflows/ci.yml" jobs: test: runs-on: ubuntu-latest permissions: contents: read id-token: write env: HUSHRUN_BACKEND: https://api.hushrun.com/o/acme steps: - uses: actions/checkout@v4 - run: npm install --global hushrun - run: hushrun run unit -- cargo test ``` Your organization's URL is in `hushrun whoami` on a signed-in machine. ## CI reuses only CI's results By default, CI skips a check that a laptop or an agent already passed on the same files. An admin can stop that for one repository: on its settings page in the dashboard, turn on **CI reuses only CI's results**. CI then skips a check only on passes CI itself recorded, while passes from laptops and agents still speed up people and agents. hushrun says so on each line: ```text hushrun: unit skipped: this exact code passed in CI 3 min ago (saves 41s); skipped only on CI's passes hushrun: unit running: no CI pass yet for this exact code; skipped only on CI's passes ``` The server enforces it; nothing in the repository can turn it off. ## Other CI GitLab CI, CircleCI, Buildkite, Jenkins and the rest record with an agent token and the key it comes with. An organization admin creates them, signed in with `hushrun login`, in the repository: ```bash hushrun token create --agent gitlab-ci --expires 365d ``` It prints the token and the key once. Store the token as a masked, protected secret named `HUSHRUN_TOKEN` in the CI's settings, and the key, the whole PEM block, as a protected secret named `HUSHRUN_MACHINE_KEY`. Where the CI can't mask a value of several lines, as on GitLab, store the key as a File variable named `HUSHRUN_MACHINE_KEY_FILE` instead: GitLab writes it to a file and sets the variable to the file's path. Then run the checks as usual: ```yaml title=".gitlab-ci.yml" test: image: node:22 script: - npm ci - npm test ``` - The token records and reads results for that one repository, as `agent:gitlab-ci`. `--org-wide` makes one for every repository of the organization. - The results it records carry the key's signature: without the key, checks still run, but their results aren't recorded. - It expires after 30 days unless `--expires` says otherwise, 365 days at most. Before it does, create the next one under the same name, update both secrets, and revoke the old one with `hushrun token revoke`. - An agent token follows `package.json`'s backend only on hushrun's hosted service. For a backend on another server, set `HUSHRUN_BACKEND` in the job too. The dashboard's **Agents** page creates the same tokens, each with its key, and lists and revokes them. [Agents and sandboxes](https://hushrun.com/docs/agents.md) has the details, which apply to CI too. # Agents and sandboxes Source: https://hushrun.com/docs/agents Coding agents run the same checks over and over. With hushrun, an agent skips every check that you, CI or another agent already passed on the same files, and its own passes let CI skip them in turn. ## Agents on your machine An agent that works on your laptop, such as Claude Code, Codex or Cursor's agent, runs your scripts the way you do. `npm test` goes through `hushrun run`, uses your sign-in, and records as you. There is nothing to set up. What an agent should know about working in a repository with hushrun, such as reading a skip as a pass, is in [For coding agents](https://hushrun.com/docs/for-agents.md), with a snippet for your `AGENTS.md`. ## Agents in sandboxes An agent in a sandbox, such as a Claude Code cloud session, `claude -p` in a container, or an E2B or Docker sandbox, has no browser to sign in with. It uses an agent token instead: one an organization admin makes, which records and reads results for one repository, as `agent:`, until it expires or someone revokes it. It can do nothing else. Every token comes with a key, the agent's machine, and hushrun's server records only results that key signed. 1. **Create a token.** On your organization's **Agents** page in the dashboard, or in the repository, signed in with `hushrun login`: ```bash hushrun token create --agent fleet-a ``` It shows the token and the agent's key once. The server keeps only the token's hash and the key's public half; the dashboard makes the key in your browser, and keeps no copy. One token and its key can serve a whole fleet of sandboxes. 2. **Give each sandbox three variables:** ```dotenv HUSHRUN_TOKEN=hra_… HUSHRUN_MACHINE_KEY="-----BEGIN PRIVATE KEY----- … -----END PRIVATE KEY-----" HUSHRUN_PROVIDER=local ``` `HUSHRUN_MACHINE_KEY` is the key shown with the token: the whole PEM block, as one value. A sandbox that keeps the key in a file outside the repository sets `HUSHRUN_MACHINE_KEY_FILE` to its path instead. Without the key, checks still run, and still skip on others' passes, but their results aren't recorded. `HUSHRUN_PROVIDER=local` matters where the sandbox sets `CI=true`, so that hushrun doesn't take it for a CI job. 3. **Let it reach** `api.hushrun.com` and your package registry. When `package.json` names your organization's backend on hushrun's hosted service, as init writes it, that's all. `npm test` in the sandbox skips what any agent, teammate or CI job already passed on the same files, and records the rest. `hushrun whoami` shows the agent, its repository and when its token expires: ```console $ hushrun whoami backend https://api.hushrun.com/o/acme (this repository's package.json names it) token HUSHRUN_TOKEN, an agent token as agent:fleet-a in acme agent fleet-a (hra_AbC123…) for github.com/acme/shop expires 2026-11-26 15:00 UTC (in 30 days) limit 300 results a minute dashboard https://app.hushrun.com/acme … ``` ### What a token covers | Option of `hushrun token create` | Default | |---|---| | `--repository github.com/acme/shop` | The repository you run it in | | `--org-wide` | Every repository of the organization, instead of one | | `--expires 90d` | 30 days; from 1 hour to 365 days, such as `12h`, `30d` or `8w` | | `--rate 1000` | The most results it records a minute: 300 on hushrun's hosted service | The key comes with every token, whatever the options. It signs the agent's results, and its [promotion evidence](https://hushrun.com/docs/promotion/evidence.md) too. `hushrun token list` shows the organization's tokens, and `hushrun token revoke` revokes one, with its key, by its prefix, or by its agent's name when only one token has it. ### Claude Code cloud sessions In the environment's settings: - the environment variables `HUSHRUN_TOKEN=hra_…`, `HUSHRUN_MACHINE_KEY`, the key's whole PEM block, and `HUSHRUN_PROVIDER=local`; - network access that reaches `api.hushrun.com`, as an allowed domain or with full access, and the npm registry; - a setup script that installs the dependencies, such as `npm ci`, which installs hushrun with them. Each session's `npm test` then goes through `hushrun run`, and skips what another session, a teammate or CI already passed on the same files. ### A container Any sandbox takes the same variables, whether through `docker run -e`, an E2B sandbox's environment or a Kubernetes job's `env`: ```bash docker run --rm \ -e HUSHRUN_TOKEN -e HUSHRUN_MACHINE_KEY -e HUSHRUN_PROVIDER=local \ -e HUSHRUN_INVOKED_BY=agent:worker-1 \ -e ANTHROPIC_API_KEY \ agent-image sh -c ' git clone https://github.com/acme/shop && cd shop && npm ci && claude -p "Fix the flaky cart test. Run npm test before you commit."' ``` ## Fleets - **One token, a label each.** `HUSHRUN_INVOKED_BY=agent:` labels each sandbox's runs, so the dashboard tells a fleet's members apart. The token still decides who recorded. - **One machine per token.** Every sandbox signs with the token's key, so the organization's machines list shows one machine for the token, however many sandboxes use it. - **A limit per token.** A token records at most so many results a minute, so a runaway fleet can't flood your organization. Past it, checks still run and earlier passes still skip; only the new result isn't recorded. - **Rotation.** Tokens expire. Create the next one under the same agent name, which comes with a new key, switch both variables, then revoke the old one. ## Another server An agent token follows `package.json`'s backend only on hushrun's hosted service, `https://api.hushrun.com`, so a change to `package.json` can't send it anywhere else. For a backend on another server, set `HUSHRUN_BACKEND` to the organization's URL in the sandbox too, or `HUSHRUN_HOST` to that server. ## When a sandbox's results aren't recorded hushrun says why in one line, and runs the check anyway: | Line | What to do | |---|---| | `test running: acme refused HUSHRUN_TOKEN: agent token hra_AbC123 (agent:fleet-a) expired on 2026-10-20` | Create another token under the same name, and update the variable | | `… was revoked on …` | The same, if the sandbox should still record | | `test passed in 41s; not recorded: agent token hra_AbC123 records results for github.com/acme/shop only, and this result is for github.com/acme/web` | The token belongs to another repository: use that repository's token, or one made with `--org-wide` | | `test passed in 41s; not recorded: agent token hra_AbC123 (agent:fleet-a) recorded 300 results in the last minute, its limit` | The fleet records faster than its token allows: slow it down, or create the token with a higher `--rate` | | `test passed in 41s; not recorded: …; upgrade: https://app.hushrun.com/acme/-/billing` | Your organization recorded every check its plan includes this period. See [Plans and limits](https://hushrun.com/docs/plans.md) | | ``test passed in 41s; not recorded: set `HUSHRUN_MACHINE_KEY` to the key `hushrun token create --agent` printed`` | The sandbox has no key, or not the one its token came with. Set `HUSHRUN_MACHINE_KEY` to the key shown with the token; if nobody kept it, create a new token | | `HUSHRUN_TOKEN isn't sent to …` | The backend isn't on hushrun's hosted service: set `HUSHRUN_BACKEND` or `HUSHRUN_HOST` | A pass an agent records skips the check in CI, as a laptop's does, unless the repository has [CI reuses only CI's results](https://hushrun.com/docs/ci.md#ci-only) turned on. # Your team on hushrun Source: https://hushrun.com/docs/teams An organization on hushrun's hosted service holds your team's repositories, the check results its members, agents and CI record, and who may change what, all in the dashboard at https://app.hushrun.com. ## Sign up Go to https://app.hushrun.com/sign-up and choose **Continue with GitHub** or **Continue with Google**. You can connect the other one later from **Account**, in the user menu at the bottom of the sidebar. The account you connect doesn't need the same email. ### Which accounts are one person A person on hushrun has at most one GitHub account and one Google account. A sign-in finds its person in this order: 1. An account already connected signs in as its person. 2. Otherwise, the account's verified email finds the person who has that email, and the account is connected to them, unless they already have an account from that provider. Google's address counts only when Google hosts it: a Gmail address, or a Google Workspace account. 3. Otherwise, hushrun makes a new person with that email. A Google account made with any other address can't join an existing person by its email: the sign-in says an account already has that email. Sign in the way you did before, then connect Google from **Account**. A GitHub account without a verified email, or a Google account whose address Google hasn't verified, can't sign in. **Account** lists your sign-in methods, and disconnects one while another stays. After you disconnect GitHub, invitations to your GitHub login no longer find you. ## Organizations - Your first sign-up makes an organization named after you, with you as its owner, and opens its **Overview**, which shows the command that sets up a repository: `npx hushrun init`. - When an invitation is waiting for you, hushrun makes no organization: you accept the invitation, or create your own. - A sign-in that starts in a terminal, from `hushrun login` or `npx hushrun init`, makes none either. The page that approves the terminal lets you name your organization, accept an invitation, or ask to join the organization the repository records to. - For another organization, open the switcher at the top of the sidebar and choose **Create organization**. It takes a name and a slug for its URLs: 2 to 39 lowercase letters, digits and hyphens, starting with a letter. You're its owner. Each organization has its own members, repositories, machines, agent tokens and plan. A terminal signed in with `hushrun login` acts as you in every organization you belong to. ## Members and roles **Members**, under Organization in the sidebar, lists everyone with their role. | Role | What it may do | |---|---| | Member | See the organization's repositories, checks, history, settings, members and usage. Record results from their own terminals, connect a repository with `npx hushrun init`, and leave | | Admin | All of that, and: invite people and answer requests to join; change roles and capabilities, and remove members, except owners; make and revoke agent tokens; see and revoke every machine; read the audit log; change the organization's and each repository's settings; add repositories to the dashboard; upgrade the plan, set the spend cap and manage billing | | Owner | All of that, and make someone an owner, or change or remove another owner | An organization always keeps an owner: make someone else an owner before the last one leaves or steps down. Owners and admins also get the usage emails described in [Plans and limits](https://hushrun.com/docs/plans.md#at-the-limit). **Approver** is a capability on top of a role, a checkbox on the Members page. It lets a member sign approvals for stages that require one, which admins and owners may do already. Approvals are covered in [Promotion](https://hushrun.com/docs/promotion.md). ## Invite people On **Members**, admins and owners invite by email address or GitHub login, with a role and, for someone who signs approvals, **Approver**. Admins invite admins and members; only owners invite owners. - hushrun sends no email, so tell the person. They see the invitation once they sign in with that email or GitHub account: under **Invitations** in the user menu, which shows a count, and on the page that approves a terminal's sign-in. - An invitation expires after 14 days. **Pending invitations** lists the open ones, and admins revoke them there. - Someone who is already a member, or already invited, can't be invited again. ## Requests to join People can also ask to get in: - When someone who isn't a member runs `npx hushrun init` in a repository that records to your organization, init offers to ask its admins to let them in. `npx hushrun init --yes` asks without the prompt. - The page that approves a terminal's sign-in offers **Request access** to the organization the terminal asked for. Admins and owners find the requests under **Requests to join** on the Members page, and a count beside Members in the sidebar. No email goes out. - **Approve as member** adds the person with the member role. - **Decline** keeps them from asking again for a week. - The person sees the answer under **Invitations**, then runs `npx hushrun init` again. - Someone who is already invited is told to accept the invitation instead. ## Machines Every sign-in with `hushrun login`, including the one `npx hushrun init` starts, registers the machine it runs on, in the organization you approve it for. The machine's key never leaves it, and signs everything it records: hushrun's server records nothing from a person or an agent without it. A browser registers its own key the first time you sign an approval with it in an organization, and an agent token comes with one machine, the key made with it. Machines are per organization: if you work in two, sign in once for each. - **Your machines** are under **Machines** in the user menu, `https://app.hushrun.com/account/machines`, for every organization, revoked ones included, with why each was revoked. Revoking one makes hushrun refuse what it writes from then on, for good: signing in again on that machine is refused, and it signs in again only with a new key. Revoking a terminal's also signs that terminal out: the token it got from `hushrun login` stops working in every organization. - **The organization's machines**, people's and agents', are under **Machines** in the Organization section, for admins and owners. Revoking one makes hushrun refuse what it writes to the organization from then on, for good. Its owner stays signed in, and can sign in on that machine again only with a new key: to keep someone out, remove them. An agent's machine is its token's only one, so the token records nothing more; to stop the agent, revoke the token. - **Unused machines.** A machine of yours that signs no result, event or approval, and isn't signed in again, for 90 days is revoked as unused, and listed that way. `hushrun login` on it brings it back. Agents' machines aren't affected. - **At most 20.** You can have 20 live machines in an organization. Signing in on a 21st is refused, with a link to your machines page: revoke one you no longer use there, then sign in again. - `hushrun logout` revokes the terminal's token and the machine it registered; `hushrun login` on the same machine registers it again. ## Agent tokens **Agents**, in the Organization section, is where admins and owners make tokens for coding agents in sandboxes and for CI outside GitHub Actions. A token records and reads check results as `agent:NAME`, for one repository or the whole organization, up to the number of results a minute set on it, until it expires (after a year at most) or someone revokes it. It does nothing else. - Each token comes with the agent's key, its one machine. The dashboard makes the key in your browser and registers its public half with the token; `hushrun token create` does the same in your terminal. Both show the token and the key once, with the environment variables to set where the agent runs: `HUSHRUN_TOKEN` and `HUSHRUN_MACHINE_KEY`. - Revoking it refuses everything that uses it from then on, and revokes its machine. The results it recorded stay. - `hushrun token create`, `hushrun token list` and `hushrun token revoke` do the same from an admin's terminal. How agents use them: [Agents and sandboxes](https://hushrun.com/docs/agents.md). ## Repositories For a repository on GitHub, `npx hushrun init` connects it to your organization: its GitHub Actions then record and read results with their own ID token, with no secret to copy. CI elsewhere uses an agent token. Both are covered in [In CI](https://hushrun.com/docs/ci.md). - Any member's init connects the repository for its CI. An admin's or owner's init also adds it to the dashboard, with its own page under **Repositories** in the sidebar, and writes its URL on hushrun into `hushrun.yaml`. Until an admin runs init there, its checks show on the organization's Overview, under **Other checks**. - **Add a repository** on the Overview, or the **+** beside Repositories in the sidebar, shows the command, and the repository appears while it runs. - hushrun keeps GitHub's numeric id for each repository, at the latest from the first request its GitHub Actions make. A renamed repository keeps working; one deleted and created again under the same name is refused, with a message that says why. ## A repository's pages Each repository's page has three tabs: - **Overview**: its checks. For each, the newest run, where it ran and who recorded it, and its runs, skips and time saved in the last 30 days. A check that passed and failed on the same code is marked **flaky**. A check opens its history: every run and skip, newest first. - **History**: what hushrun recorded for the repository, newest first: `hushrun run`'s results and the pipeline's events (checks, builds, deploys, approvals). It filters by scope, stage, kind, digest, and whether hushrun accepted the event. An event opens its details, and a digest opens its page: the commits that carried it, each stage's attempts and deploys, and the checks, artifacts and approvals recorded for it. - **Settings**: whose events count, the repository's CI rules, and how CI outside GitHub Actions takes part, with its repository tokens. The organization's **Overview** adds them up: the time `hushrun run`'s skips saved in the last 7 days, a card for each repository, recent activity, flaky checks across the organization, and checks from repositories without a page. ### CI reuses only CI's results A switch under **CI rules** on a repository's Settings, off by default. When it's on, the repository's GitHub Actions skip a check only on passes CI itself recorded. Passes from laptops and agents still let people and agents skip, but never CI. Admins and owners switch it; members see it. ## The organization's settings **Settings**, in the Organization section, lists the stages that take evidence from CI only, in every repository of the organization, such as `production`. Such a stage counts checks, artifacts and deploys only when the repository's own CI signed them; approvals count only from people. - The setting lives on hushrun's server, not in `hushrun.yaml`, so a pull request can't lift it. - It applies to what is recorded while it holds: lifting it later doesn't make that evidence count. - A repository adds stages of its own under **CI rules** on its Settings. - Admins and owners change them; members see them. What a CI-only stage means for promotion: [Promotion](https://hushrun.com/docs/promotion.md). ## Audit log **Audit log**, in the Organization section, is for admins and owners. It lists every admin action in the organization, newest first, with who did it: members, repositories, tokens, machines, approvals and billing. It filters by Members, Repositories, Repository tokens, Agent tokens, Machines, Approvals and Billing. An entry by `hushrun` is the server acting on its own, such as revoking a removed member's machines, or recording a repository's GitHub id. ## Plans and billing **Usage**, in the Organization section, at `https://app.hushrun.com//-/billing`, shows every member the plan and this period's recorded checks. Owners and admins upgrade, set the spend cap and manage billing there. Plans, limits and what counts: [Plans and limits](https://hushrun.com/docs/plans.md). ## Membership is trust No file in your repository lists who may sign. Membership in the organization decides: a teammate's evidence counts from their first sign-in, and nothing they sign after they leave counts, with nothing to merge either way. **Results that let a check skip.** `hushrun run` decides on the newest pass or fail recorded for the same check on the same code ([How skipping works](https://hushrun.com/docs/run.md)) by any of these: a member's terminal, one of the organization's agent tokens, or the GitHub Actions of a repository the organization connected. A teammate's pass lets your CI skip, and CI's lets you skip. Two exceptions: - an agent token for one repository reads only that repository's results; - with [CI reuses only CI's results](#ci-reuses-only-ci) on, the repository's CI skips only on CI's passes. **Evidence that counts for promotion.** An event counts when it is signed by a registered machine of a current member, a machine of one of the organization's agent tokens that covers the repository, a browser key a member registered by signing an approval with it, or the repository's own CI. An approval counts only from people: a member's machine or browser key, never an agent or the CI. At a [CI-only stage](#settings), checks, artifacts and deploys count only from CI. **Removing and revoking** stops what comes next, not what came before: | When | What stops | What stays | |---|---|---| | A member is removed, or leaves | Their access, at once, and their machines in the organization | Their sign-in in their other organizations; the results they recorded | | A machine is revoked | What it writes from then on | What it signed before | | An agent token is revoked | Every request that uses it, and its machine | The results it recorded | | A person's machine goes 90 days unused | What it writes, until its owner signs in on it again | What it signed before | | Someone runs `hushrun logout` | That terminal's token, and its machine | The results it recorded | An event signed before a revocation keeps counting when it carries a trusted timestamp from before it; one without a timestamp stops counting. # Plans and limits Source: https://hushrun.com/docs/plans Each plan on hushrun's hosted service includes a number of recorded checks a month: the passes and fails it records. Skips and reads are free, and at the limit your checks still run. ## The plans | Plan | Price | Included | Past what's included | How | |---|---|---|---|---| | Free | $0 | 2,000 recorded checks a month | Recording pauses until the next month; checks still run | [Start free](https://app.hushrun.com/sign-up) | | Team | $49/month, billed monthly, or $490 billed yearly | 10,000 recorded checks a month | $3 per 1,000 more, up to your spend cap ($30 of overage by default) | Checkout in the dashboard | | Growth | $99/month, billed monthly, or $990 billed yearly | 75,000 recorded checks a month | $1.50 per 1,000 more, up to your spend cap ($112.50 of overage by default) | Checkout in the dashboard | | Scale | ~$399/month, billed yearly | 300,000 recorded checks a month | Never cut off; a contract sets what is included | [Talk to the founder](https://hushrun.com/pricing#scale) | | Enterprise | From $1,500/month, billed yearly | By contract | Never cut off; a contract sets what is included | [Contact form](https://hushrun.com/pricing#enterprise) | - Prices are in US dollars. Included checks are per month, on yearly plans too. - A plan belongs to an organization ([Your team on hushrun](https://hushrun.com/docs/teams.md)). Every organization starts on Free, and the checks its members, agents and CI record all count toward its plan. - Free takes a sign-up at https://app.hushrun.com/sign-up. Owners and admins buy Team and Growth in the dashboard. Scale and Enterprise start with a conversation, from the forms on [the pricing page](https://hushrun.com/pricing); hushrun then sets the plan on your organization. ## What counts as a recorded check A recorded check is a pass or a fail that hushrun's hosted service records: - a result of `hushrun run NAME -- COMMAND` that passed or failed; - a check event from a pipeline, `hushrun run --stage` or `hushrun attest --check`, passed or failed. Everything else is free: - skips, when this exact code already passed the check; - every read: result lookups, the dashboard, `hushrun repository usage` and `hushrun repository stats`; - every other event: builds, deploys, approvals and the rest of a pipeline's record. Repeats of the same run count once: - Within a usage period, re-runs and retries of the same CI run count once per check: GitHub Actions' second and third attempts of a run, or a retried job in the same GitLab pipeline. - On a laptop or in an agent's sandbox, each run counts, and a request sent twice counts once. ## Free on your own machine or server hushrun's hosted service meters only what it records. These never reach it, and cost nothing: - results kept in hushrun's cache directory on your machine, where they go when no backend is set and you aren't signed in; - results and events recorded to a server you run. On a machine that keeps its results, `hushrun repository usage` says `results stay on this machine, so nothing is metered`. How to choose where results go: [Where results go](https://hushrun.com/docs/results.md). ## At the limit, checks still run On Free, once your organization has recorded the checks its plan includes this month, hushrun's server stops recording new passes and fails until the next month, or until an owner or admin upgrades. On Team and Growth, the same happens past the [spend cap](#spend-cap). Nothing breaks: - Checks run as they did before hushrun: with no new pass recorded, there is nothing to skip on. - Exit codes don't change. `hushrun run NAME -- COMMAND` exits with the command's code; `hushrun run --stage` exits 0 when its checks passed and 8 when one failed; `hushrun attest` exits 0. - Passes recorded before still skip: lookups and skips stay free. - Builds, deploys and the pipeline's other events are still recorded. Owners and admins get an email at 80% and 100% of the included checks, and when recording stops. The dashboard's [Usage page](#usage) shows where the organization stands. ### What the CLI prints `hushrun run` ends with its usual line, then why the result wasn't recorded and where to upgrade: - `hushrun: NAME passed in TIME; not recorded: REASON; upgrade: URL` - `hushrun: NAME failed in TIME (exit CODE); not recorded: REASON; upgrade: URL` REASON is the server's sentence, which names the day recording starts again: - on Free: `ORG used the N recorded checks its PLAN plan includes this month, so checks run instead of skipping until DATE`; - past a spend cap: `ORG reached its spend cap for this period (N recorded checks, AMOUNT of overage), so checks run instead of skipping until DATE`. URL is the organization's Usage page, `https://app.hushrun.com//-/billing`. A pipeline warns once per command: `warning: check results aren't recorded: REASON; upgrade: URL`. GitHub Actions shows it as a warning annotation titled `hushrun: check results not recorded`. The output marks each check that wasn't recorded: `hushrun run --stage` adds `not recorded` to the check's detail, and `hushrun attest` prints `not recorded check: NAME`. ## Overage and the spend cap Team and Growth keep recording past the checks they include, as overage, priced per 1,000 checks, up to the organization's spend cap. The table above shows the price and the cap each plan starts with. - Owners and admins change the cap under **Spend cap** on the Usage page: from 0, which records no overage, up to a maximum, or back to the plan's default. Each change is in the audit log. - Past the cap, recording stops, as on Free, until the period ends, the cap goes up or the plan changes. - During early access, overage isn't charged automatically. When a period ends with overage, owners and admins get an email with what it comes to: reply to settle it with the founder, or move to a plan that includes more. - When a Team organization records enough checks, the Usage page suggests Growth, and says from how many checks Growth costs less. Scale and Enterprise have no limit the server enforces: a contract says what they include. ## Usage periods - On Free, a period is the calendar month, in UTC. - On a paid plan, a period is a month from the day the subscription started, on yearly plans too. ## See where your organization stands ### In the dashboard **Usage**, under Organization in the sidebar, at `https://app.hushrun.com//-/billing`, is for every member. It shows the plan, the period and how far into it you are, the recorded checks against those included, a chart by day, any overage and what it comes to, each repository's share, and the checks `hushrun run` skipped this period, with the time they saved. ### `hushrun repository usage` ```bash hushrun repository usage ``` It asks the server that `hushrun run` records results to here, with the same credential, and prints: | Line | What it shows | |---|---| | `backend` | The server it asked, and what named it | | `org`, `plan`, `period` | The organization, its plan and the current period | | `checks` | Recorded checks this period, of those included | | `limit` | Where recording stops, and whether the organization reached it | | `overage` | Checks past the included ones and what they come to, when there are any | | `repository` | This repository's share of the period's checks | | `dashboard` | The organization's Usage page | When results stay on this machine, it says nothing is metered; when the server doesn't report usage, it says that. With `--format json`, it prints a `hushrun.repository.usage/v1` document, whose `usage.state` is `ok`, `near` (80% of the included checks), `over_included` (in overage) or `at_limit` (recording stopped). It exits 0, or 6 when the server can't be reached or refuses the credential. ### `hushrun repository stats` ```bash hushrun repository stats hushrun repository stats --local ``` It counts what hushrun recorded, so you can share your volume without telemetry. It prints the counts, and sends nothing else anywhere. - Without `--local`, it counts the repository's events on its server from one listing, reading no event: events by kind, distinct digests, and check events by stage. It needs the repository's URL on hushrun, from `hushrun.yaml` or `HUSHRUN_REPOSITORY_URL`. - With `--local`, it counts this machine's `hushrun run` results in hushrun's cache directory: by verdict, by check, distinct run keys, and the time skips saved. It sends nothing. - `--since` sets how far back it counts, such as `7d` or `12w`: 30 days by default. These count everything recorded, skips and re-runs included. What your plan meters is what `hushrun repository usage` shows. ## Upgrade or change plans Owners and admins change the plan under **Plans** on the Usage page. Members see the plans there, and ask an owner or admin. - **Upgrade to Team** and **Upgrade to Growth** open the checkout, billed yearly unless you choose monthly. Back on the Usage page, the plan changes once the payment is confirmed, usually within a minute. - On [the pricing page](https://hushrun.com/pricing), **Choose Team** and **Choose Growth** sign you in and open your organization's Usage page with that plan ready to buy. With several organizations, you choose one. Someone who is only a member is asked to go to an owner or admin, or to create an organization of their own. - **Manage billing**, once the organization has paid, opens the billing portal: changing plans, the card, invoices and cancelling. - A canceled plan lasts until the end of the period paid for; then the organization is on Free. - For Scale and Enterprise, **Talk to the founder** opens the pricing page's forms. ## Refund guarantee Paid plans come with a 60-day guarantee: if the time hushrun saved you in your first 60 days on a paid plan doesn't cover the bill, you get a refund. Refunds are handled by hand for now: [the pricing page](https://hushrun.com/pricing#refund) has the details. The Usage page and each repository's checks show the time saved. ## Other limits - hushrun's server takes only so many recorded checks a minute from one credential, set high enough to catch loops, not pipelines. Past it, a write gets 429, and `hushrun run` ends with `not recorded:` and the server's reason. - An agent token records at most the results a minute set on it: [Agents and sandboxes](https://hushrun.com/docs/agents.md). - Lookups aren't limited. # Promotion Source: https://hushrun.com/docs/promotion Promotion turns hushrun into a deploy pipeline: CI builds and validates each change once, and every later stage deploys that exact artifact, copied by digest. ## What promotion adds [`hushrun run -- COMMAND`](https://hushrun.com/docs/run.md) skips a check this exact code already passed. Its results decide only whether a check runs again. Promotion adds stages, such as staging and production, and a plan for each: - Before any work, each stage asks hushrun's planner what is left to do for this content. - Staging builds the image, runs the checks, and deploys. - Production deploys the image staging built and validated, copied to production's registry by digest. It rebuilds nothing and repeats no check. - Each step is recorded as a signed event on hushrun's server, so every decision can be checked later. ## When you need it | You want | Use | |---|---| | CI, laptops and agents to skip checks that already passed | `hushrun run` alone | | Each environment to deploy the artifact the one before it validated | Promotion | | A person's approval before a deploy | Promotion | | A stage that counts only CI's evidence | Promotion | | A signed record of every check, build and deploy | Promotion | You can use both. They don't share results: a plan counts only evidence recorded for a stage, by `hushrun run --stage` or `hushrun attest`, never the results `hushrun run -- COMMAND` keeps. Promotion asks one thing of your build: it bakes in no per-environment configuration. An image promoted to production runs there unchanged, so build-time values such as Next.js `NEXT_PUBLIC_*` or Vite `VITE_*` would carry staging's settings into production. `hushrun doctor` looks for them. ## Source digests and scopes hushrun hashes the files of each deployable scope into a source digest. Commits, branches and the version control system don't enter it, so two commits with the same files have the same digest. Here `main`'s merge commit has the files `develop` had: ```console $ hushrun digest --rev develop sha256:bad90344f9e5f7d8a3f4a46a99b40eede644f97d1dc93de3ce59c7cbb9ee28f8 default $ hushrun digest --rev main sha256:bad90344f9e5f7d8a3f4a46a99b40eede644f97d1dc93de3ce59c7cbb9ee28f8 default ``` - Without `scopes` in `hushrun.yaml`, one scope, `default`, covers every file. - A monorepo declares one scope per deployable. Each covers everything that decides its artifact and its deployment: its sources, build files, Dockerfile and deploy workflow. - `digest.exclude` leaves out files that never affect what ships, such as `**/*.md`. A commit that changes only those deploys nothing new. - `hushrun digest --explain` lists every file, with the pattern that decided its scope. ```yaml title="hushrun.yaml" scopes: api: include: ["services/api/**", "go.mod", "go.sum", ".github/workflows/deploy-*.yml", "hushrun.yaml"] web: include: ["services/web/**", ".github/workflows/deploy-*.yml", "hushrun.yaml"] digest: exclude: ["**/*.md"] ``` ## Stages and requirements A stage names its branch, the stage it comes after, and what it requires before it deploys. The whole `hushrun.yaml` of a two-stage repository: ```yaml title="hushrun.yaml" version: 1 repository: github.com/acme/api checks: lint: { kind: source, run: pnpm lint } unit: { kind: source, run: pnpm test, timeout: 20m } integration: { kind: source, run: pnpm test:integration, timeout: 30m } artifacts: image: type: oci locations: staging: us-docker.pkg.dev/acme-staging/api/server production: us-docker.pkg.dev/acme-prod/api/server stages: staging: branch: develop require: [lint, unit, integration] production: branch: main after: staging require: [lint, unit, integration] ``` - `branch`: the stage deploys only from it. A run on another branch plans `blocked`. - `after`: the upstream stage. This stage reuses its artifacts, and doesn't deploy a digest it failed. - `require` lists what must hold before the stage deploys: - **Source checks** (`kind: source`) depend only on the content, so evidence recorded at any stage counts. With `max_age`, such as `7d`, a check runs again once its evidence is older. - **Deployment checks** (`kind: deployment`) run against a stage's deployment. A later stage requires them as `@`, such as `e2e@staging`. - **Approvals** (`kind: approval`) are a person's sign-off, with no `run`: see [Approvals](https://hushrun.com/docs/promotion/evidence.md#approvals). - `artifacts` names each image and its registry repository at every stage. Every key is in [Configuration](https://hushrun.com/docs/reference/configuration.md). ## Evidence Each step of a pipeline records what it did as a signed event on hushrun's server: - check results, from `hushrun run --stage` or `hushrun attest --check`; - artifacts, from `hushrun attest --artifact`, or the copy `hushrun artifact resolve` makes; - each attempt's lifecycle, `started`, `deployed`, `passed`, `failed` or `superseded`, from `hushrun plan --start`, `hushrun mark` and `hushrun guard`; - approvals, from `hushrun approve` or the dashboard. Every signature gets a trusted timestamp, from Sigstore's timestamp authority by default. CI appends events and never overwrites or deletes one; old events go only through an org admin's `hushrun repository gc`. `hushrun verify` shows who signed each event of a digest: ```console $ hushrun verify --scope default --digest sha256:bad90344f9e5f7d8a3f4a46a99b40eede644f97d1dc93de3ce59c7cbb9ee28f8 ok v1/events/default/bad90344f9e5f7d8a3f4a46a99b40eede644f97d1dc93de3ce59c7cbb9ee28f8/01M63WSKM1RC1PA3Y6HHBTFNFC.started.staging.json: started at staging signed by ci (custody ci) for sha256:bad90344f9e5f7d8a3f4a46a99b40eede644f97d1dc93de3ce59c7cbb9ee28f8, with identity https://github.com/acme/api/.github/workflows/deploy-staging.yml@refs/heads/develop, timestamped at 2026-10-29T13:15:08.802Z ok v1/events/default/bad90344f9e5f7d8a3f4a46a99b40eede644f97d1dc93de3ce59c7cbb9ee28f8/01M63WSKS5VZAK8574WJTKDWS8.check.staging.json: check at staging signed by ci (custody ci) for sha256:bad90344f9e5f7d8a3f4a46a99b40eede644f97d1dc93de3ce59c7cbb9ee28f8, with identity https://github.com/acme/api/.github/workflows/deploy-staging.yml@refs/heads/develop, timestamped at 2026-10-29T13:15:08.966Z … ``` ## Who counts Nobody keeps a list of signers. An event counts when one of these signed it: - **A member's machine.** `hushrun login` registers the machine it runs on, and its events count while its owner is a member of the repository's org. See [Machines](https://hushrun.com/docs/teams.md#machines). - **An agent of the org.** Every agent token, made with `hushrun token create --agent NAME` or on the dashboard's Agents page, comes with a machine key of its own. See [Evidence from agents](https://hushrun.com/docs/promotion/evidence.md#agents). - **The repository's own CI.** On GitHub Actions, a job with `id-token: write` signs keylessly, as the repository's CI. See [Keyless signing](https://hushrun.com/docs/promotion/github.md#keyless). hushrun's server answers who these are, and signs its answer with the log key `hushrun.yaml` pins, so it can't change the answer silently ([Transparency log](https://hushrun.com/docs/promotion/transparency.md)). `hushrun repository signers` prints that answer. Removing someone stops the future, not the past. When a member leaves the org or logs out, or an agent token is revoked, what they sign from then on stops counting. What they signed before keeps counting, by its trusted timestamp. ### CI-only stages A stage such as production can take evidence from the repository's CI only. An org admin sets it in the dashboard: - for every repository of the org, on the org's **Settings** page; - for one repository, on its repository page. It is never in `hushrun.yaml`, so no pull request can drop it. At a CI-only stage: - checks, artifacts and lifecycle events count only when the repository's CI signed them; - an event someone else signed counts there only when its trusted timestamp falls outside every period the stage was CI-only: evidence made before the rule keeps counting, and evidence made while it held never does, even after it is lifted; - hushrun's server refuses a member's or an agent's write to the stage, other than an approval. Approvals count only from people, at every stage: a member, from a machine they signed in on or from the dashboard. Never from an agent or the CI, so no pipeline or agent approves its own deploy. `hushrun policy explain --stage production` shows whether the server makes the stage CI-only, since when, and whose events count for each requirement. ## The planner A pipeline's first job runs `hushrun plan --stage `, and every later job carries that plan out instead of deciding again. Each plan ends in one of six modes: | Mode | What the pipeline does | |---|---| | `promote` | Deploys the artifact an earlier stage built and validated, copied by digest | | `refresh` | Runs only the checks whose evidence is missing, expired or failed, then deploys the reused artifact | | `full` | Builds, runs the checks that lack valid evidence, and deploys | | `wait` | Nothing yet: an upstream run for this digest is still going, and its completion plans again | | `skip` | Nothing: this stage already runs this digest | | `blocked` | Nothing, and the job fails with exit code 3: the upstream failed this digest, the run is on another branch than the stage's, or evidence is missing that this stage can't produce, such as an approval | Here is what each mode printed for that repository, whose `develop` (0e5399f) and `main` (6083e16) have the same files. For `refresh`, its integration check had a `max_age`. `promote`: staging passed this digest, so production deploys staging's image. ```console $ hushrun plan --stage production default promote: every requirement of `production` is met: deploying image us-docker.pkg.dev/acme-staging/api/server@sha256:3cae5d5c7887d7eb92e7e4a157ea3bc4d155b7d9aad6cd9f297c72d860f4b157 by digest image: us-docker.pkg.dev/acme-staging/api/server@sha256:3cae5d5c7887d7eb92e7e4a157ea3bc4d155b7d9aad6cd9f297c72d860f4b157 requirement lint satisfied requirement unit satisfied requirement integration satisfied ``` `refresh`: integration's evidence is older than its `max_age`, so only integration runs, and the image is still reused. ```console $ hushrun plan --stage production default refresh: the artifacts are reusable: deploying image us-docker.pkg.dev/acme-staging/api/server@sha256:3cae5d5c7887d7eb92e7e4a157ea3bc4d155b7d9aad6cd9f297c72d860f4b157 by digest; running integration (expired) first checks to run: integration image: us-docker.pkg.dev/acme-staging/api/server@sha256:3cae5d5c7887d7eb92e7e4a157ea3bc4d155b7d9aad6cd9f297c72d860f4b157 requirement lint satisfied requirement unit satisfied requirement integration expired ``` `full`: staging's first run of new content. ```console $ hushrun plan --stage staging default full: no reusable `image` (none was recorded): building and running lint (missing), unit (missing), integration (missing) checks to run: lint unit integration requirement lint missing requirement unit missing requirement integration missing ``` `wait`: staging is still running this digest. ```console $ hushrun plan --stage production default wait: `staging` is still running sha256:bad90344f9e5 in github run 201 attempt 1 (https://github.com/acme/api/actions/runs/201/attempts/1); its completion triggers a new plan here requirement lint missing requirement unit missing requirement integration missing ``` `skip`: production already runs this digest. ```console $ hushrun plan --stage production default skip: `production` already runs sha256:bad90344f9e5 (deployed at 2026-10-29T13:15:17.142Z); re-run, or run manually, to deploy it again requirement lint satisfied requirement unit satisfied requirement integration satisfied ``` `blocked`: staging failed this digest. ```console $ hushrun plan --stage production default blocked: `staging` failed sha256:bad90344f9e5 in github run 201 attempt 1 (https://github.com/acme/api/actions/runs/201/attempts/1); fix forward, or force requirement lint satisfied requirement unit failed requirement integration missing ``` - `--force-full`, or `HUSHRUN_FORCE_FULL=true`, builds and runs every source check whatever the evidence. It never waives an approval, and a stage with `allow_force: false` refuses it. - `hushrun status` shows each stage's mode and requirements for the current digest, without recording anything. ## Artifacts by digest When a plan says `promote` or `refresh`, the deploy job runs `hushrun artifact resolve --stage `: 1. It verifies again the event that recorded the upstream's image. 2. When this stage's registry repository is another one, it copies the image there by digest: an index with every platform, and a mount instead of a copy on the same registry host. It tags the copy with the revision, and records it. 3. It outputs the reference to deploy, pinned by digest. When the plan says `full`, `resolve` outputs where to push the build, `:`, and `build=true`. After the push, `hushrun attest --artifact image=$REF` records the image by its digest. - If the copy fails, `resolve` falls back to a build, with a warning that says why. For Artifact Registry, Amazon ECR, Azure Container Registry and GitHub's registry, a refused copy's warning names the permissions it needs. - With `--no-fallback`, or at a stage that requires deployment checks, which a rebuilt image never passed, it exits 7 instead. - Registry credentials come from Docker's configuration, so `gcloud auth configure-docker` and `docker login` work as they are. ## Get started With hushrun installed ([Quickstart](https://hushrun.com/docs/quickstart.md)): 1. Write a starter `hushrun.yaml` for the pipeline: ```bash hushrun init --pipeline \ --staging-image us-docker.pkg.dev/acme-staging/api/server \ --production-image us-docker.pkg.dev/acme-prod/api/server ``` It writes two stages, `staging` from `develop` and `production` from `main`, with one `unit` check: `go test ./...`, `cargo test --locked`, `npm test` or `python -m pytest`, by what the repository looks like. `--staging-branch` and `--production-branch` name other branches. 2. Run `npx hushrun init`. In a repository with a `hushrun.yaml`, it adds the repository's URL on hushrun and the server's log key, and keeps the rest of the file. A repository gets its URL from an org admin's init. 3. Add the pipeline to your CI: on GitHub Actions, `hushrun adopt` writes two workflows that call hushrun's reusable workflow ([GitHub Actions](https://hushrun.com/docs/promotion/github.md)); on GitLab, use the pipeline in [GitLab CI](https://hushrun.com/docs/promotion/gitlab.md). 4. Run `hushrun doctor`, commit, and push to `develop`. `hushrun init --pipeline` never replaces a `hushrun.yaml` without `--force`. If `npx hushrun init` already wrote one, run `hushrun init --pipeline --force`, then `npx hushrun init` again: it adds the URL and the log key back. Then let agents and laptops record evidence CI's plan counts, and people approve deploys: [Evidence from agents and laptops](https://hushrun.com/docs/promotion/evidence.md). # Promotion on GitHub Actions Source: https://hushrun.com/docs/promotion/github This page sets up a repository that deploys staging from `develop` and production from `main`, so that production deploys the image staging built and validated, copied by digest. For what promotion is, start at [Promotion](https://hushrun.com/docs/promotion.md). ## What you need - Admin rights on the repository, and its default branch set to the production branch, `main`. GitHub runs `workflow_run` workflows only from the default branch, and production's workflow is started by staging's. `hushrun doctor` checks it. - An org on hushrun ([sign up](https://app.hushrun.com/sign-up)), and the repository added to it: an org admin's `npx hushrun init` adds it. - The container registries your images already use. - A build that bakes in no per-environment configuration: see [When you need it](https://hushrun.com/docs/promotion.md#when). - hushrun installed: see [Quickstart](https://hushrun.com/docs/quickstart.md). ## Set it up 1. Write the starter `hushrun.yaml`: ```bash hushrun init --pipeline \ --staging-image us-docker.pkg.dev/acme-staging/api/server \ --production-image us-docker.pkg.dev/acme-prod/api/server ``` 2. Edit `hushrun.yaml`: the checks each stage requires and their commands, the scopes of a monorepo, `digest.exclude`, and the stages ([Promotion](https://hushrun.com/docs/promotion.md#stages)). In a monorepo, each scope includes `.github/workflows/deploy-*.yml` and `hushrun.yaml`, so that a change to the deploy changes the digest. Keep `ci: {github: {workflow: deploy-staging.yml}}` on staging: it lets production see staging runs that are queued but haven't recorded anything yet. Then check the file: ```console $ hushrun policy validate hushrun.yaml is valid: 1 scope(s), 3 check(s), 1 artifact(s), 2 stage(s) ``` 3. Run `npx hushrun init`. It adds `repository_url` and `transparency_log` to `hushrun.yaml`. With the GitHub CLI (`gh`) signed in, it also tells hushrun the repository's GitHub id, which is how hushrun's server recognizes the repository's CI; without `gh`, the server learns the id from the repository's first CI run. Commit `hushrun.yaml`: hushrun reads it as committed. 4. Write the workflows: ```bash hushrun adopt ``` It writes `.github/workflows/deploy-staging.yml` and `deploy-production.yml`, two short workflows that call hushrun's reusable workflow, and keeps any file that already differs from what it would write (`--dry-run` prints what it would write, `--force` replaces). In each workflow, set `build`, `deploy` and your cloud credentials, and pin the reusable workflow to a release commit: see [The reusable workflow](#reusable-workflow). 5. Check the setup with `hushrun doctor` ([below](#doctor)). 6. Push to `develop`. Staging plans `full`: it runs the checks, builds, deploys, and records each step. Merge to `main`. Production plans `promote`: it copies staging's image to the production registry by digest and deploys it, with no build and no checks to repeat. Each plan job's step summary shows the decision and why. ## Keyless signing Give every job that runs hushrun `permissions: id-token: write`. With GitHub's ID token, the job authenticates to hushrun's server and signs its events keylessly, as the repository's CI. No secret is needed, and no key. - The job asks GitHub for an ID token, Sigstore's certificate authority (Fulcio) certifies a key that lives only in the job, and a timestamp authority timestamps each signature. - The certificate names the repository's numeric id. hushrun's server names GitHub Actions with that id as the repository's CI, so the events count as `ci`. - Any workflow of the repository signs as its CI, except a run that `pull_request_target` started, which runs the base branch's workflow for a change that hasn't merged. - In CI, hushrun checks that the server's CI is this job's repository, and exits 2 otherwise: a `repository_url` that points at another repository fails loudly. - `HUSHRUN_SIGNING=sigstore` is the default in a job with an ID token and no key; the reusable workflow sets it with `signing: sigstore`. - When Fulcio or the timestamp authority can't be reached, a signing step exits 9 and records nothing: re-run the job. Verifying needs neither. It is offline, against the Sigstore trust root built into hushrun, or a newer one that `hushrun sigstore refresh` fetches and caches. Public-good Sigstore logs every certificate it issues in a public log, with the repository's and the workflow's names. For a private repository whose names must stay out of it, name a private Sigstore instance with `HUSHRUN_FULCIO_URL`, `HUSHRUN_TSA` and `HUSHRUN_SIGSTORE_TRUSTED_ROOT`, or sign with an agent's machine key ([Evidence from agents](https://hushrun.com/docs/promotion/evidence.md#agents)). Events signed that way are the agent's, which a CI-only stage doesn't count. ### The repository's URL in CI - In CI, hushrun reads the repository's URL from the `repository_url` of the committed `hushrun.yaml`, and sends it only the job's ID token, whose audience is that URL. No variable is needed. - To set it outside the repository too, add a repository variable `HUSHRUN_REPOSITORY_URL` (Settings, Secrets and variables, Actions, Variables). `hushrun adopt`'s workflows pass it, empty when unset. When it is set, it must match `hushrun.yaml`'s, or every command that reads or writes events exits 2. - `HUSHRUN_REPOSITORY_TOKEN` goes only to a URL that `HUSHRUN_REPOSITORY_URL` or `--repository-url` names, never to one only `hushrun.yaml` names, so a change to `hushrun.yaml` can't send a secret elsewhere. ## The plan and the jobs after it The plan job decides once, and every later job carries out its plan: - **plan**: `hushrun plan --stage staging --start` decides, records `started` for the scope it deploys, writes its outputs to `$GITHUB_OUTPUT` (`mode`, `reason`, `digest`, `revision`, `attempt`, `build`, `ref`, `artifacts`, `checks`, `deployment_checks`, and the whole plan as `json`), and writes a step summary with the mode and the reason. It exits 3 when the plan is `blocked`, which fails the workflow on purpose. With several scopes, `actionable_scopes` lists the ones that deploy, in place of the per-scope outputs. - **test**: `hushrun run --stage staging` runs exactly the plan's checks, and records each result. It exits 8 when a check fails. - **deploy**: runs when the mode is `promote`, `refresh` or `full`: 1. `hushrun guard --stage staging` stops a deploy of a revision that is no longer the branch's head ([Guard](#guard)). 2. `hushrun artifact resolve --stage staging` outputs `ref`, the image to deploy, and `build`. In `promote` and `refresh`, it copies the upstream's image by digest when needed. 3. When `build` is `true`: your build pushes the image to `$REF`, then `hushrun attest --stage staging --artifact image=$REF` records it, pinned by digest. 4. Your deploy deploys `$REF`, then `hushrun mark deployed --stage staging --artifact image=$REF`. 5. `hushrun run --stage staging` runs the deployment checks other stages require of this one, the plan's `deployment_checks`. 6. `hushrun mark passed --stage staging --artifact image=$REF`. - **report**: when the run is cancelled, `hushrun mark superseded --stage staging --reason cancelled`; when a job failed or timed out, `hushrun mark failed --stage staging --reason "…"`. Each later job gets the plan as `HUSHRUN_PLAN_JSON`, from the plan job's `json` output, or as a file with `--plan plan.json`. The plan job, written by hand: ```yaml title=".github/workflows/deploy-staging.yml" jobs: plan: runs-on: ubuntu-latest permissions: { contents: read, actions: read, id-token: write } outputs: mode: ${{ steps.plan.outputs.mode }} checks: ${{ steps.plan.outputs.checks }} deployment_checks: ${{ steps.plan.outputs.deployment_checks }} json: ${{ steps.plan.outputs.json }} steps: - uses: actions/checkout@v4 - uses: hushrun/hushrun/action/setup@main # pin to a release commit with: { version: 0.1.0 } - id: plan run: hushrun plan --stage staging --start env: GITHUB_TOKEN: ${{ github.token }} # lets the plan ask GitHub about staging's runs ``` The plan asks GitHub whether upstream runs are queued, running or finished, which needs `actions: read` and `GITHUB_TOKEN`. Every job checks out the revision the plan was made for: never check out `ref: main` or pass `--rev`. ## Guard A newer commit can land on the branch while a deploy runs. `hushrun guard --stage ` checks that the revision being deployed is still the head of the stage's branch on `origin`: - It is: `guard` outputs `stale=false` and exits 0. - The branch moved on: `guard` records `superseded`, outputs `stale=true`, and exits 0. The deploy steps after it skip on `steps.guard.outputs.stale == 'true'`, and the newer run deploys instead. - `--on-stale fail` exits 4 instead of 0. `--branch` and `--remote` name another branch or remote. ## Pull requests A job on `pull_request` reads `hushrun.yaml` from the branch the pull request merges into, which was reviewed, never from the change. Fetch that branch, and name it with `--config-ref`: ```bash git fetch --depth=1 origin "$GITHUB_BASE_REF" hushrun plan --stage pr --config-ref "origin/$GITHUB_BASE_REF" ``` Without `--config-ref`, or with another branch, every command that reads or writes events refuses, and exits 2. - A pull request's jobs sign as the repository's CI too. Give their plans a stage of their own, such as a `pr` stage with no `branch` that requires the source checks, and deploy only from the stage branches. - Source checks a pull request's jobs pass count at every stage for the same digest. When the base branch hasn't moved, the merged commit has the same files, and staging doesn't run those checks again. ## The reusable workflow `hushrun adopt` writes one short workflow per stage, each calling hushrun's reusable workflow, which holds the plan, test, deploy and report jobs above. A fix to those jobs reaches your repository as a new commit of the reusable workflow. Production's, with its values set: ```yaml title=".github/workflows/deploy-production.yml" name: Deploy to Production on: push: branches: [main] workflow_run: # staging finished: plan again workflows: ["Deploy to Staging"] types: [completed] branches: [develop] workflow_dispatch: inputs: force_full: description: Build and run every check, whatever the evidence type: boolean default: false # One deploy at a time; runs started off main get a group of their own. concurrency: group: ${{ github.ref == 'refs/heads/main' && 'deploy-production' || format('deploy-production-refused-{0}', github.run_id) }} cancel-in-progress: false permissions: contents: read actions: read # the plan asks GitHub about earlier runs id-token: write # workload identity federation; ID tokens for hushrun's server jobs: deploy: uses: hushrun/hushrun/.github/workflows/hushrun-deploy.yml@main # pin to a release commit with: stage: production version: 0.1.0 repository-base: ${{ vars.HUSHRUN_REPOSITORY_BASE }} repository-url: ${{ vars.HUSHRUN_REPOSITORY_URL }} force-full: ${{ inputs.force_full || false }} toolchain: node setup: gcloud auth configure-docker us-docker.pkg.dev --quiet build: docker build -t "$REF" . && docker push "$REF" deploy: gcloud run deploy api --image "$REF" --region us-central1 gcp-workload-identity-provider: projects/123456789/locations/global/workloadIdentityPools/github/providers/github gcp-service-account: deployer@acme-prod.iam.gserviceaccount.com signing: sigstore # keylessly, as this repository's CI deploy-environment: production ``` Its inputs: | Input | What it is | |---|---| | `stage` | The stage to deploy, as `hushrun.yaml` names it. Required | | `version` | The exact hushrun release to install, such as `0.1.0`. Required | | `deploy` | The deploy command. `$REF` is the image, pinned by digest. Required | | `build` | The build command, run when the plan builds. It pushes the image to `$REF` | | `setup` | Commands every job runs after its credentials, such as `gcloud auth configure-docker` | | `image` | The artifact of `hushrun.yaml` that `build` and `deploy` work on. Default `image` | | `scope` | The scope to deploy, in a repository with several. Default its only one | | `force-full` | Build and run every check, whatever the evidence | | `isolated` | Run the plan's checks in their pinned images with Docker, as agents run them ([pinned images](https://hushrun.com/docs/promotion/evidence.md#images)) | | `toolchain`, `toolchain-version` | The toolchain the checks need, `go`, `node`, `python`, `java` or `none`, and its version | | `runs-on` | The runner of every job. Default `ubuntu-latest` | | `signing` | `sigstore` to sign keylessly, as the repository's CI | | `deploy-environment` | The GitHub environment of the deploy job | | `signing-environment` | The GitHub environment of the jobs that sign | | `repository-url`, `repository-base` | The repository's URL from your settings. Empty, hushrun reads `hushrun.yaml`'s | | `gcp-workload-identity-provider`, `gcp-service-account` | Google Cloud credentials through workload identity federation | | `aws-role-to-assume`, `aws-region` | AWS credentials through GitHub's OIDC provider | | `azure-client-id`, `azure-tenant-id`, `azure-subscription-id` | Azure credentials through a federated credential | Its outputs are `mode`, the plan's mode, and `ref`, the image deployed. No job needs a cloud secret: credentials come through your cloud's workload identity federation with GitHub's ID token. ### A monorepo Call the reusable workflow once per scope. Each plans its own digest, so a scope whose files didn't change skips while the others deploy. `build` and `deploy` see the scope as `$SCOPE`: ```yaml title=".github/workflows/deploy-staging.yml" jobs: deploy: strategy: fail-fast: false matrix: scope: [api, web] uses: hushrun/hushrun/.github/workflows/hushrun-deploy.yml@main # pin to a release commit with: stage: staging scope: ${{ matrix.scope }} image: ${{ matrix.scope }}-image version: 0.1.0 build: docker build -t "$REF" "services/$SCOPE" && docker push "$REF" deploy: gcloud run deploy "$SCOPE" --image "$REF" --region us-central1 signing: sigstore ``` ## Check the setup ```bash hushrun doctor ``` `doctor` exits 2 when a check fails. It checks: - git, and the CI it runs in; - `hushrun.yaml`; - hushrun's server: that it is reachable, speaks hushrun's protocol and holds this repository, and how far this machine's clock is from it; - the transparency log, against `hushrun.yaml`'s `transparency_log`; - who counts, and, with `HUSHRUN_SIGNING` or a key set, whether this job or machine signs as someone who counts, and whether the timestamp authority answers; - read access to the registries `hushrun.yaml` names; - that every scope covers the workflows that run hushrun; - configuration baked into the build, such as `NEXT_PUBLIC_*` or `VITE_*`; - with `GITHUB_TOKEN` (or `GH_TOKEN`) and `GITHUB_REPOSITORY` set, that the default branch is the branch of the stages an upstream starts. ## Day to day - **A plan says `wait`.** Staging is still running this digest. Production plans again when staging's workflow completes, through `workflow_run`: nothing to do. - **A plan says `blocked`.** The reason names the run and what to do, usually "fix forward, or force". - **Forcing a full build.** Run the production workflow by hand with `force_full`. A forced run rebuilds and runs every source check, but never waives an approval, and a stage with `allow_force: false` refuses it. - **A plan says `refresh`.** Evidence expired (`max_age`), or a check's definition changed. Only those checks run, and the image is still reused. - **A newer commit landed while deploying.** `guard` records `superseded`, and the deploy steps skip. The newer run deploys instead. - **Re-running failed jobs.** A re-run is a new attempt of the same run, and hushrun records it as one. - **Old events.** An org admin runs `hushrun repository gc --older-than 180d --dry-run` to see what would go, then without `--dry-run`. It keeps the newest deployment of each scope and stage. - **The org's plan is used up.** New check results aren't recorded, and `run` and `attest` say so once, with where to upgrade. Everything else is recorded, and deploys go on: see [Plans and limits](https://hushrun.com/docs/plans.md#at-the-limit). ## Exit codes The ones a pipeline meets most, from [the full list](https://hushrun.com/docs/reference/cli.md#exit-codes): | Code | Meaning | |---|---| | 2 | A usage or configuration error, such as a `repository_url` that doesn't match, or a pull request without `--config-ref` | | 3 | `plan`: blocked | | 4 | `guard --on-stale fail`: the branch moved on | | 6 | hushrun's server is unavailable or refused access, or a transparency check failed | | 7 | `artifact resolve` couldn't copy the image and may not fall back to a build | | 8 | `run --stage`: a check failed, and its result was recorded | | 9 | The signer is unavailable: GitHub's ID token, Fulcio, or the timestamp authority | # Promotion on GitLab CI Source: https://hushrun.com/docs/promotion/gitlab This page sets up a GitLab project that deploys staging from `develop` and production from `main`, so that production deploys the image staging built and validated, copied by digest. For what promotion is, start at [Promotion](https://hushrun.com/docs/promotion.md). ## How a GitLab pipeline signs hushrun's server verifies GitHub Actions' ID tokens, not GitLab's. So a GitLab pipeline signs and writes as one of the org's agents, with two of the project's CI/CD variables: - **The agent's token**, `HUSHRUN_TOKEN`. hushrun sends it to the repository's URL that `hushrun.yaml` names, because that URL is on hushrun's host, which takes an agent token for its org only. Other tokens, such as `HUSHRUN_REPOSITORY_TOKEN`, never go to a URL that only a file in the repository names. - **The machine key** registered with the token, `HUSHRUN_MACHINE_KEY_FILE`. It signs every event, and the events count as `agent:` at every stage that isn't CI-only, until an admin revokes the token. Events it signed before keep counting. A CI-only stage counts only the repository's own CI, so don't make a GitLab project's stages CI-only: its pipeline's events wouldn't count there. ## What you need - The Maintainer role on the project, with `develop` and `main` protected (Settings, Repository, Protected branches). Protected variables reach only protected branches, so the agent's token and key reach only your stage pipelines. - GitLab 17.0 or later, on GitLab.com or self-managed. From 17.0, GitLab runs a job's `after_script` when it cancels the job, which is how the pipeline records a cancel. - An org on hushrun ([sign up](https://app.hushrun.com/sign-up)), and the project added to it: an org admin's `npx hushrun init` adds it. - An org admin, to make the pipeline's agent token. - The container registries your images already use, and a build that bakes in no per-environment configuration: see [When you need it](https://hushrun.com/docs/promotion.md#when). - Linux runners. The install script supports Linux and macOS, and hushrun's container image is `linux/amd64` and `linux/arm64`. ## Set it up 1. Write the starter `hushrun.yaml`: ```bash hushrun init --pipeline \ --staging-image us-docker.pkg.dev/acme-staging/api/server \ --production-image us-docker.pkg.dev/acme-prod/api/server ``` It recognizes a GitLab project by its `.gitlab-ci.yml`, or by its host, such as `gitlab.com` or `gitlab.example.com`. 2. Edit `hushrun.yaml`: the checks each stage requires and their commands, the scopes of a monorepo, `digest.exclude`, and the stages ([Promotion](https://hushrun.com/docs/promotion.md#stages)). Each scope covers `.gitlab-ci.yml` and anything it includes, so that a change to the pipeline changes the digest. Production finds staging's pipelines on staging's branch; when they run on another ref, name it on staging with `ci: {gitlab: {ref: }}`. 3. Run `npx hushrun init`. It adds `repository_url` and `transparency_log` to `hushrun.yaml`. Commit `hushrun.yaml`: hushrun reads it as committed, and the pipeline takes the repository's URL from it. 4. Make the pipeline's agent token, as an org admin, in the project: ```bash hushrun token create --agent gitlab-ci --expires 365d ``` It prints the token, `hra_…`, and the agent's machine key, a PEM block, once: every agent token comes with its key. A token lasts 30 days unless `--expires` says otherwise, 365 days at most: make the next one under the same name before it expires. 5. Set the CI/CD variables ([below](#variables)). 6. Add the pipeline ([below](#pipeline)): `.gitlab-ci.yml` at the project's root, and `install-hushrun.sh` at `ci/install-hushrun.sh`. 7. Check the setup with `hushrun doctor` ([below](#doctor)). 8. Push to `develop`. Staging plans `full`: it runs the checks, builds, deploys, and records each step. Merge to `main`. Production plans `promote`: it copies staging's image to the production registry by digest and deploys it, with no build and no checks to repeat. The plan job's log shows the decision and why. ## CI/CD variables In Settings, CI/CD, Variables: | Variable | Type | Flags | Value | |---|---|---|---| | `HUSHRUN_TOKEN` | Variable | Masked, protected | The agent's token, `hra_…` | | `HUSHRUN_MACHINE_KEY_FILE` | File | Protected | The agent's machine key: the whole PEM block | | `HUSHRUN_GITLAB_TOKEN` | Variable | Masked | A project access token with the Reporter role and `read_api` | - The key is a File variable, because GitLab can't mask a PEM key. GitLab writes it to a file outside the repository, and `HUSHRUN_MACHINE_KEY_FILE` holds the file's path. Every job signs with it, so the pipeline sets nothing for signing. - The pipeline takes the repository's URL from `hushrun.yaml`, so it needs no `HUSHRUN_REPOSITORY_URL`. Set one only to pin the URL in the project's settings too: it must then match `hushrun.yaml`'s, or commands exit 2. - On a hushrun server of your own, set `HUSHRUN_HOST` to its origin, such as `https://hushrun.example.com`, so that the agent's token goes there. - With `HUSHRUN_GITLAB_TOKEN`, plans ask GitLab whether staging's pipelines are still running, or how they ended. Make it in Settings, Access tokens. Without it, a staging pipeline that ended without recording its outcome holds production until its lease runs out, 3 hours by default. - Variables someone sets when they run a pipeline outrank project variables, so whoever may set them could point the pipeline at another server, or change how it signs. Set Settings, CI/CD, Variables, **Minimum role to use pipeline variables** to **No one allowed**, or to **Maintainer** if Maintainers force full builds. `hushrun doctor` reports the setting when its token may read it. ## The pipeline Take `install-hushrun.sh` from the hushrun release you pin, and read it before you commit it: it installs that exact release for the runner's OS and architecture, after checking the archive's SHA-256 against the checksum pinned in it. `HUSHRUN_DOWNLOAD_URL` points it at a mirror of the release archives. Then add this `.gitlab-ci.yml`, and set: - `HUSHRUN_VERSION` and `HUSHRUN_IMAGE`, the same exact version, and the image's digest; - the `test` job's image: the toolchain your checks need; - the `deploy` job's image and scripts: `./build.sh "$HUSHRUN_REF"` pushes the image to `$HUSHRUN_REF`, and `./deploy.sh "$HUSHRUN_REF"` deploys it, pinned by digest; - your cloud's credential step for the registry and the deploy, in `.hushrun` ([Cloud access](#cloud-access)). ```yaml title=".gitlab-ci.yml" # Staging deploys develop; production deploys main with the image staging built and validated, # copied by digest. Names, images and the build and deploy scripts are placeholders. # The project's CI/CD variables, never set in this file: HUSHRUN_TOKEN, HUSHRUN_MACHINE_KEY_FILE # and HUSHRUN_GITLAB_TOKEN (hushrun.com/docs/promotion/gitlab#variables). workflow: rules: - if: '$CI_COMMIT_BRANCH == "develop"' variables: HUSHRUN_STAGE: staging - if: '$CI_COMMIT_BRANCH == "main"' variables: HUSHRUN_STAGE: production - when: never stages: [plan, test, deploy, finish, notify] variables: HUSHRUN_VERSION: "0.1.0" # exact, as is the image tag below; never a moving tag HUSHRUN_IMAGE: ghcr.io/hushrun/hushrun:0.1.0 # pin its digest too: …:0.1.0@sha256:… # Every job that runs hushrun extends this. GitLab's `extends` replaces a before_script rather # than merging it, so your cloud's credential step, if it needs one, goes here too. .hushrun: before_script: # The image has hushrun; on any other image, install the pinned release, checked against # its checksum. - '[ "$(hushrun --version 2>/dev/null)" = "hushrun $HUSHRUN_VERSION" ] || export PATH="$(dirname "$(sh ci/install-hushrun.sh)"):$PATH"' # A cancel records `superseded`: GitLab runs after_script, in a shell of its own, when it # cancels a running job. A timeout is a failure, which mark-failed records. after_script: - | if [ "$CI_JOB_STATUS" = canceled ] && [ -f plan.json ]; then case "$HUSHRUN_MODE" in promote | refresh | full) [ "$(hushrun --version 2>/dev/null)" = "hushrun $HUSHRUN_VERSION" ] || export PATH="$(dirname "$(sh ci/install-hushrun.sh)"):$PATH" hushrun mark superseded --stage "$HUSHRUN_STAGE" --plan plan.json --reason cancelled || true ;; esac fi plan: stage: plan extends: .hushrun image: $HUSHRUN_IMAGE script: - hushrun plan --stage "$HUSHRUN_STAGE" --start --out plan.json --dotenv plan.env artifacts: paths: [plan.json] reports: dotenv: plan.env # HUSHRUN_MODE, HUSHRUN_CHECKS, HUSHRUN_DIGEST, HUSHRUN_REF, … test: stage: test extends: .hushrun image: golang:1.25 # the toolchain the checks need needs: [plan] script: - '[ -n "$HUSHRUN_CHECKS" ] || { echo "nothing to run"; exit 0; }' - hushrun run --stage "$HUSHRUN_STAGE" --plan plan.json # exactly the plan's checks deploy: stage: deploy extends: .hushrun image: google/cloud-sdk:slim # what build.sh and deploy.sh need needs: [plan, test] resource_group: deploy-$HUSHRUN_STAGE # one deploy at a time per stage script: - 'case "$HUSHRUN_MODE" in promote|refresh|full) ;; *) echo "mode $HUSHRUN_MODE: nothing to deploy"; exit 0 ;; esac' # Each output goes through a variable, so a command that fails fails the job. - out=$(hushrun guard --stage "$HUSHRUN_STAGE" --plan plan.json --format env); eval "$out" - '[ "$HUSHRUN_STALE" != true ] || { echo "a newer revision took over"; exit 0; }' - out=$(hushrun artifact resolve --stage "$HUSHRUN_STAGE" --plan plan.json --format env); eval "$out" - | if [ "$HUSHRUN_BUILD" = true ]; then ./build.sh "$HUSHRUN_REF" out=$(hushrun attest --stage "$HUSHRUN_STAGE" --plan plan.json --artifact "image=$HUSHRUN_REF" --format env) eval "$out" fi - ./deploy.sh "$HUSHRUN_REF" - hushrun mark deployed --stage "$HUSHRUN_STAGE" --plan plan.json --artifact "image=$HUSHRUN_REF" # Deployment checks run here: hushrun run e2e --stage "$HUSHRUN_STAGE" --plan plan.json - hushrun mark passed --stage "$HUSHRUN_STAGE" --plan plan.json --artifact "image=$HUSHRUN_REF" mark-failed: stage: finish extends: .hushrun image: $HUSHRUN_IMAGE when: on_failure script: - 'case "$HUSHRUN_MODE" in promote|refresh|full) ;; *) exit 0 ;; esac' # nothing started - hushrun mark failed --stage "$HUSHRUN_STAGE" --plan plan.json --reason "pipeline failed" # Staging only: plan production again once staging's outcome is recorded, after mark-failed. plan-production: stage: notify rules: - if: '$CI_COMMIT_BRANCH == "develop"' when: always inherit: variables: false # forward nothing: staging's HUSHRUN_STAGE must not reach production trigger: project: $CI_PROJECT_PATH # this project; the downstream pipeline counts as an upstream trigger branch: main ``` What each job does: - **plan** decides once. It records `started`, keeps the plan as the artifact `plan.json`, and passes its outputs to later jobs as a dotenv report: `HUSHRUN_MODE`, `HUSHRUN_CHECKS`, `HUSHRUN_DIGEST`, `HUSHRUN_REF` and more. The dotenv report leaves out the plan itself, which is why `plan.json` travels as an artifact. A `blocked` plan exits 3, which fails the pipeline on purpose. - **test** runs exactly the plan's checks, and exits 8 when one fails. - **deploy** runs `guard`, which stops a deploy of a revision that is no longer the branch's head; `artifact resolve`, which copies the upstream's image by digest, or asks for a build; the build and `attest` when the plan builds; your deploy; and `mark deployed` and `mark passed`. Deployment checks other stages require of this one run between the two marks. - **mark-failed** records `failed` when a job failed or timed out. A job GitLab cancels records `superseded` from the `.hushrun` template's `after_script`. - **plan-production**, on staging's pipelines only, starts a pipeline on `main` once staging's outcome is recorded, so production plans again. It forwards no variables, and it runs even when no one may set pipeline variables. Starting a pipeline on a protected `main` needs the right to run pipelines there. Two rules hold in every job: - A command whose output a job evaluates goes through a variable, `out=$(hushrun guard … --format env); eval "$out"`, because `eval "$(hushrun guard …)"` succeeds when hushrun fails. - Every job carries out the plan for the pipeline's commit: never check out another ref, and never pass `--rev`. Jobs that only run hushrun use the image `ghcr.io/hushrun/hushrun:`: Alpine, git, CA certificates and hushrun's static binary, with no entrypoint. The other jobs install the same release with the script. ## Cloud access hushrun's server needs no cloud credentials. Your registry and your deploy do: - Give the jobs those roles through your cloud's workload identity federation with GitLab's ID tokens, with no long-lived cloud key. - Bind the access to the project's id and a protected ref. - Put the credential step in `.hushrun`'s `before_script`: a job's own `before_script` would replace the template's. ## Merge requests In a merge request's pipeline, hushrun reads `hushrun.yaml` from the branch the merge request targets, which was reviewed, never from the change. Every command that reads or writes events needs that branch, fetched, in `--config-ref`, or it exits 2: ```bash git fetch --depth=1 origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" hushrun plan --stage pr --config-ref "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" ``` - The agent's token and key are protected variables, so they don't reach a merge request pipeline from an unprotected branch. The pipeline on this page runs hushrun's jobs on `develop` and `main` only. - A merge request pipeline has no branch, so it never stands for a deploy stage. Give its checks a stage of their own, such as a `pr` stage with no `branch`, and deploy only from the stage branches. ## Check the setup From a job, or on your machine with `HUSHRUN_GITLAB_TOKEN` and `CI_PROJECT_PATH` set (and `CI_API_V4_URL` on a self-managed GitLab): ```bash hushrun doctor ``` Besides what it checks everywhere ([GitHub Actions](https://hushrun.com/docs/promotion/github.md#doctor) lists it), `doctor` checks on GitLab that `HUSHRUN_GITLAB_TOKEN` can read the project, that the stage branches are protected, and who may set pipeline variables. In a job, it checks that the server lists the job's machine key for the agent. It exits 2 when a check fails. ## Day to day - **A plan says `wait`.** Staging is still running this digest. Staging's `plan-production` job plans production again when staging is done: nothing to do. - **A plan says `blocked`.** The reason names the pipeline and what to do, usually "fix forward, or force". - **Forcing a full build.** With **Minimum role to use pipeline variables** set to **Maintainer**, a Maintainer runs the pipeline for `main` with `HUSHRUN_FORCE_FULL=true`. A forced run rebuilds and runs every source check, but never waives an approval, and a stage with `allow_force: false` refuses it. - **Retrying a job.** GitLab keeps the pipeline's id, and hushrun counts the attempts: a retried plan job starts a new attempt. - **A newer commit landed while deploying.** `guard` records `superseded`, and the deploy job stops. The newer pipeline deploys instead. - **The token expires.** Make the next one under the same agent name, which comes with a new key, and replace `HUSHRUN_TOKEN` and `HUSHRUN_MACHINE_KEY_FILE`. Then revoke the old one with `hushrun token revoke hra_XXXXXX`, its prefix from `hushrun token list`. Events the old key signed before keep counting. - **The org's plan is used up.** New check results aren't recorded, and `run` and `attest` say so once, with where to upgrade. Everything else is recorded, and deploys go on: see [Plans and limits](https://hushrun.com/docs/plans.md#at-the-limit). Exit codes are the same as on GitHub: see [Exit codes](https://hushrun.com/docs/promotion/github.md#exit-codes). ## Limits - **Child pipelines** record runs under their own pipeline id: run hushrun's jobs in the parent pipeline. - **Merge request pipelines** are for checks, not stages ([above](#merge-requests)). - **Windows runners**: the install script is for Linux and macOS. - **`hushrun adopt`** writes GitHub workflows only: start a GitLab project from the pipeline on this page. - **Keyless signing** with GitLab's ID tokens (`id_tokens: {SIGSTORE_ID_TOKEN: {aud: sigstore}}` and `HUSHRUN_SIGNING: sigstore`) counts as the project's CI only on a server that verifies GitLab's tokens for the project. hushrun's server doesn't, so sign with the agent's key there. # Evidence from agents and laptops Source: https://hushrun.com/docs/promotion/evidence A coding agent, or a person, can run a stage's checks before pushing, in the image CI runs them in, on exactly the files about to be committed, and sign what happened. CI's plan then counts that evidence and skips those checks. To skip checks with `hushrun run -- COMMAND` alone, an agent token is all a sandbox needs: see [Agents in sandboxes](https://hushrun.com/docs/agents.md#sandboxes). This page is about promotion, and assumes the pipeline is set up ([Promotion](https://hushrun.com/docs/promotion.md)). ## How it works ```bash hushrun run --all --stage staging --isolated --worktree ``` - `--worktree` digests the files as `git add -A && git commit` would record them now: the evidence is about the commit about to be made. - `--all` runs every source check the stage requires that has no valid evidence yet. - `--isolated` runs each check in its pinned image and platform, with Docker or Podman, on an export of exactly the digested files. The check gets no network unless it declares one, and only the variables its `env` names: never a secret, and never the signing key, which stays with hushrun. - The evidence records custody `isolated`, with the image, platform and network it ran with. CI's plan counts it at every stage that isn't CI-only. Then commit exactly what was validated, with no other change, and push. If the files differ from what ran, the digest differs, and CI runs the checks itself. ## Pin the images CI runs checks in An isolated run needs the check's `image`, pinned by digest, and its `platform`. A check that pins them counts evidence only from that image and platform, so pin the image your CI job already uses: ```yaml title="hushrun.yaml" checks: unit: kind: source run: go test ./... image: golang@sha256: platform: linux/amd64 env: [GOFLAGS] # the only variables the check sees network: none # the default ``` - `network` is `none` by default. `open` is for a check that downloads. `restricted` runs the check on the Docker or Podman network that `HUSHRUN_RESTRICTED_NETWORK` names, one whose egress you restrict. - CI must run the check in the same image. With the reusable workflow on GitHub, set `isolated: true`: its test job then runs the plan's checks with `hushrun run --isolated` too. Otherwise run the CI job in that image, and pass it to `hushrun run --stage` as `--environment-image @sha256:…`. - A check whose `env` names a secret never runs isolated: keep it in CI. hushrun refuses any name that contains `TOKEN`, `SECRET`, `PASSWORD`, `PASSWD`, `PASSPHRASE`, `CREDENTIAL`, `PRIVATE`, `AUTH`, `ACCESS_KEY`, `APIKEY`, `DATABASE_URL` or `_KEY_`, ends in `_KEY`, `_PAT`, `_DSN` or `_PWD`, or is one of a few more, such as `GOOGLE_APPLICATION_CREDENTIALS`, `REDIS_URL` and `SSH_AUTH_SOCK`. The list is a safety net: name in `env` only what the check needs. ## Give each agent a key An agent signs with the machine key its agent token came with. Its events count because the agent is one of the org's signers: nothing goes in `hushrun.yaml`. An org admin makes the token, which always comes with its key, in the repository or on the dashboard's **Agents** page: ```bash hushrun token create --agent claude-code ``` It prints the token, `hra_…`, and the machine key, once. Every sandbox of the agent signs as that one machine. Give each sandbox: | Variable | Value | |---|---| | `HUSHRUN_TOKEN` | The agent token | | `HUSHRUN_MACHINE_KEY` | The machine key: the whole PEM block, as one value. Or `HUSHRUN_MACHINE_KEY_FILE`, the path of a file outside the repository that holds it | | `HUSHRUN_PROVIDER` | `local`, in a sandbox that sets `CI=true`, so that hushrun doesn't take it for CI | | `HUSHRUN_INVOKED_BY` | Optional: `agent:`, which events record. Nothing decides on it | The sandbox also needs Docker or Podman. `HUSHRUN_CONTAINER_RUNTIME` picks one, or names another Docker-compatible CLI. Exports go under hushrun's cache directory, which Docker Desktop, Colima and Podman machines share with their VMs; `HUSHRUN_EXPORT_DIR` moves them. - The token lasts 30 days unless `hushrun token create --expires` says otherwise, 365 days at most. The same token serves `hushrun run -- COMMAND`'s results ([Agents in sandboxes](https://hushrun.com/docs/agents.md#sandboxes)). - The key never goes in the repository: a key there would be part of what `--worktree` digests and the agent commits. hushrun refuses a key file inside the repository, and an isolated run refuses to export one. - Revoking the token, with `hushrun token revoke claude-code` or in the dashboard ([Agent tokens](https://hushrun.com/docs/teams.md#agent-tokens)), stops what the agent signs from then on from counting. What it signed before keeps counting. - `HUSHRUN_SIGNING=machine hushrun doctor` says whether hushrun's server counts the machine, and as whose. ## People's machines A teammate's machine counts the same way, with no token to make. `hushrun login` registers the machine it runs on, with a key hushrun makes the first time and keeps beside its credentials, never in a repository. Its events count while its owner is a member of the org, at every stage that isn't CI-only. - On a signed-in machine, hushrun signs with that key by default, but never in CI: there it signs with a machine key only when `HUSHRUN_MACHINE_KEY` or `HUSHRUN_MACHINE_KEY_FILE` gives one, so a person's sign-in left on a runner never signs a pipeline's events as that person. - `hushrun whoami` shows the machine's key id. - `hushrun logout` revokes the sign-in and the machine with it. What it signed before keeps counting. - A machine unused for 90 days is revoked, and `hushrun login` on it brings it back. A person has at most 20 machines in an org. - Checks that pin no image can also run on the machine itself, with `hushrun run --all --stage staging --worktree`: that evidence records custody `local`. The organization's machines, and how to revoke one, are in [Machines](https://hushrun.com/docs/teams.md#machines). ## Keep a stage to CI's evidence A stage that should never count an agent's or a laptop's evidence, such as production, is made CI-only in the dashboard: for every repository of the org on the org's **Settings** page, or for one repository on its repository page. It isn't in `hushrun.yaml`, so no pull request, an agent's included, can drop it. - At a CI-only stage, checks, artifacts and lifecycle events count only when the repository's CI signed them. Approvals still come from people. - Evidence others signed before the stage became CI-only keeps counting there; evidence signed while it was CI-only never does. - A GitLab pipeline signs as an agent, so don't make a GitLab project's stages CI-only ([GitLab CI](https://hushrun.com/docs/promotion/gitlab.md#signing)). More in [CI-only stages](https://hushrun.com/docs/promotion.md#ci-only). ## Tell the agent Add this to `AGENTS.md`, or the file your agent reads, and adjust the stage: ```markdown ## Before pushing Run `hushrun run --all --stage staging --isolated --worktree`. It runs the missing checks in the pinned CI image on exactly the content you are about to commit, and records signed evidence. Commit everything it validated, with no other changes, then push. CI then skips those checks. ``` ## Approvals A stage that requires an approval check waits until someone approves the digest it would deploy. Until then its plan says `blocked`, and names the approval: ```yaml title="hushrun.yaml" checks: release: { kind: approval } stages: production: branch: main after: staging require: [lint, unit, integration, release] ``` From your own machine, signed in with `hushrun login`: ```bash hushrun approve release --stage production --comment "QA signed off" ``` - It shows what you approve, the digest and its revision, what the stage runs now, and the `git log` range between the two. Then it signs with your machine's key, and records the approval. - It checks first that the approval would count. If it wouldn't, it records nothing, and says why. - Approvals count only from people. `hushrun approve` refuses to run in CI or as an agent, and a plan never counts an approval the CI or an agent signed, at any stage: no pipeline or agent approves its own deploy. - `--out approval.json` writes the signed approval to a file instead of recording it, to upload in the dashboard. In the dashboard, an approval is given on the digest's review page, reached from the overview's "waits for approval" and from the digest's page. Approving there registers your browser's key as your machine. Then run the stage's plan again, such as by re-running its workflow: it deploys. A forced run never waives an approval. ## When something is refused | Message | What to do | |---|---| | ``pins no `image`, so it can't run isolated`` | Pin the check's image, by digest ([above](#images)) | | `an isolated run never passes secrets in` | Keep that check in CI, or drop the variable from its `env` | | ``declares `network: restricted`, which hushrun can't provide`` | Set `HUSHRUN_RESTRICTED_NETWORK` to a network whose egress you restrict | | `isn't on PATH: install Docker or Podman` | Install one, or set `HUSHRUN_CONTAINER_RUNTIME` | | `collides with another digested path` | Two paths differ only in case: set `HUSHRUN_EXPORT_DIR` to a case-sensitive directory | | `here digests to …, not …` | The checkout isn't the content named: run where it is, with `--worktree` for uncommitted changes | | `a machine's key must never be committed` | Keep the key in hushrun's configuration directory, or set `HUSHRUN_MACHINE_KEY_FILE` outside the repository | | `this command signs events` | Sign in with `hushrun login`, or set `HUSHRUN_MACHINE_KEY` | | `no valid signature by a machine of a member or an agent of the org` | Sign in with `hushrun login`, or give the agent the key its token came with, in `HUSHRUN_MACHINE_KEY` | | `this approval wouldn't count` | Approve from a machine registered with `hushrun login` | | `approvals count only from people` | An agent or the CI tried to approve: a member approves on their own machine with `hushrun approve`, or in the dashboard | # Transparency log Source: https://hushrun.com/docs/promotion/transparency hushrun's server keeps each repository's events in an append-only log, and signs what it says about that log with a key `hushrun.yaml` pins, so every command can check the server instead of trusting it. ## The log key in hushrun.yaml Every write to a repository on hushrun is a leaf of an append-only Merkle log. The server signs the log's checkpoints, each listing it answers, and its answer about who counts, with the repository's log key. `hushrun.yaml` names that key at its top level, so the server can't vouch for itself: ```yaml title="hushrun.yaml" transparency_log: origin: api.hushrun.com/l/acme/api key: "ed25519:…" ``` - `npx hushrun init` writes these lines when the server tells it the key. Otherwise, `hushrun doctor` prints them. - `origin` is the repository's URL on hushrun without its scheme, and `key` the log's Ed25519 public key. - hushrun reads them with the rest of `hushrun.yaml`, as committed, so changing them is a reviewed change like any other. ## What every command checks With the key pinned, every command that reads or writes the repository's events checks what the server answers: - the receipt of each write; - the signed statement of each listing; - the signers answer, which says who counts; - that every checkpoint it meets, the server's own and those recorded inside events, belongs to one history. A check that fails ends the command with exit code 6. `hushrun plan --start` records the listings its decision read in its `started` event, for monitors to check, and `hushrun mark deployed` refuses an attempt without that event, so every deploy has one. Until `hushrun.yaml` names the key, hushrun checks none of this, and warns on every command. ## Monitor the log A listing that leaves an event out could hide a failure from a plan. `hushrun repository monitor` finds it afterwards: ```bash hushrun repository monitor --state monitor.json ``` - It replays the repository's log, and checks it against the server's latest signed checkpoint, which must extend the one the last run kept in `--state`. - It checks every checkpoint and listing plans recorded, every deletion against an org admin's signed statement, and every map the server committed to. - It exits 5 when the server contradicts its own log, and prints the server's own signed statements as proof, and 0 when everything agrees. - It needs read access only, and `hushrun.yaml`'s log key. The monitor that counts is one the server's operator doesn't run. Run it from your own CI, every hour: ```yaml title=".github/workflows/hushrun-monitor.yml" name: Monitor the hushrun repository on: schedule: - cron: "17 * * * *" workflow_dispatch: concurrency: group: hushrun-monitor cancel-in-progress: false jobs: monitor: runs-on: ubuntu-latest # id-token: write lets hushrun ask for an ID token whose audience is the repository's URL. permissions: { contents: read, id-token: write } steps: - uses: actions/checkout@v4 # hushrun.yaml names the log key - uses: hushrun/hushrun/action/setup@main # pin to a release commit with: { version: 0.1.0 } # The newest checkpoint the last run verified: a server that rolls back between runs # contradicts it. - uses: actions/cache/restore@v4 with: path: hushrun-monitor.json key: hushrun-monitor-${{ github.run_id }} restore-keys: hushrun-monitor- - run: hushrun repository monitor --state hushrun-monitor.json --format json > monitor.json - if: always() run: jq '{ok, checkpoint: .checkpoint.size, checked, findings}' monitor.json - uses: actions/cache/save@v4 if: success() with: path: hushrun-monitor.json key: hushrun-monitor-${{ github.run_id }} - uses: actions/upload-artifact@v4 # the findings and their proofs, if any if: failure() with: { name: hushrun-monitor, path: monitor.json } ``` ## Who can read and write A repository's events must be append-only for CI: a pipeline that could delete or overwrite an event could remove a `failed` result and unblock a digest that never passed. Signatures can't prove an event is missing, so hushrun's server enforces it: | Who | Events | Delete | |---|---|---| | CI, with its ID token or a writer token | Append and read | Never | | A member or an agent, from a registered machine | Append what that machine signed, and read | Never | | Org admins and owners | Read | Only with `hushrun repository gc` | - A token has a role: `reader` reads, `writer` also appends events, and `admin` also deletes. hushrun creates each event only if it doesn't exist yet, and the roles keep a client that misbehaves from overwriting or deleting one. - A GitHub Actions job with `id-token: write` gets the writer role for the repository its ID token names. Elsewhere, CI uses an agent token ([GitLab CI](https://hushrun.com/docs/promotion/gitlab.md#variables)). - An org admin makes repository tokens, `hrl_…`, for monitors and scripts, on the repository's page in the dashboard. They go in `HUSHRUN_REPOSITORY_TOKEN`, or come from the command in `HUSHRUN_REPOSITORY_TOKEN_COMMAND`. - hushrun reads tokens only from the environment, never from the repository, and never logs them. `HUSHRUN_REPOSITORY_TOKEN` goes only to a URL that `HUSHRUN_REPOSITORY_URL` or `--repository-url` names. A URL only `hushrun.yaml` names gets a CI job's ID token; outside CI, it gets the login you made for its origin, or an agent token when it is on hushrun's own host. - A repository's URL is `https://`; `http://` works only on a loopback host. Old events go with `hushrun repository gc`, run by an org admin or owner from their signed-in machine, first with `--dry-run`: ```bash hushrun repository gc --older-than 180d --dry-run ``` It keeps the newest deployment of each scope and stage, and the events of its digest. On a repository that keeps a log, it signs each deletion with the admin's machine, so a deletion is never the server's own doing, and monitors report any deletion without such a signature. # Commands Source: https://hushrun.com/docs/reference/cli Every `hushrun` command, grouped by what it's for, with its usage, the flags that matter, their defaults and an example. `hushrun --help` prints every flag. In a repository that `npx hushrun init` set up, hushrun is in `devDependencies`, so `package.json` scripts call it as `hushrun`. Anywhere else, `npx hushrun ` runs it. ## Everyday ### hushrun run Runs a check's command unless this exact code already passed it, and records the result. [How skipping works](https://hushrun.com/docs/run.md) explains the key and every line it prints, and [Where results go](https://hushrun.com/docs/results.md) where it records. Usage: `hushrun run [NAME] -- COMMAND` | Argument or flag | Default | What it does | |---|---|---| | `NAME` | The `package.json` script that runs it, else the program's name | The check's name, part of its key | | `COMMAND` | Required | Everything after `--`, run as it is, argument by argument | | `--force` | Off | Runs the command even after a pass, and records the result | | `--scope NAME` | The only scope | The scope whose files make the key. Needed when `hushrun.yaml` has several | | `--rev REV` | The files as they are now | Hashes a commit's files instead | | `--digest sha256:…` | Computed | Uses this source digest instead of hashing | | `-q` | Off | Silences hushrun's own lines | - hushrun's lines go to stderr and start with `hushrun: `. Stdout is the command's. - It exits with the command's exit code, or 0 when it skips: see [its exit codes](#hushrun-run-without-a-stage). - `--all`, `--isolated`, `--timeout`, `--environment-image` and `--plan` need `--stage`, and exit 2 without it. With `--stage`, `run` runs a promotion stage's checks instead: see [hushrun run --stage](#hushrun-run-stage). ```console $ hushrun run unit -- sleep 2 hushrun: unit running: no pass yet for this exact code hushrun: unit passed in 2s; it won't run again on this code $ hushrun run unit -- sleep 2 hushrun: unit skipped: this exact code passed just now (saves 2s) ``` ### hushrun init Sets up `hushrun run` in the git repository it runs in. It finds the checks among `package.json`'s scripts and the GitHub Actions jobs that run them, signs you in, shows each change it would make, and makes them once you agree: - wraps each check as `hushrun run -- …`, keeping the rest of each file as it was; - names your organization's backend in `package.json`, and adds `hushrun` to `devDependencies` with the repository's package manager; - gives the GitHub Actions jobs that run those checks `id-token: write`, so that CI records results with no secret; - connects a repository on github.com, so that its CI records results with its own ID token; when an admin or owner of the organization runs it, also adds the repository to the organization for promoting its builds, and writes `hushrun.yaml` with its URL on hushrun; - runs a check twice, to show the second run skip. Usage: `hushrun init [OPTIONS]` | Flag | Default | What it does | |---|---|---| | `-y`, `--yes` | Off | Applies the changes without asking. Without a terminal, init needs it | | `--dry-run` | Off | Shows what init would change, and changes nothing: no sign-in, no files, no install | | `--no-browser` | Off | Prints the sign-in page's address instead of opening a browser | | `--no-run` | Off | Doesn't end by running a check twice | | `--run` | Off | With `--yes`, still ends by running a check twice | | `--pipeline` | Off | Writes a starter `hushrun.yaml` for a promotion pipeline instead | | `--force` | Off | With `--pipeline`: replaces an existing `hushrun.yaml` | | `--repository ID` | From the `origin` remote | With `--pipeline`: the repository id, such as `github.com/acme/app` | | `--staging-branch BRANCH` | `develop` | With `--pipeline`: the staging stage's branch | | `--production-branch BRANCH` | `main` | With `--pipeline`: the production stage's branch | | `--staging-image REPOSITORY` | `registry.example.com/staging/app` | With `--pipeline`: the staging image repository | | `--production-image REPOSITORY` | `registry.example.com/production/app` | With `--pipeline`: the production image repository | The last six flags imply `--pipeline`, and none of the pipeline flags go with `--yes`, `--dry-run`, `--no-browser`, `--run` or `--no-run`. init exits 0 when it's done, when there is nothing to do, and when you answer no; 2 for usage errors, such as running outside a git repository or without a terminal and without `--yes`; and 1 when a file can't be written or the install fails. ```console $ hushrun init --dry-run Looking at /Users/ana/shop repository github.com/acme/shop packages npm (no lockfile) checks lint, test, typecheck workflows .github/workflows/ci.yml runs test, lint (job test) hushrun init will change: package.json "lint": "eslint ." → "hushrun run -- eslint ." "test": "vitest" → "hushrun run -- vitest run" (vitest alone watches on a terminal) "typecheck": "tsc --noEmit" → "hushrun run -- tsc --noEmit" "hushrun": {"backend": ""}, where CI, teammates and agents share results devDependencies: "hushrun": "^0.1.0" .github/workflows/ci.yml job test: adds permissions contents: read and id-token: write. The job's token then has only these two: add any other it needs your org connects github.com/acme/shop, so that its CI records results with its own ID token adds github.com/acme/shop to your org, for promoting its builds, unless it is there already hushrun.yaml a new file: repository github.com/acme/shop, the URL the org gives it, and its log key Nothing changed (--dry-run). ``` ### hushrun login Signs in to a backend, so that `hushrun run` shares results with your organization. hushrun shows a code and opens the page that approves it, where you pick the organization. Once you approve, it saves a token only you can read, registers this machine, and makes the organization's URL the backend `hushrun run` uses when nothing else names one. Usage: `hushrun login [OPTIONS] [URL]` | Argument or flag | Default | What it does | |---|---|---| | `URL` | `HUSHRUN_BACKEND`, else the backend in the repository's `package.json`, else the last one you signed in to, else `https://api.hushrun.com` | Your organization's URL, such as `https://api.hushrun.com/o/acme`, or a server's address | | `--no-browser` | Off | Prints the approval page's address instead of opening a browser | URL must use `https://`, or `http://` on `localhost`, or hushrun exits 2. A server that can't be reached exits 6. `HUSHRUN_HOST` names another hosted server than `https://api.hushrun.com`. Signing in on a machine revoked for going 90 days unused, or by `hushrun logout`, brings it back. The server can refuse to register the machine, and then hushrun saves nothing and exits 1: when you already have 20 machines in the organization, its message links to your machines page, where you revoke one; when you or an admin revoked this machine in the dashboard, hushrun says to remove its key file, so that it makes a new key. ```bash npx hushrun login hushrun login https://hushrun.example/o/acme ``` ### hushrun logout Revokes the token `hushrun login` saved for a server, and forgets it, along with the saved backend when it's on that server. hushrun forgets the token even when the server doesn't answer. Usage: `hushrun logout [OPTIONS] [URL]` | Argument | Default | What it does | |---|---|---| | `URL` | The server `hushrun login` would sign in to | The backend, or its host, to sign out of | ```bash hushrun logout ``` ### hushrun whoami Shows where `hushrun run` records results here and what named that backend, the credential that goes there, and who the backend says you are: your organization, and for an agent token its agent, repository, expiry and rate limit. It also shows this machine's key id. Usage: `hushrun whoami [OPTIONS]` It exits 0 when the backend answered, or when results stay on this machine or are off; 1 when the backend refused the credential; and 6 when it couldn't be reached. `--format json` prints `hushrun.whoami/v1`. ```console $ hushrun whoami backend this machine only, in /Users/ana/Library/Caches/hushrun/results share sign in with hushrun login, or name a backend in package.json ``` ## Agent tokens An agent token, which starts with `hra_`, lets an agent in a sandbox, or CI outside GitHub, record and read results without a browser. Its results record as `agent:NAME`, for one repository or the whole organization, until it expires or someone revokes it. Each token comes with the agent's key, its one machine, which signs what it records. The sandbox gets them as `HUSHRUN_TOKEN` and `HUSHRUN_MACHINE_KEY`: see [Agents and sandboxes](https://hushrun.com/docs/agents.md#sandboxes). The `hushrun token` commands are for organization admins, and use their `hushrun login`, never `HUSHRUN_TOKEN`. Their backend is `--backend`, else `HUSHRUN_BACKEND`, else the backend in the repository's `package.json`, else the one `hushrun login` saved. It must be an organization's URL, such as `https://api.hushrun.com/o/acme`. They exit 2 without a login on that server or when the server answers 400; 1 when it answers 401, 403 or 404; and 6 when it can't be reached or fails. ### hushrun token create Creates an agent token and the agent's key, and shows both once. Usage: `hushrun token create [OPTIONS] --agent ` | Flag | Default | What it does | |---|---|---| | `--agent NAME` | Required | The agent's name, such as `claude-code`, `fleet-a` or `gitlab-ci`. Its results record as `agent:NAME` | | `--repository REPO` | The repository hushrun runs in | `host/owner/name` of the repository whose results it records and reads | | `--org-wide` | Off | For every repository of the organization instead | | `--expires DURATION` | `30d` | How long it lasts, in whole hours: `12h`, `30d`, `8w`; at most `365d` | | `--rate N` | The server's, 300 on hushrun's hosted service | The most results it records a minute, 1 to 10000 | | `--backend URL` | See above | The organization's backend | The token goes alone to stdout, so `$(hushrun token create …)` holds it. Stderr says what it is and which variables to give the sandbox: `HUSHRUN_TOKEN`, `HUSHRUN_PROVIDER=local`, and `HUSHRUN_BACKEND` when the backend isn't on hushrun's hosted service; then the agent's key, a P-256 key hushrun makes and registers with the token, to set as `HUSHRUN_MACHINE_KEY`, or to keep in a file that `HUSHRUN_MACHINE_KEY_FILE` names, as a GitLab File variable does. Every sandbox or pipeline of the agent signs as that one machine, and the server records the agent's results only with its signature. Outside a repository, it needs `--repository` or `--org-wide`. `--format json` prints `hushrun.token/v1`, with the token and the key. ```bash hushrun token create --agent fleet-a hushrun token create --agent gitlab-ci --org-wide --expires 365d ``` ### hushrun token list Lists the organization's agent tokens that aren't revoked, expired ones included: each one's prefix, agent, repository or whole organization, expiry, last use and creator. Usage: `hushrun token list [OPTIONS]` | Flag | Default | What it does | |---|---|---| | `--backend URL` | See above | The organization's backend | ### hushrun token revoke Revokes an agent token: anything that uses it is refused from then on. Usage: `hushrun token revoke [OPTIONS] ` | Argument or flag | Default | What it does | |---|---|---| | `TOKEN` | Required | The token's id, the prefix `hushrun token list` shows (`hra_` and 6 characters), the token itself, or its agent's name when only one token has it | | `--backend URL` | See above | The organization's backend | An agent name that more than one token has exits 2 and lists them. ```bash hushrun token revoke hra_AbC123 ``` ## Promotion These commands run a promotion pipeline: source digests, plans, signed evidence and artifact promotion by digest. Most need `hushrun.yaml` and the repository's URL on hushrun: see [Promotion](https://hushrun.com/docs/promotion.md) and [Configuration](https://hushrun.com/docs/reference/configuration.md). In a pull request's pipeline, the commands that read or write the repository's events need `--config-ref` naming the branch it merges into. Many of them take the same options for the content they work on: | Option | Default | What it does | |---|---|---| | `--scope NAME` | The only scope | A scope to work on. Repeatable where a command takes several | | `--all-scopes` | Off | Every scope | | `--rev REV` | `HEAD` | The revision | | `--worktree` | Off | The files as `git add -A && git commit` would record them now: what you are about to commit | | `--digest sha256:…` | Computed | Uses this source digest instead of computing it, for one scope only | ### hushrun digest Prints the source digest of each scope. It needs no repository URL, key or server, and without `hushrun.yaml` the one scope, `default`, covers every file. Usage: `hushrun digest [OPTIONS]` | Flag | Default | What it does | |---|---|---| | `--scope NAME`, `--all-scopes` | Every scope | The scopes to digest | | `--rev REV` | `HEAD` | The revision to digest | | `--worktree` | Off | Digests the files as `git add -A && git commit` would record them now | | `--dir PATH` | Off | Digests a plain directory, such as a clean export | | `--explain` | Off | Lists every entry, its kind, hash and path, and the pattern that decided its scope | | `--manifest-out FILE` | Off | Writes the exact manifest bytes, for one scope; `sha256sum` of the file is the digest | ```console $ hushrun digest sha256:79a343b472b042bd73eb52f6457750d062d7991490b2d4baacc896dac50c921d api sha256:83bc69ea5b9510c5aae5712d3d33a107f205e0e32435b6e657bdffa7a6407273 web ``` ### hushrun plan Decides what a stage does with the digest, and exits 3 when it's blocked: | Mode | What the pipeline does | |---|---| | `promote` | Deploys the artifact an earlier stage built and validated, copied by digest | | `refresh` | Runs only the checks whose evidence is missing, expired or failed, then promotes | | `full` | Builds, runs the checks that lack valid evidence, and deploys | | `wait` | Nothing yet: an upstream run for this digest is still going | | `skip` | Nothing: this stage already runs this digest | | `blocked` | Nothing, and the run fails: the upstream failed this digest, or evidence is missing that this stage can't produce | Usage: `hushrun plan [OPTIONS] --stage ` | Flag | Default | What it does | |---|---|---| | `--stage STAGE` | Required | The stage to plan | | `--scope NAME`, `--all-scopes` | The only scope | The scopes to plan | | `--rev REV` | `HEAD` | The revision | | `--worktree` | Off | Plans for the files as `git add -A && git commit` would record them now | | `--trigger TRIGGER` | From the CI provider, else `manual` | What started this run: `push`, `upstream`, `manual` or `schedule` | | `--attempt N` | From the CI provider, else one more than recorded | This run's attempt | | `--force-full` | Off, or `HUSHRUN_FORCE_FULL` | Builds and runs every check, whatever the evidence. A stage with `allow_force: false` is blocked instead | | `--start` | Off | Records `started` for every scope that deploys | | `--lease DURATION` | The stage's `lease` | The lease `--start` records | | `--wait-timeout DURATION` | None | While the decision is `wait`, asks again every 15 seconds for up to DURATION | | `--out FILE` | Off | Writes the plan as JSON, for later jobs' `--plan` | | `--explain-json FILE` | Off | Writes the planner's full input and output | ```bash hushrun plan --stage staging --start --out plan.json ``` ### hushrun guard Refuses to deploy a revision that is no longer the head of the stage's branch. When the branch moved on, it records `superseded`, then outputs `HUSHRUN_STALE=true` and exits 0, or exits 4 with `--on-stale fail`. When it can't resolve the branch's head, it exits 1 and records nothing. Usage: `hushrun guard [OPTIONS] --stage ` | Flag | Default | What it does | |---|---|---| | `--stage STAGE` | Required | The stage being deployed | | `--branch BRANCH` | The stage's `branch` | The branch whose head the revision must be | | `--remote REMOTE` | `origin` | The git remote whose branch head it reads | | `--on-stale ACTION` | `skip` | `skip`: record `superseded`, output `stale=true` and exit 0. `fail`: record `superseded` and exit 4 | It also takes the content options above. ```bash hushrun --plan plan.json guard --stage production ``` ### hushrun status Shows each stage's requirements and evidence for the current digest. Usage: `hushrun status [OPTIONS]` It takes `--scope`, `--all-scopes`, `--rev` and `--worktree` (see above). ```bash hushrun status --worktree ``` ### hushrun run --stage Runs a stage's checks and records their evidence with the repository. Without names it runs the plan's checks to run; without a plan, every source check the stage requires that lacks valid evidence. It exits 8 when a check fails, once its evidence is recorded. Usage: `hushrun run --stage [OPTIONS] [CHECK]... [-- ...]` | Argument or flag | Default | What it does | |---|---|---| | `--stage STAGE` | Required | The stage the evidence is for | | `CHECK` | The plan's checks to run; without a plan, every source check the stage requires that lacks valid evidence | The checks to run | | `COMMAND` | The check's `run` | After `--`: the command of a check that has no `run` in `hushrun.yaml` | | `--all` | Off | Every source check the stage requires, skipping those with valid evidence | | `--force` | Off | Runs checks whose evidence is valid too | | `--isolated` | Off | Runs each check in its pinned image and platform, with Docker or Podman, on an export of exactly the digested files, with no network unless the check declares one, and only its `env` variables | | `--timeout DURATION` | The check's `timeout`, else none | Each check's time limit | | `--environment-image REF@sha256:…` | None | The image this job runs in, pinned by digest. Checks that pin `image` need it | It also takes the content options above. ```bash hushrun run --all --stage staging --isolated --worktree ``` ### hushrun attest Records evidence other tools produced: check results, artifacts and reports. Usage: `hushrun attest [OPTIONS] --stage ` | Flag | Default | What it does | |---|---|---| | `--stage STAGE` | Required | The stage the evidence belongs to | | `--check NAME=RESULT` | None | A check's result, `pass` or `fail`. Repeatable | | `--artifact NAME=REF` | None | An artifact this run built, at its location for the stage. A tag is resolved to its digest. Repeatable | | `--report NAME=FILE` | None | A report the checks produced, recorded by digest. Repeatable | | `--duration DURATION` | `0s` | How long the checks ran | | `--attempts N` | `1` | The attempts the checks took | | `--environment-image REF@sha256:…` | None | The image the checks ran in, pinned by digest. Checks that pin `image` need it | It also takes the content options above. ```bash hushrun attest --stage staging --check unit=pass --duration 95s ``` ### hushrun mark Records a lifecycle event: `started`, `deployed`, `passed`, `failed` or `superseded`. Usage: `hushrun mark [OPTIONS] --stage ` | Argument or flag | Default | What it does | |---|---|---| | `KIND` | Required | `started`, `deployed`, `passed`, `failed` or `superseded` | | `--stage STAGE` | Required | The stage | | `--reason TEXT` | None | Why it failed, for `failed`; `stale` or `cancelled`, for `superseded` | | `--artifact NAME=REF` | None | An artifact deployed or passed, at its location for the stage. A tag is resolved to its digest. Repeatable | | `--target NAME[=DETAIL]` | None | Where it was deployed, for `deployed` | | `--by-revision REV` | None | The revision that took over, for `superseded` | It also takes the content options above. ```bash hushrun --plan plan.json mark deployed --stage production ``` ### hushrun approve Records your signed approval of an approval check, for a digest and a stage. It shows what you approve, and signs it, by default with this machine's key. Approvals are people's: in CI, or signing as an agent, it exits 2 and records nothing. Usage: `hushrun approve [OPTIONS] --stage ` | Argument or flag | Default | What it does | |---|---|---| | `CHECK` | Required | The approval check, as `hushrun.yaml` names it | | `--stage STAGE` | Required | The stage that requires the approval | | `--comment TEXT` | None | Why, recorded with the approval, up to 1000 characters | | `--out FILE` | Off | Writes the signed event to FILE instead of recording it, to upload elsewhere, such as the dashboard | It also takes the content options above. ```bash hushrun approve release --stage production --comment "QA signed off" ``` ### hushrun verify Verifies events and shows who signed them. It exits 5 when verification fails. Usage: `hushrun verify [OPTIONS] [FILES]...` | Argument or flag | Default | What it does | |---|---|---| | `FILES` | None | Event files to verify | | `--scope NAME --digest sha256:…` | None | Verifies every event the repository holds for this digest instead | | `--signers FILE` | Asks the repository's server | The signers `hushrun repository signers --out` saved, to verify files offline | ```bash hushrun verify --scope default --digest sha256:… ``` ### hushrun artifact resolve Outputs the reference to deploy for the plan's decision, and copies the artifact to this stage's location by digest when it lives elsewhere. When the copy fails, it falls back to a build, with `HUSHRUN_BUILD=true` and a warning that says why. With `--no-fallback`, or for a stage that requires deployment checks, it exits 7 instead. Usage: `hushrun artifact resolve [OPTIONS] --stage ` | Flag | Default | What it does | |---|---|---| | `--stage STAGE` | Required | The stage being deployed | | `--scope NAME` | The only scope | The scope | | `--worktree` | Off | Without a plan, decides for the files as `git add -A && git commit` would record them now | | `--tag TAG` | The revision | The tag written next to the digest | | `--no-fallback` | Off | Exits 7 when the copy fails, instead of falling back to a build | Registry credentials come from the Docker configuration, so `docker login` and `gcloud auth configure-docker` work as they are. ```bash hushrun --plan plan.json artifact resolve --stage production ``` ### hushrun artifact copy Copies an image or index to another repository by digest, and exits 7 when the transfer fails. Usage: `hushrun artifact copy [OPTIONS] ` | Argument | What it does | |---|---| | `SOURCE` | The source, pinned by digest or tagged; a tag is resolved first | | `DESTINATION` | The destination repository, `REPOSITORY` or `REPOSITORY:TAG` | ```bash hushrun artifact copy us-docker.pkg.dev/acme/staging/api@sha256:… us-docker.pkg.dev/acme/prod/api:v1 ``` ### hushrun artifact digest Prints the digest a reference points to. Usage: `hushrun artifact digest [OPTIONS] ` ```bash hushrun artifact digest us-docker.pkg.dev/acme/staging/api:v1 ``` ### hushrun policy validate Validates `hushrun.yaml`, and exits 2 at an error, which names the file, line and column. It reads the working tree's file and needs no server; in a pull request's pipeline, it reads the pull request's own file. Usage: `hushrun policy validate [OPTIONS]` | Flag | Default | What it does | |---|---|---| | `--rev REV` | The working tree | Validates `hushrun.yaml` at this revision instead | ```console $ hushrun policy validate hushrun.yaml is valid: 2 scope(s), 3 check(s), 1 artifact(s), 2 stage(s) ``` ### hushrun policy explain Shows a stage's requirements, whose events count for each, and whether hushrun's server makes the stage CI-only. Without a repository URL, or when the server doesn't answer within 15 seconds, it shows the rest and says that CI-only stages weren't checked. Usage: `hushrun policy explain [OPTIONS] --stage ` | Flag | Default | What it does | |---|---|---| | `--stage STAGE` | Required | The stage | | `--signers FILE` | Asks the repository's server | The signers `hushrun repository signers --out` saved, to tell offline whether the stage is CI-only | ```bash hushrun policy explain --stage production ``` ### hushrun adopt Writes a starter `hushrun.yaml` and GitHub Actions workflows that call hushrun's reusable deploy workflow, and prints the next steps. Usage: `hushrun adopt [OPTIONS]` | Flag | Default | What it does | |---|---|---| | `--repository ID` | The CI provider's, else the `origin` remote's | The repository id, such as `github.com/acme/app` | | `--stage NAME=BRANCH` | `staging=develop`, `production=main` | A stage and its branch, in delivery order. Repeatable | | `--image STAGE=REPOSITORY` | None | A stage's image repository. Repeatable | | `--repository-base URL` | `HUSHRUN_REPOSITORY_BASE` | The organization's base URL, under which each repository's URL is `/` | | `--workflow OWNER/REPO/PATH@REF` | This release's | The reusable workflow to call, pinned by commit | | `--hushrun-version VERSION` | This one's | The hushrun version the workflows install | | `--dry-run` | Off | Prints what it would write, and writes nothing | | `--force` | Off | Replaces files that differ from what adopt writes | ```bash hushrun adopt --dry-run ``` ## Server and setup ### hushrun repository ls Lists the repository's events on hushrun's server. Usage: `hushrun repository ls [OPTIONS]` | Flag | Default | What it does | |---|---|---| | `--scope NAME` | All | Events of this scope | | `--digest sha256:…` | All | Events of this digest; needs `--scope` | | `--stage STAGE` | All | Events of this stage | ### hushrun repository show Shows one event. Usage: `hushrun repository show [OPTIONS] ` ### hushrun repository signers Shows the signers whose events count for the repository, as its server names them, with the server's signed statement over them. Usage: `hushrun repository signers [OPTIONS]` | Flag | Default | What it does | |---|---|---| | `--out FILE` | Off | Saves the answer, for `hushrun verify --signers` and `hushrun policy explain --signers` | ```bash hushrun repository signers --out signers.json ``` ### hushrun repository gc Deletes events older than a duration, except the newest deployment of each scope and stage and the events of its digest. For organization admins. Usage: `hushrun repository gc [OPTIONS] --older-than ` | Flag | Default | What it does | |---|---|---| | `--older-than DURATION` | Required | Deletes what was written more than this long ago, such as `180d` | | `--dry-run` | Off | Lists what it would delete, and deletes nothing | | `--key FILE` | The command's signer | The key that signs each deletion, on a server that keeps a transparency log | ```bash hushrun repository gc --older-than 180d --dry-run ``` ### hushrun repository monitor Replays the repository's transparency log on hushrun's server, and checks every checkpoint, and every listing that plans recorded against it. It exits 5 when the server contradicts its own log, and needs `transparency_log` in `hushrun.yaml`. Usage: `hushrun repository monitor [OPTIONS]` | Flag | Default | What it does | |---|---|---| | `--state FILE` | Off | Remembers the newest checkpoint in FILE, and checks the next run against it | ```bash hushrun repository monitor --state monitor.json ``` ### hushrun repository usage Shows the organization's plan and the checks recorded this period, against what the plan includes and its limit, with this repository's share. It asks the backend `hushrun run` records to, as `hushrun whoami` shows it, with the same credential. Results kept on this machine aren't metered. It exits 0, or 6 when the server can't be reached, refuses the credential or answers something else. See [Plans and limits](https://hushrun.com/docs/plans.md). Usage: `hushrun repository usage [OPTIONS]` ```console $ hushrun repository usage backend this machine only, in /Users/ana/Library/Caches/hushrun/results usage results stay on this machine, so nothing is metered ``` ### hushrun repository stats Counts what hushrun recorded, to share volume without telemetry, and prints what you may choose to paste. It sends nothing else anywhere. Usage: `hushrun repository stats [OPTIONS]` | Flag | Default | What it does | |---|---|---| | `--since DURATION` | `30d` | Counts what was recorded in this long, such as `7d` or `12w` | | `--local` | Off | Counts this machine's `hushrun run` results, in hushrun's cache directory, instead of the repository's events on the server: by verdict, by check, distinct run keys and the time skips saved | Without `--local`, it reads one listing of the repository's events, and no event: events by kind, distinct digests, and check events by stage. ```console $ hushrun repository stats --local hushrun run's results on this machine in the last 30d (since 2026-09-27 16:51 UTC), in /Users/ana/Library/Caches/hushrun/results total 11 by verdict skip 5, pass 4, fail 2 run keys 5 by check unit 7, lint 4 saved by skips 4s ``` ### hushrun sigstore refresh Updates the Sigstore trust root, which verifies keyless signatures and timestamps, from its TUF repository. hushrun verifies what it gets, and caches it for offline verification. It exits 9 when the TUF repository can't be reached. Usage: `hushrun sigstore refresh [OPTIONS]` ### hushrun keys generate Generates a signing key, PKCS#8 PEM with mode 0600. It never overwrites a key file. Usage: `hushrun keys generate [OPTIONS]` | Flag | Default | What it does | |---|---|---| | `--out FILE` | Stdout | Writes the private key to FILE | | `--algorithm ALGORITHM` | `ed25519` | `ed25519`, or `ecdsa-p256`, which hardware keys use | ```console $ hushrun keys generate --out signing.pem public key: ed25519:e2iTpmZCZRojWi76uPqruO7B6y5vW26tItDTBpv6Mh0= key id: sha256:0c33bcfe81849a0ffe99200fb1e55533ed278adf56e2411e8420bc08cb8e1a97 private key: signing.pem (mode 0600) ``` ### hushrun keys public Prints the public key and key id of a private key. Usage: `hushrun keys public [OPTIONS]` | Flag | Default | What it does | |---|---|---| | `--key FILE` | The key this command would sign with: `HUSHRUN_SIGNING_KEY`, `HUSHRUN_SIGNING_KEY_FILE` or this machine's | The private key | ### hushrun doctor Checks the setup and says what to fix: git, the CI provider, where results go, `hushrun.yaml`, the repository's URL, server and signers, its transparency log, the clock, the signer, the timestamp authorities and the Sigstore trust root, registry access, the deploy workflow, configuration baked into the source, and the default branch, or on GitLab its API, protected branches and who may set pipeline variables. It exits 2 when any check fails; warnings don't change the exit code. `--format json` prints `hushrun.doctor/v1`. Usage: `hushrun doctor [OPTIONS]` ```bash hushrun doctor --repository-url https://api.hushrun.com/l/acme/shop ``` ## Global options Every command takes these, before or after its name. A flag wins over its variable. | Option | Variable | Default | What it does | |---|---|---|---| | `-C DIR`, `--repo DIR` | None | The current directory | Runs as if hushrun started in DIR | | `--config PATH` | `HUSHRUN_CONFIG` | `hushrun.yaml` in the content being evaluated | The path of `hushrun.yaml` | | `--config-ref REF` | None | None | Reads `hushrun.yaml` from this git ref, such as `origin/main`. A pull request's pipeline must name the branch it merges into. Doesn't go with `--config` | | `--repository-url URL` | `HUSHRUN_REPOSITORY_URL` | `hushrun.yaml`'s `repository_url` | The repository's URL on hushrun. In CI only `HUSHRUN_REPOSITORY_URL` counts, and a different `--repository-url` exits 2 | | `--plan FILE` | `HUSHRUN_PLAN_JSON`, which holds the plan itself | None | The plan this command carries out: a file, or `-` for stdin | | `--format FORMAT` | `HUSHRUN_FORMAT` | `text` | The output format on stdout: `text`, `json`, `env` or `dotenv` | | `--dotenv FILE` | None | None | Also writes the outputs to FILE in dotenv form, for GitLab | | `--no-ci-output` | `HUSHRUN_NO_CI_OUTPUT` | Off | Writes no CI outputs, step summaries or annotations | | `-v`, `--verbose` | `HUSHRUN_LOG` | Warnings | More logging on stderr: `-v` info, `-vv` debug, `-vvv` trace | | `-q`, `--quiet` | None | Off | Errors only on stderr. In `hushrun run` without a stage, none of hushrun's lines | | `--color WHEN` | `NO_COLOR` | `auto` | Colored logs: `auto`, `always` or `never`. `auto` colors them when stderr is a terminal and `NO_COLOR` isn't set | | `-j N`, `--jobs N` | None | The available cores | Threads that hash files | | `-h`, `--help` | None | None | Prints help. `--help` says more than `-h` | | `-V`, `--version` | None | None | Prints the version | [Environment variables](https://hushrun.com/docs/reference/environment.md) has every variable, and [Configuration](https://hushrun.com/docs/reference/configuration.md#sources) the order in which hushrun reads them. ## Output formats `--format` decides what a command prints on stdout. Logs and messages always go to stderr. | Format | What stdout gets | |---|---| | `text` | Lines for people. The default | | `json` | One JSON object on one line, whose `schema` field names its shape, such as `hushrun.digest/v1` | | `env` | `HUSHRUN_=value` lines, quoted for a POSIX shell's `eval` | | `dotenv` | The same lines unquoted, as GitLab's dotenv reports read them. hushrun refuses values of several lines, more than 20 variables, or 5 KiB or more, and leaves out `HUSHRUN_JSON` | - `--dotenv FILE` writes the dotenv form to FILE as well, whatever `--format` is, for `digest`, `plan`, `guard`, `attest`, `mark` and `artifact`. - A command without outputs prints text for `env` and `dotenv`. - On GitHub Actions, `plan`, `digest`, `guard`, `attest` and `artifact` also append their outputs to `$GITHUB_OUTPUT` under their lowercase names, such as `mode` and `digest`, write a step summary, and annotate the run, unless `--no-ci-output` is given. - Keep the output in a variable before you `eval` it: `out=$(hushrun guard --stage production --format env) && eval "$out"`. `eval "$(hushrun …)"` succeeds even when hushrun fails. The outputs, by command, with `` and `` uppercased and `-` written as `_`: | Command | Outputs | |---|---| | `hushrun digest` | `HUSHRUN_DIGEST`, or `HUSHRUN_DIGEST_` for each of several scopes; `HUSHRUN_REVISION` | | `hushrun plan`, one scope | `HUSHRUN_MODE`, `HUSHRUN_REASON`, `HUSHRUN_DIGEST`, `HUSHRUN_REVISION`, `HUSHRUN_ATTEMPT`, `HUSHRUN_BUILD`, `HUSHRUN_REF`, `HUSHRUN_ARTIFACTS` (JSON), `HUSHRUN_CHECKS` and `HUSHRUN_DEPLOYMENT_CHECKS` (names separated by spaces), `HUSHRUN_JSON` (the whole plan) | | `hushrun plan`, several scopes | `HUSHRUN_REVISION`, `HUSHRUN_ATTEMPT`, `HUSHRUN_ACTIONABLE_SCOPES` (JSON), `HUSHRUN_JSON` | | `hushrun guard` | `HUSHRUN_STALE` (`true` or `false`), `HUSHRUN_HEAD` | | `hushrun attest` | `HUSHRUN_DIGEST`, `HUSHRUN_REVISION`, `HUSHRUN_REF` for a single artifact, `HUSHRUN_REF_` | | `hushrun mark` | `HUSHRUN_DIGEST`, `HUSHRUN_ATTEMPT` | | `hushrun artifact resolve` | `HUSHRUN_BUILD`, `HUSHRUN_REF` and `HUSHRUN_DIGEST` for a single artifact, `HUSHRUN_REF_` | | `hushrun artifact copy`, `hushrun artifact digest` | `HUSHRUN_DIGEST`, `HUSHRUN_REF` | | `hushrun keys generate`, `hushrun keys public` | `HUSHRUN_PUBLIC_KEY`, `HUSHRUN_KEYID` | ```console $ hushrun digest --format env HUSHRUN_DIGEST_API=sha256:79a343b472b042bd73eb52f6457750d062d7991490b2d4baacc896dac50c921d HUSHRUN_DIGEST_WEB=sha256:83bc69ea5b9510c5aae5712d3d33a107f205e0e32435b6e657bdffa7a6407273 HUSHRUN_REVISION=c3b57fb1add5a62e13a97fa1324e083067bc48c1 $ hushrun digest --scope api --format json {"canonicalization":"hushrun-source-v1","schema":"hushrun.digest/v1","scopes":[{"bytes":13,"digest":"sha256:79a343b472b042bd73eb52f6457750d062d7991490b2d4baacc896dac50c921d","files":1,"scope":"api"}],"source":{"objectFormat":"sha1","ref":"refs/heads/main","revision":"c3b57fb1add5a62e13a97fa1324e083067bc48c1","tree":"bc00258b51f36ddfe425f303ce7239b69fb1b29d","vcs":"git"},"stats":{"cacheHits":1,"elapsedMs":1,"entries":3,"hashed":0,"hashedBytes":0}} ``` ## Exit codes ### Every command | Code | Meaning | Where | |---|---|---| | 0 | Success | Every command. `plan`: any decision but `blocked`. `guard`: a stale revision with `--on-stale skip` | | 1 | An unexpected error | Every command. `whoami`: the backend refused the credential. `token`: the server answered 401, 403 or 404. `init`: a file couldn't be written, or the install failed. `guard`: the branch's head can't be resolved | | 2 | A usage or configuration error | Every command: a flag or variable hushrun can't take, a `hushrun.yaml` that doesn't validate, a missing repository URL, or one that doesn't match in CI. `doctor`: a check failed. `init`: no terminal and no `--yes`. `approve`: run in CI | | 3 | Blocked | `plan` | | 4 | Stale | `guard --on-stale fail`: the revision is no longer the head of the stage's branch | | 5 | Verification failed | `verify`. `repository monitor`: the server contradicts its own log | | 6 | The server is unavailable or refused access | Commands that read or write the repository's events, and a write refused for the organization's plan, other than a check result. `whoami`, `login`, `token` and `repository usage` when the server can't be reached | | 7 | The transfer failed | `artifact copy`. `artifact resolve` with `--no-fallback`, or for a stage that requires deployment checks | | 8 | A check failed, and its evidence was recorded | `run --stage` | | 9 | The signer is unavailable or refused | Commands that sign: the CI's ID token, Fulcio, or every timestamp authority. `sigstore refresh`: the TUF repository | ### hushrun run without a stage `hushrun run [NAME] -- COMMAND` ends as the command does, so a script or CI job reads the check's own result: | Code | When | |---|---| | The command's | It ran. A code outside 0 to 255 becomes 1 | | 0 | It skipped: this exact code already passed | | 2 | A usage error: no command after `--`, more than one NAME, no NAME when the program gives none, or a flag that needs `--stage`. The command doesn't run | | 126 | The command was found but can't start, such as a file without execute permission | | 127 | The command wasn't found | | 128 + N | The command was stopped by signal N, such as 143 after `SIGTERM`. Nothing is recorded | Results being off, a backend that can't be reached, one that refuses the credential, or a variable hushrun can't read never change the exit code: the command runs, and its code is hushrun's. # Environment variables Source: https://hushrun.com/docs/reference/environment Every variable hushrun reads: the `HUSHRUN_` ones you set, and the ones CI providers and other tools set. A command-line flag wins over its variable, as [Configuration](https://hushrun.com/docs/reference/configuration.md#sources) explains. - An empty value counts as unset, except in two variables, where it turns something off: `HUSHRUN_BACKEND=` turns results off, and `HUSHRUN_CACHE_DIR=` the cache. - `HUSHRUN_CONFIG`, `HUSHRUN_FORMAT`, `HUSHRUN_NO_CI_OUTPUT`, `HUSHRUN_FORCE_FULL`, `HUSHRUN_GIT_READER` and `HUSHRUN_LOG` stand in for flags, and never stop a command. The true-or-false ones also take `1`, `0`, `yes`, `no`, `on` and `off`, in any case. A value hushrun can't read is ignored, with one line on stderr, such as `hushrun: HUSHRUN_FORMAT is "xml", not text, json, env or dotenv, so it's ignored`, and the command goes on with the default. The same goes for an unknown `HUSHRUN_PROVIDER`, and for a `HUSHRUN_RUN_ATTEMPT` or `GITHUB_RUN_ATTEMPT` that isn't a positive whole number. `-q` drops these lines. A bad flag on the command line is still a usage error. - Variables such as `HUSHRUN_MODE` and `HUSHRUN_DIGEST` are outputs hushrun prints, not settings: see [Output formats](https://hushrun.com/docs/reference/cli.md#output-formats). ## Results and sign-in What `hushrun run` uses to find its backend, and the token it sends there. [Where results go](https://hushrun.com/docs/results.md) has the rules in full. | Variable | Default | What it does | Secret | |---|---|---|---| | `HUSHRUN_BACKEND` | Unset | The organization's URL where `hushrun run` records and looks up results, such as `https://api.hushrun.com/o/acme`. It ranks first, before the backend in `package.json` and the one `hushrun login` saved. Set to nothing, it turns results off: every check runs, and nothing is recorded. A URL that isn't `https://`, or `http://` on `localhost`, `127.0.0.1` or `[::1]`, turns them off too, with a message. `hushrun login` signs in to it, and `hushrun token` manages its tokens, when no URL is given | No | | `HUSHRUN_TOKEN` | Unset | A token for the results backend. It goes only to the backend `HUSHRUN_BACKEND` names. An agent token, `hra_…`, also goes to the repository's or the login's backend when it is on `HUSHRUN_HOST`, and to the repository's URL on that server, in CI too. An agent token's results need the key it came with, in `HUSHRUN_MACHINE_KEY` or `HUSHRUN_MACHINE_KEY_FILE`. The `hushrun token` commands never use it | Yes | | `HUSHRUN_HOST` | `https://api.hushrun.com` | hushrun's hosted server: where `hushrun login` signs in when nothing names a backend, and `npx hushrun init` when `package.json` names none. It is the only server an agent token follows `package.json`'s backend, the login's, or `hushrun.yaml`'s repository URL to | No | ## Agents and sandboxes For coding agents in sandboxes, and for checks that `hushrun run --isolated` runs in containers. See [Agents and sandboxes](https://hushrun.com/docs/agents.md#sandboxes). | Variable | Default | What it does | Secret | |---|---|---|---| | `HUSHRUN_PROVIDER` | Detected, see [CI detection](#ci-detection) | The CI provider running hushrun, instead of detecting it: `github`, `gitlab`, `buildkite`, `circleci`, `generic` or `local`. Set `local` in a sandbox that sets `CI=true`, so that hushrun doesn't treat it as CI. Another value is ignored, with a line saying so, and hushrun detects the provider | No | | `HUSHRUN_INVOKED_BY` | Unset | A label for who ran the command, `agent:` or `human:`, such as `agent:worker-1`, to tell a fleet's runs apart. Run results and signed events record it; nothing decides on it. In `hushrun run` without a stage, another form is left out with a line saying so; commands that sign events exit 2 | No | | `HUSHRUN_MACHINE_KEY` | Unset | This machine's key itself, as PEM, for sandboxes that keep no files. It wins over the key file. An agent's is the key its token came with, which `hushrun token create` and the dashboard's Agents page show once: with an agent token, hushrun signs results only with this or `HUSHRUN_MACHINE_KEY_FILE`, never a key a sign-in left on the machine, and without either it records nothing. In CI, hushrun signs events with a machine key only when this or `HUSHRUN_MACHINE_KEY_FILE` is set | Yes | | `HUSHRUN_CONTAINER_RUNTIME` | `docker`, else `podman`, found on `PATH` | The container runtime `hushrun run --isolated` uses: `docker`, `podman`, or the path of a Docker-compatible CLI | No | | `HUSHRUN_RESTRICTED_NETWORK` | Unset | The Docker or Podman network, whose egress you restrict, that a check declaring `network: restricted` runs on. Without it, such a check can't run isolated, and the command exits 2 | No | ## CI Run identity on CI other than GitHub Actions and GitLab CI, which set their own (see [Set by CI providers](#set-by-ci-providers)), and a token for GitLab's API. | Variable | Default | What it does | Secret | |---|---|---|---| | `HUSHRUN_RUN_ID` | Unset | On CI other than GitHub Actions and GitLab CI, the run's id, which signed events record. There, a command that records events exits 2 without it, unless it carries out a plan, whose id it takes | No | | `HUSHRUN_RUN_ATTEMPT` | Unset | On GitLab CI and other CI, this run's attempt, a positive whole number. Without it, commands take the plan's attempt, else 1, and `hushrun plan` counts one more than recorded. Another value is ignored, with a line saying so. GitHub Actions' attempt comes from `GITHUB_RUN_ATTEMPT`, read the same way | No | | `HUSHRUN_RUN_URL` | Unset | On CI other than GitHub Actions and GitLab CI, a link to the run, which run results and events record | No | | `HUSHRUN_GITLAB_TOKEN` | Unset | A GitLab project access token with `read_api`. With it, `hushrun plan` asks GitLab whether an upstream pipeline is still running, and `hushrun doctor` checks protected branches and who may set pipeline variables | Yes | ## Signing How commands that record promotion events sign them, and what verifies signatures. All of it comes from the environment, never from the repository's files. See [Evidence from agents](https://hushrun.com/docs/promotion/evidence.md). | Variable | Default | What it does | Secret | |---|---|---|---| | `HUSHRUN_SIGNING` | A set signing key means `key`; else, in GitHub Actions with an ID token, `sigstore`; else `machine`, when there is a machine key, which in CI only `HUSHRUN_MACHINE_KEY` or `HUSHRUN_MACHINE_KEY_FILE` gives; else, in CI with `SIGSTORE_ID_TOKEN`, `sigstore` | How events are signed: `key`, `sigstore` (keyless, through Sigstore) or `machine` (this machine's key, which `hushrun login` registered, or `HUSHRUN_MACHINE_KEY`). Another value exits 2, and so does a command that signs with no signer | No | | `HUSHRUN_SIGNING_KEY` | Unset | A signing key's PEM, Ed25519 or ECDSA P-256, for `key`. It wins over `HUSHRUN_SIGNING_KEY_FILE` | Yes | | `HUSHRUN_SIGNING_KEY_FILE` | Unset | The path of a signing key's PEM file, for `key`. An isolated run refuses a key file inside the files it exports | No; the file is a private key | | `HUSHRUN_CUSTODY` | `ci` in CI; `isolated` for checks `hushrun run --isolated` runs; else `local` | The custody signed events claim: `local`, `isolated`, `sandbox` or `ci`. `isolated` is recorded only for `--isolated` runs; otherwise hushrun records `local`, with a warning. Another value exits 2 | No | | `HUSHRUN_TSA` | `https://timestamp.sigstore.dev/api/v1/timestamp` | The timestamp authorities for each signature, URLs separated by commas, tried in order. `none` turns timestamps off, which keyless signing refuses | No | | `HUSHRUN_FULCIO_URL` | `https://fulcio.sigstore.dev` | The Fulcio that certifies keyless signatures | No | | `HUSHRUN_OIDC_ISSUER` | `https://oauth2.sigstore.dev/auth` | Outside CI, for keyless signing as yourself: where you sign in with your browser. Without a terminal, hushrun signs in only to an issuer this variable names | No | | `HUSHRUN_OIDC_CLIENT_ID` | `sigstore` | The client id for that sign-in | No | | `HUSHRUN_SIGSTORE_TRUSTED_ROOT` | The trust root `hushrun sigstore refresh` cached, else the one built into hushrun | The path of a Sigstore `trusted_root.json` that verifies certificates and timestamps | No | | `HUSHRUN_SIGSTORE_TUF_URL` | `https://tuf-repo-cdn.sigstore.dev` | Another Sigstore TUF repository for the trust root. Set it with `HUSHRUN_SIGSTORE_TUF_ROOT`, or neither; with both, hushrun needs a cached trust root from `hushrun sigstore refresh` | No | | `HUSHRUN_SIGSTORE_TUF_ROOT` | The root built into hushrun | The path of the TUF root that repository starts from | No | ## Promotion and the repository's URL Where the promotion commands read and write the repository's events, with which token, and from which plan and `hushrun.yaml`. [Configuration](https://hushrun.com/docs/reference/configuration.md#sources) has the order, which differs in CI. | Variable | Default | What it does | Secret | |---|---|---|---| | `HUSHRUN_REPOSITORY_URL` | Unset | The repository's URL on hushrun, such as `https://api.hushrun.com/l/acme/shop`. In CI it is the URL that counts: when `hushrun.yaml`'s `repository_url` names another, or `--repository-url` does, the command exits 2 | No | | `HUSHRUN_REPOSITORY_BASE` | Unset | One URL for an organization: each repository's URL is `/`, such as `https://api.hushrun.com/l/acme/github.com/acme/shop`. In CI, the id comes from the CI provider, and `hushrun.yaml`'s `repository` and `repository_url` must agree with it, or the command exits 2. `HUSHRUN_REPOSITORY_URL` wins when both are set. It is also `hushrun adopt`'s default `--repository-base` | No | | `HUSHRUN_REPOSITORY_TOKEN` | Unset | A token for the repository's server. It goes only to a URL that `HUSHRUN_REPOSITORY_URL` or `--repository-url` names, or in CI one `HUSHRUN_REPOSITORY_BASE` gives, never to one only `hushrun.yaml` names | Yes | | `HUSHRUN_REPOSITORY_TOKEN_COMMAND` | Unset | A command that prints that token and nothing else, run with `sh -c` (`cmd /C` on Windows) and with `HUSHRUN_REPOSITORY_URL` set to the server's URL. hushrun reuses what it prints for 5 minutes. Its token goes where `HUSHRUN_REPOSITORY_TOKEN` goes, which wins when both are set | No; it prints one | | `HUSHRUN_CONFIG` | `hushrun.yaml` in the content being evaluated | The path of `hushrun.yaml`, as `--config` | No | | `HUSHRUN_PLAN_JSON` | Unset | The plan itself, as JSON, for the commands that carry one out, `run --stage`, `attest`, `mark`, `guard` and `artifact resolve`, when `--plan` isn't given | No | | `HUSHRUN_FORCE_FULL` | `false` | `true` makes `hushrun plan` build and run every check whatever the evidence, as `--force-full` | No | Without a token from these, the repository's server gets, in GitHub Actions, the job's ID token; else an agent token in `HUSHRUN_TOKEN`, when the server is `HUSHRUN_HOST`, in CI too; else, outside CI, the token `hushrun login` saved for that server. ## Paths and caches `` is `~/.config` on Linux, `~/Library/Application Support` on macOS and `%APPDATA%` on Windows; `` is `~/.cache`, `~/Library/Caches` and `%LOCALAPPDATA%`. [Configuration](https://hushrun.com/docs/reference/configuration.md#files) lists what each file holds. | Variable | Default | What it does | Secret | |---|---|---|---| | `HUSHRUN_CREDENTIALS` | `/hushrun/credentials.json` | The file `hushrun login` keeps its tokens in, readable by you only | No; the file holds tokens | | `HUSHRUN_MACHINE_KEY_FILE` | `machine-key.pem` beside the credentials file | This machine's key file. hushrun refuses one inside the repository it runs in. With an agent token, and in CI, set it to the agent's key file, such as a GitLab File variable, for hushrun to sign with it | No; the file is a private key | | `HUSHRUN_CACHE_DIR` | `/hushrun` | hushrun's cache: `hushrun run`'s results when no backend applies, file hashes, the tokens this machine registered with, the Sigstore trust root `hushrun sigstore refresh` keeps, and isolated runs' exports. Set to nothing, it turns the cache off, and with it the results kept on this machine | No | | `HUSHRUN_EXPORT_DIR` | `exports` in hushrun's cache directory, else the system's temporary directory | Where `hushrun run --isolated` exports the files a check runs on. Point it at a case-sensitive directory when two paths differ only in case | No | ## The npm package | Variable | Default | What it does | Secret | |---|---|---|---| | `HUSHRUN_NPM_PACKAGE` | `hushrun@^` | The package `hushrun init` adds to `devDependencies`. The npm package's `hushrun` command sets it to its own name and version unless it is set | No | | `HUSHRUN_BINARY` | Unset | Read by the npm package's `hushrun` command: a hushrun binary to run where the package has none for this platform | No | ## Output | Variable | Default | What it does | Secret | |---|---|---|---| | `HUSHRUN_FORMAT` | `text` | The output format, as `--format`: `text`, `json`, `env` or `dotenv` | No | | `HUSHRUN_NO_CI_OUTPUT` | `false` | `true` writes no CI outputs, step summaries or annotations, as `--no-ci-output` | No | | `HUSHRUN_LOG` | `warn` | The log filter on stderr, such as `info` or `debug`, when neither `-v` nor `-q` is given. A value hushrun can't read is ignored, with a line saying so. `-q` leaves errors only | No | | `HUSHRUN_GIT_READER` | `auto` | How hushrun reads git objects: `auto`, `gix` or `cli`. Meant for hushrun's own tests | No | ## Set by CI providers ### CI detection hushrun takes the first of these that is `true` or `1` as the CI it runs in, unless `HUSHRUN_PROVIDER` names one. In CI, the promotion commands take the repository's URL by CI's rules; sign keylessly by default in GitHub Actions with an ID token, and elsewhere with the agent's machine key the environment gives; claim custody `ci`; and send no person's login to the repository's server, only an agent token, to `HUSHRUN_HOST`. `hushrun approve` refuses to run; and `hushrun run` records which CI a result came from, so that a later skip says `passed in CI`. | Variable | What hushrun uses it for | |---|---| | `GITHUB_ACTIONS` | GitHub Actions | | `GITLAB_CI` | GitLab CI | | `BUILDKITE` | Buildkite, identified as generic CI: runs need `HUSHRUN_RUN_ID` | | `CIRCLECI` | CircleCI, identified as generic CI: runs need `HUSHRUN_RUN_ID` | | `CI` | Any other CI, generic: runs need `HUSHRUN_RUN_ID` | ### GitHub Actions | Variable | What hushrun uses it for | |---|---| | `ACTIONS_ID_TOKEN_REQUEST_URL`, `ACTIONS_ID_TOKEN_REQUEST_TOKEN` | Set when the job has `permissions: id-token: write`. hushrun asks for ID tokens with them: for the results backend, for the repository's server, and for keyless signing, each with its own audience | | `GITHUB_RUN_ID`, `GITHUB_RUN_ATTEMPT` | The run's identity, which events record, and the run's link in run results | | `GITHUB_REPOSITORY` | The repository, `owner/name`: the id `hushrun run` records results under, the one `HUSHRUN_REPOSITORY_BASE` uses, and the default of `hushrun token create` and `hushrun adopt` | | `GITHUB_SERVER_URL` | GitHub's address, `https://github.com` by default, for the repository's id and the run's link | | `GITHUB_REPOSITORY_ID` | The repository's numeric id. When the server names another id as the repository's CI, hushrun exits 2 | | `GITHUB_EVENT_NAME` | What started the run: `push`; `workflow_run` as upstream; `workflow_dispatch` and `repository_dispatch` as manual; `schedule`. `pull_request` and `pull_request_target` make it a pull request's pipeline | | `GITHUB_EVENT_PATH` | The event's payload: the upstream run of a `workflow_run`, and the repository's default branch | | `GITHUB_BASE_REF` | The branch a pull request merges into, which `--config-ref` must name | | `GITHUB_REF` | The branch, from `refs/heads/…` | | `GITHUB_SHA` | The revision the run is for | | `GITHUB_JOB` | The job, which events record | | `GITHUB_OUTPUT`, `GITHUB_STEP_SUMMARY` | Where `plan`, `digest`, `guard`, `attest` and `artifact` write their outputs and a step summary | | `GITHUB_TOKEN`, `GH_TOKEN` | When set, a token with `actions: read`: `hushrun plan` asks GitHub's API whether an upstream run is still going, and `hushrun doctor` checks the default branch | | `GITHUB_API_URL` | GitHub's API, `https://api.github.com` by default | ### GitLab CI | Variable | What hushrun uses it for | |---|---| | `SIGSTORE_ID_TOKEN` | The ID token for keyless signing: on GitLab, the job's `id_tokens` entry `SIGSTORE_ID_TOKEN` with `aud: sigstore`. On other CI, any ID token Fulcio takes | | `CI_PIPELINE_ID`, `CI_PIPELINE_URL` | The run's identity and link | | `CI_PIPELINE_SOURCE` | What started the run: `push`; `pipeline`, `parent_pipeline` and `trigger` as upstream; `web` and `api` as manual; `schedule`. `merge_request_event` makes it a merge request's pipeline | | `CI_MERGE_REQUEST_TARGET_BRANCH_NAME` | The branch a merge request merges into, which `--config-ref` must name | | `CI_COMMIT_BRANCH` | The branch | | `CI_COMMIT_SHA` | The revision the run is for | | `CI_JOB_NAME`, `CI_JOB_ID` | The job, which events record | | `CI_PROJECT_URL` | The repository: the id `hushrun run` records results under, and the one `HUSHRUN_REPOSITORY_BASE` uses | | `CI_PROJECT_ID` | The project's numeric id. When the server names another id as the repository's CI, hushrun exits 2. With `HUSHRUN_GITLAB_TOKEN`, the project hushrun asks GitLab's API about | | `CI_PROJECT_PATH` | The project for GitLab's API, when `CI_PROJECT_ID` isn't set | | `CI_DEFAULT_BRANCH` | The repository's default branch | | `CI_API_V4_URL` | GitLab's API, `https://gitlab.com/api/v4` by default | ### Package managers | Variable | What hushrun uses it for | |---|---| | `npm_lifecycle_event` | The script npm, pnpm, Yarn and Bun are running. `hushrun run` without NAME names the check after it, unless it is `npx`, which `npx` and `npm exec` set, or isn't a valid name | ## Other variables | Variable | What hushrun uses it for | |---|---| | `NO_COLOR` | Set, it turns off colored logs under `--color auto` | | `BROWSER` | The command that opens the sign-in page for keyless signing as yourself, at a terminal; without it, the system's opener | | `DOCKER_CONFIG` | The directory whose `config.json` holds registry credentials, `~/.docker` by default: its `credHelpers`, `credsStore` and `auths` | | `PATH`, `PATHEXT` | Where `hushrun run` finds the command. On Windows, a bare name is looked up with `PATHEXT`'s `.com`, `.exe`, `.bat` and `.cmd`, so `vitest` finds `node_modules\.bin\vitest.cmd` | | `XDG_CONFIG_HOME`, `XDG_CACHE_HOME` | On Linux, the configuration and cache directories, `~/.config` and `~/.cache` by default | # Configuration Source: https://hushrun.com/docs/reference/configuration Where hushrun reads its configuration: `package.json`'s `hushrun` key, `hushrun.yaml`, the files it keeps on each machine, and the environment. `hushrun run` needs none of it to skip checks. - **`package.json`** names the backend where `hushrun run` shares results. - **`hushrun.yaml`** configures promotion: the repository's URL on hushrun, its scopes, checks, artifacts and stages, and the log key readers pin. `hushrun run` reads only its scopes, `digest.exclude` and `repository`. - **Environment variables and flags** come first, with one exception in CI: see [Where configuration comes from](#sources), [Environment variables](https://hushrun.com/docs/reference/environment.md) and the [global options](https://hushrun.com/docs/reference/cli.md#global-options). - **Files hushrun keeps** on each machine hold your sign-in, this machine's key and the cache. ## package.json ```json title="package.json" { "scripts": { "lint": "hushrun run -- eslint .", "test": "hushrun run -- vitest run" }, "devDependencies": { "hushrun": "^0.1.0" }, "hushrun": { "backend": "https://api.hushrun.com/o/acme" } } ``` | Key | What it is | |---|---| | `hushrun.backend` | The organization's URL where `hushrun run` records and looks up results. hushrun reads it from the `package.json` at the repository's root, the git top level, and reads nothing else under `hushrun` | - **Who writes it:** `npx hushrun init`, with your organization's URL, when `package.json` names no backend. Committed with the code, it tells CI, your teammates and their agents where results go, with no variable to set. - **How it ranks:** `hushrun run` takes the first of `HUSHRUN_BACKEND`, this key, the backend `hushrun login` saved, and this machine's cache directory. Without a URL, `hushrun login` signs in to it unless `HUSHRUN_BACKEND` names another, and the `hushrun token` commands manage its tokens unless `--backend` or `HUSHRUN_BACKEND` does. - **What it must be:** an `https://` URL, or `http://` on `localhost`, `127.0.0.1` or `[::1]`. Anything else turns results off, with a line that names the file. A `package.json` that doesn't parse names no backend. - **Which credential goes there:** the token `hushrun login` saved for that server, and in GitHub Actions the job's ID token. A token in `HUSHRUN_TOKEN` doesn't follow it, so that a change to the file can't send a CI secret elsewhere, except an agent token, `hra_…`, when the backend is on hushrun's hosted server. See [Which credential goes where](https://hushrun.com/docs/results.md#credentials). A backend that isn't `https://` turns results off, and the check runs: ```text hushrun: lint running without skipping: results are off (package.json's hushrun.backend: "http://example.com/o/acme" is not a repository URL: http:// is allowed only on a loopback host (localhost, 127.0.0.1, [::1]); use https://) ``` ## hushrun.yaml `hushrun.yaml` lives at the repository's root. hushrun reads the one in the content it evaluates: the commit by default, `HEAD` or `--rev`, and the working tree with `--worktree`. `hushrun run` without a stage and `hushrun policy validate` read the working tree's. `--config` (or `HUSHRUN_CONFIG`) names another file, and `--config-ref` reads it from another git ref, as a pull request's pipeline must ([Pull requests](#pull-requests)). `npx hushrun init` writes a first one, with `version`, `repository`, `repository_url` and `transparency_log`. `hushrun init --pipeline` and `hushrun adopt` write starters with stages. `hushrun policy validate` checks it. ### A complete example Every key hushrun accepts, with example values. The keys and digests written `…` come from elsewhere: `npx hushrun init` writes `transparency_log`'s, and the image's digest is the one your CI runs. ```yaml title="hushrun.yaml" # hushrun.yaml, at the repository's root. Unknown keys are errors. version: 1 # required, and 1 repository: github.com/acme/shop # required: host/owner/name repository_url: https://api.hushrun.com/l/acme/shop # the repository's URL on hushrun transparency_log: # the server's log key, which readers pin origin: api.hushrun.com/l/acme/shop # repository_url without its scheme key: "ed25519:…" # npx hushrun init writes it scopes: # without it, one scope, default, covers every file api: include: ["services/api/**", "go.mod", "go.sum", ".github/workflows/deploy-*.yml"] exclude: ["**/*_test.go"] web: include: ["web/**", ".github/workflows/deploy-*.yml"] digest: exclude: ["**/*.md"] # left out of every scope checks: unit: kind: source # source, deployment or approval run: go test ./... # what hushrun run --stage runs shell: bash # sh, bash, pwsh or cmd env: [GOFLAGS] # variables the check depends on image: golang@sha256:… # the image it runs in, pinned by digest platform: linux/amd64 network: none # none, restricted or open, for --isolated timeout: 20m retries: 1 max_age: 7d # older evidence has expired version: "2" # change it to invalidate earlier evidence e2e: kind: deployment # required as e2e@ run: ./scripts/e2e.sh release: kind: approval # a person approves it: no run artifacts: api-image: type: oci # oci or file scope: api # default: default locations: # one for every stage staging: us-docker.pkg.dev/acme/staging/api production: us-docker.pkg.dev/acme/prod/api stages: staging: branch: develop require: [unit] ci: github: { workflow: deploy-staging.yml } # lets production see staging's queued runs # gitlab: { ref: develop } # on GitLab: the ref whose pipelines are its runs production: branch: main after: staging require: [unit, e2e@staging, release] upstream: passed # default: not-failed upstream_grace: 30s lease: 3h dedupe: [push, upstream] allow_force: true on_repository_error: fail # fail or full ``` ### Top level | Key | Required | What it is | |---|---|---| | `version` | Yes | `1` | | `repository` | Yes | The repository's canonical id, `host/owner/name`: a lowercase host, no scheme, no `.git`, such as `github.com/acme/shop` | | `repository_url` | No | The repository's URL on hushrun, where its events go, such as `https://api.hushrun.com/l/acme/shop`: `https://`, or `http://` on `localhost`. In CI, `HUSHRUN_REPOSITORY_URL` must agree with it: see [In CI](#in-ci) | | `transparency_log` | No | The server's log key: see [`transparency_log`](#transparency_log) | | `scopes` | No | Named sets of files, each with its own digest. Without it, one scope, `default`, covers every file | | `digest` | No | `exclude`: patterns left out of every scope | | `checks` | No | What can be checked, by name | | `artifacts` | No | What stages deploy, by name | | `stages` | No | Where it deploys, and what each stage requires | A `hushrun.yaml` without stages is valid: it names the repository, its URL and the log key, and `hushrun plan --stage` exits 2 until the stage exists. ### `scopes` and `digest` | Key | Default | What it is | |---|---|---| | `scopes..include` | `["**"]` | Patterns of the files in the scope. It can't be empty | | `scopes..exclude` | `[]` | Patterns left out of the scope | | `digest.exclude` | `[]` | Patterns left out of every scope | A file belongs to a scope when it matches one of the scope's `include` patterns, none of its `exclude` patterns, and no `digest.exclude` pattern. A scope must cover everything that decides its artifact and its deployment: build files, the Dockerfile, the deploy workflow. A scope that matches no file fails when hushrun digests it. Once there are several scopes, `hushrun run` needs `--scope`: see [Several projects in one repository](https://hushrun.com/docs/run.md#monorepos). Patterns match the whole path, `/`-separated and case-sensitively, the same on every OS: | Pattern | Matches | |---|---| | `**` as a whole segment | Any number of whole segments. Last in a pattern, one or more: `a/**` matches `a/b` and `a/b/c`, not a file named `a` | | `*` | Any run of characters within one segment, possibly none | | `?` | One character within one segment | | `[abc]`, `[a-z]` | One character of the class, never `/` | | `[!abc]` | One character not in the class, never `/` | | Anything else | Itself, `\` included, which never escapes | `services/api/**` matches `services/api/main.go`, not `services/apix/a.go`; `**/*.md` matches `README.md` and `docs/a/b.md`; `*.go` matches `main.go`, not `cmd/main.go`. Braces, `**` with other characters in its segment, empty segments, and a class that starts with `^` are errors. ### `checks` | Key | Default | What it is | |---|---|---| | `kind` | Required | `source`: depends only on the files, and its evidence counts at every stage. `deployment`: belongs to one stage's environment, and is required as `@`. `approval`: a person's sign-off, which `hushrun approve` records | | `run` | None | The command `hushrun run --stage` runs. Without it, another tool attests the check, or `hushrun run --stage -- ` gives the command. An approval check has none | | `shell` | `sh`, or `pwsh` on Windows outside an isolated run | What runs `run`: `sh`, `bash`, `pwsh` or `cmd` | | `env` | `[]` | Names of the variables the check depends on. Their values are never recorded. An isolated run passes in only these, and a check that names a secret, such as `NPM_TOKEN`, doesn't run isolated | | `image` | None | The image the check runs in, pinned by digest, `…@sha256:…`. Evidence counts only from the same image, and `--isolated` needs it | | `platform` | None | The platform, such as `linux/amd64`. Evidence counts only from the same one | | `network` | `none` | The network of an isolated run: `none`; `restricted`, the network `HUSHRUN_RESTRICTED_NETWORK` names; or `open`, the container runtime's default | | `timeout` | None | How long `hushrun run --stage` lets the check run | | `retries` | `0` | How many times `hushrun run --stage` runs a failed check again | | `max_age` | None | How long evidence stays valid, such as `7d`. Older evidence has expired | | `version` | None | Free text. Change it to invalidate the check's evidence | A change to `kind`, `run`, `shell`, `image`, `platform`, `env`, `network` or `version` gives the check a new definition, and its earlier evidence stops counting. `timeout`, `retries` and `max_age` aren't part of it. ### `artifacts` | Key | Default | What it is | |---|---|---| | `type` | Required | `oci`, an image or index in a registry, or `file` | | `scope` | `default` | The scope the artifact is built from | | `locations.` | Required for every stage | Where the stage's copy lives. For `oci`, a repository with its registry host and no tag or digest, such as `us-docker.pkg.dev/acme/prod/api` | Every stage deploys every artifact of its scope, so each artifact needs a location for every stage. ### `stages` | Key | Default | What it is | |---|---|---| | `branch` | None | The only branch the stage deploys from, a branch name such as `main`, never `refs/…`. A plan for a run on another branch is blocked, and `hushrun guard` checks the branch's head | | `after` | None | The upstream stage. Cycles are errors | | `require` | `[]` | `` for a source or approval check, and `@` for a deployment check of an upstream stage | | `upstream` | `not-failed` | `not-failed`: the upstream mustn't have failed this digest. `passed`: it must have passed it | | `upstream_grace` | `30s` | How long a plan that a push started waits for an upstream run to appear before deciding there is none | | `lease` | `3h` | The lease `hushrun plan --start` records, and the longest any lease at this stage can be | | `dedupe` | `[push, upstream]` | The triggers, of `push`, `upstream`, `manual` and `schedule`, whose first attempt skips a digest the stage already deployed | | `allow_force` | `true` | Whether `hushrun plan --force-full` may force this stage. With `false`, such a plan is blocked | | `on_repository_error` | `fail` | When a plan can't read the repository's events: `fail` exits 6, `full` decides `full`. `full` isn't allowed on a stage with `after`, a deployment check or an approval check | | `ci.github.workflow` | None | The workflow file of the stage's runs, such as `deploy-staging.yml`, so that a later stage sees them queued before they record anything | | `ci.gitlab.ref` | The stage's `branch` | The branch or tag whose pipelines are the stage's runs, when they aren't its `branch`'s | Whether a stage counts only CI's evidence isn't set in `hushrun.yaml`, where a pull request could remove it: an organization admin sets it on hushrun's server, and `hushrun policy explain --stage ` shows it. See [Promotion](https://hushrun.com/docs/promotion.md). ### `transparency_log` | Key | Default | What it is | |---|---|---| | `origin` | Required | The name the log's checkpoints carry: the repository's URL without its scheme, such as `api.hushrun.com/l/acme/shop`. Printable ASCII without spaces or `+` | | `key` | Required | The public half of the server's log key, `ed25519:` | - With the key, hushrun checks every receipt, listing and checkpoint the server gives against it. Without it, hushrun checks nothing and warns, and `hushrun repository monitor` refuses to run. - `npx hushrun init` writes `origin` and `key` when the server tells it, and `hushrun doctor` prints the lines otherwise. See [Transparency log](https://hushrun.com/docs/promotion/transparency.md). ### Names and durations - Scopes, checks, artifacts and stages are named with a lowercase letter, then lowercase letters, digits, `_` and `-`, 63 characters at most. Two names of one kind can't become the same once uppercased with `-` as `_`, such as `unit-test` and `unit_test`, since outputs such as `HUSHRUN_DIGEST_` use that form. - A duration is numbers with units, largest first: `ms`, `s`, `m`, `h`, `d` and `w`, such as `30s`, `20m`, `3h`, `7d` or `1h30m`. ### Validation Besides the rules in the tables above, hushrun refuses a `hushrun.yaml`: - with a key it doesn't know, at any level, or a key written twice; - that refers to a check, stage or scope it doesn't define; - with a boolean other than `true` or `false`, such as `yes`; - larger than 1 MiB. Each error names the file, line and column, and the command exits 2: ```console $ hushrun policy validate error: hushrun.yaml:13:5: unknown field `accept`, expected one of branch, after, require, upstream, upstream_grace, lease, dedupe, allow_force, on_repository_error, ci ``` ## Where hushrun keeps its files | Directory | Linux | macOS | Windows | |---|---|---|---| | `` | `~/.config` | `~/Library/Application Support` | `%APPDATA%` | | `` | `~/.cache` | `~/Library/Caches` | `%LOCALAPPDATA%` | On Linux, `XDG_CONFIG_HOME` and `XDG_CACHE_HOME` replace `~/.config` and `~/.cache`. | File | Where | Moved by | What it holds | |---|---|---|---| | The credentials file | `/hushrun/credentials.json` | `HUSHRUN_CREDENTIALS`, the file's path | For each server you signed in to, your token, your email and this machine's key id; and the backend `hushrun run` uses when nothing else names one. Readable by you only, mode 0600 on Unix | | This machine's key | `machine-key.pem`, beside the credentials file | `HUSHRUN_MACHINE_KEY_FILE`, the file's path; or `HUSHRUN_MACHINE_KEY`, the key itself | An ECDSA P-256 key, made the first time the machine signs in or records with a personal or agent token, mode 0600. hushrun refuses one inside the repository it runs in | | The cache | `/hushrun/` | `HUSHRUN_CACHE_DIR`; set to nothing, there is none | `results/`, `hushrun run`'s results when no backend applies; file hashes; the tokens this machine registered with; the Sigstore trust root `hushrun sigstore refresh` keeps; and `exports/`, for isolated runs | `HUSHRUN_CREDENTIALS` moves the machine key with it, unless `HUSHRUN_MACHINE_KEY_FILE` is set. The credentials file looks like this: ```json title="credentials.json" { "backend": "https://api.hushrun.com/o/acme", "hosts": { "https://api.hushrun.com": { "token": "…", "email": "ana@acme.dev", "machine": "sha256:…" } } } ``` ## Where configuration comes from A flag wins over its environment variable, a variable over `hushrun.yaml`, and `hushrun.yaml` over the defaults. In CI, the repository's URL is the exception. Two things never come from the repository's files: - **Who counts as a signer.** hushrun's server answers it, signed with the log key `hushrun.yaml` pins, so the server can't change it unseen. Without a pinned key, hushrun takes the answer unchecked, and warns. - **How hushrun signs**, from `HUSHRUN_SIGNING` and the variables beside it: see [Signing](https://hushrun.com/docs/reference/environment.md#signing). ### Where results go `hushrun run` takes the first of `HUSHRUN_BACKEND`, `package.json`'s `hushrun.backend`, the backend `hushrun login` saved, and this machine's cache directory, in CI as anywhere else. What differs is the credential: see [Where results go](https://hushrun.com/docs/results.md). ### Locally Outside CI, the repository's URL is the first of: 1. `--repository-url`; 2. `HUSHRUN_REPOSITORY_URL`; 3. `HUSHRUN_REPOSITORY_BASE` with `hushrun.yaml`'s `repository`: `/`; 4. `hushrun.yaml`'s `repository_url`. `hushrun.yaml` is the evaluated revision's, as committed. Without any of these, the command exits 2. When only the working tree's `hushrun.yaml` names a `repository_url`, as right after `npx hushrun init`, the message says to commit it or pass `--config hushrun.yaml`. `HUSHRUN_REPOSITORY_TOKEN` and `HUSHRUN_REPOSITORY_TOKEN_COMMAND` go only to a URL that `--repository-url` or `HUSHRUN_REPOSITORY_URL` names. A URL on the server you signed in to gets the token `hushrun login` saved there, and one on hushrun's hosted server gets an agent token in `HUSHRUN_TOKEN`, even when only `hushrun.yaml` names it. ### In CI In CI, any provider hushrun detects (see [CI detection](https://hushrun.com/docs/reference/environment.md#ci-detection)), the commands that read or write the repository's events take its URL from: 1. `HUSHRUN_REPOSITORY_URL`, set in CI settings that live outside the repository, such as GitHub repository variables or GitLab project variables; 2. else `HUSHRUN_REPOSITORY_BASE`: `/`, with the id from the CI provider, not from the files. `hushrun.yaml`'s `repository` must be exactly that id, and its `repository_url` that URL; 3. else `hushrun.yaml`'s `repository_url`, in the `hushrun.yaml` being evaluated. - Only `HUSHRUN_REPOSITORY_URL` counts: a `--repository-url` that differs exits 2. - When `HUSHRUN_REPOSITORY_URL` differs from `hushrun.yaml`'s `repository_url`, compared with the scheme and host in lowercase and no trailing `/`, the command exits 2. So does a CI where none of the three is set. - When both `HUSHRUN_REPOSITORY_URL` and `HUSHRUN_REPOSITORY_BASE` are set, the URL wins, with a warning when the base gives another. - `HUSHRUN_REPOSITORY_TOKEN` and `HUSHRUN_REPOSITORY_TOKEN_COMMAND` go only to a URL that `HUSHRUN_REPOSITORY_URL` names or `HUSHRUN_REPOSITORY_BASE` gives. A URL only `hushrun.yaml` names gets the GitHub Actions job's ID token, whose audience names that URL; else, on hushrun's hosted server, an agent token in `HUSHRUN_TOKEN`, which that server takes for its org only. That is how a GitLab pipeline writes ([GitLab CI](https://hushrun.com/docs/promotion/gitlab.md#signing)). A person's login never goes to the repository's server in CI. - When the provider reports the repository's numeric id, a server whose answer names another repository's CI makes the command exit 2. - On GitLab, pipeline variables override project variables, so limit who may set them; `hushrun doctor` reports the setting when its token can read it. - Commands that never touch the server, such as `hushrun digest`, `hushrun keys` and `hushrun policy validate`, need no URL. ### Pull requests `hushrun.yaml` is read from the revision under evaluation, because it is reviewed with the code it governs. A pull request's own `hushrun.yaml` hasn't been reviewed yet, so in a pull request's pipeline, GitHub's `pull_request` and `pull_request_target` events and GitLab's merge request pipelines, the commands that read or write the repository's events exit 2 unless `--config-ref` names the branch it merges into, as `main` or `origin/main`: ```bash git fetch --depth=1 origin "$GITHUB_BASE_REF" hushrun plan --stage pr --config-ref "origin/$GITHUB_BASE_REF" ``` `--config-ref` reads `hushrun.yaml` as that ref has it, for the scopes, checks and stages alike. A ref without the file exits 2. Commands that touch no server, such as `hushrun policy validate`, read the pull request's own file. # Troubleshooting Source: https://hushrun.com/docs/troubleshooting Start with `hushrun whoami`: it shows where results go here, what named that backend, which credential goes with it, and what the backend says about it. Add `-v` to any command for more of hushrun's reasoning on stderr. ## A check runs every time hushrun skips only when it finds a pass for the same key. When a check keeps running: - **The files differ.** Something changes the repository on every run: a generated file, a lockfile your install rewrites, or the check's own output. Commit or ignore it. If the check writes files, hushrun says `files changed while it ran, so it wasn't recorded`: list them in `.gitignore`. - **The command differs.** Extra arguments, another directory or another name make another key. `npm test -- --coverage` is not `npm test`. - **Nobody has recorded a pass yet.** The first run of each check on new code always runs, and so does the first run after a failure. - **Results go nowhere shared.** `hushrun whoami` says `this machine only`, or a line says `sign in to share results`: sign in with `npx hushrun login`. - **Results are off.** `HUSHRUN_BACKEND` is set to nothing, or `HUSHRUN_CACHE_DIR` is. - **`hushrun.yaml` has several scopes.** hushrun says `can't tell which code this is`: add `--scope NAME` to the check's command. - **CI ignores laptops' passes.** In CI, a line ending `skipped only on CI's passes` means the repository has [CI reuses only CI's results](https://hushrun.com/docs/ci.md#ci-only) turned on: CI skips only checks CI itself passed. - **Your organization's plan is at its limit.** A line ending `; upgrade: …` means new results aren't recorded until the period ends or an admin upgrades. See [Plans and limits](https://hushrun.com/docs/plans.md). ## A check skipped when it shouldn't have A skip means the same check, with the same command, passed on exactly these files. If a check's result also depends on something outside the files, such as an environment variable, a service, the Node.js version or the operating system, give each variant its own name, such as `unit-node22`. To run a check once more and record the new result, use `--force`: ```bash npx hushrun run --force test -- vitest run ``` ## Sign-in and credentials | Line | What to do | |---|---| | `hushrun: sign in to share results with acme: npx hushrun login` | Sign in once on this machine | | `hushrun: results aren't shared with acme: …` | Nothing on this run could sign in: no login, no `HUSHRUN_TOKEN`, and in GitHub Actions no `id-token: write`. In a script or a sandbox, set `HUSHRUN_TOKEN` ([Agents and sandboxes](https://hushrun.com/docs/agents.md#sandboxes)); in GitHub Actions, add `id-token: write` to the job ([In CI](https://hushrun.com/docs/ci.md)) | | `hushrun: test running: acme refused your login: …` | Your sign-in no longer works there, for example because you left the organization. Sign in again with `hushrun login` | | `hushrun: test running: acme refused HUSHRUN_TOKEN: … expired on …` | Create a new agent token under the same name, and update the variable | | `hushrun: test running: acme refused this job's ID token: …` | The backend doesn't accept this repository's CI: check that the repository is connected to the organization, and that it wasn't deleted and created again under the same name | | `hushrun: HUSHRUN_TOKEN isn't sent to …` | The token goes only to the backend `HUSHRUN_BACKEND` names, and an agent token to hushrun's hosted service. Set `HUSHRUN_BACKEND` to the backend's URL | | `not signed in: …`, with a link to your machines page | You have 20 machines in that organization, the most it takes. Revoke one you no longer use on that page, `https://app.hushrun.com/account/machines`, then run `hushrun login` again | | `not signed in: …`, saying this machine was revoked | You or an admin revoked this machine in the dashboard, and a revoked machine doesn't come back. Remove the key file the message names, then run `hushrun login`: hushrun makes a new key. Sign in again in your other organizations too, since they knew the old key | A pull request from a fork gets no ID token from GitHub, so its checks run and record nothing. That is expected. ## Results that aren't recorded These lines end a run that passed or failed as usual, with its exit code kept: | Line | What to do | |---|---| | `test passed in 41s; not recorded: …; upgrade: https://app.hushrun.com/acme/-/billing` | The organization recorded every check its plan includes this period. An admin upgrades at that page, or the next period starts over | | `test passed in 41s; not recorded: agent token … recorded 300 results in the last minute, its limit` | Slow the fleet down, or create its token with a higher `--rate` | | `test passed in 41s; not recorded: agent token … records results for github.com/acme/shop only, and this result is for …` | Use that repository's token, or one made with `--org-wide` | | ``test passed in 41s; not recorded: this machine isn't registered: run `hushrun login` `` | Run `hushrun login` on this machine. A result from your sign-in carries this machine's signature, and the server takes none without it: the machine has no key yet, its registration was revoked, or it went 90 days unused, which signing in again undoes | | ``test passed in 41s; not recorded: set `HUSHRUN_MACHINE_KEY` to the key `hushrun token create --agent` printed`` | The job or sandbox has an agent token but not the key it came with. Set `HUSHRUN_MACHINE_KEY` to the key shown with the token, or `HUSHRUN_MACHINE_KEY_FILE` to a file that holds it. If nobody kept it, create a new token ([Agents and sandboxes](https://hushrun.com/docs/agents.md#sandboxes)) | | `test passed in 41s, but it couldn't be recorded (…)` | The backend couldn't be reached. The next run on this code runs again | | `test passed in 12s, but files changed while it ran, so it wasn't recorded. …` | List what the check writes in `.gitignore` | ## The backend can't be reached ```text hushrun: test running: can't read earlier results (https://api.hushrun.com/o/acme: v0/results/…: error sending request: …) ``` hushrun gives a run's requests 10 seconds in all, then runs the check, and still tries to record the result. Nothing fails because a backend is down. `hushrun whoami` exits 6 when it can't reach the backend. ## The command doesn't start ```text hushrun: can't start vitest: No such file or directory (os error 2) ``` hushrun exits 127, as a shell does, when the command isn't found, and 126 when it can't start it. Run wrapped scripts through your package manager (`npm test`), which puts `node_modules/.bin` on the `PATH`, or call the command by its path. ## A variable is ignored ```text hushrun: HUSHRUN_NO_CI_OUTPUT is "maybe", not true or false, so it's ignored ``` hushrun couldn't read the variable, so the command goes on as if it weren't set, and exits as it would have. It usually means a CI setting holds a typo, or a value meant for something else. Fix the value or remove the variable; `-q` hides the line. What each variable takes is in [Environment variables](https://hushrun.com/docs/reference/environment.md). ## init | Message | What to do | |---|---| | `hushrun init sets up a git repository: run it inside one` | Run it inside a git repository, at its root | | `init can't ask without a terminal: run hushrun init --yes to apply` | In a script or as an agent, add `--yes` | | `… the sign-in wasn't approved for an organization …` | Create or join an organization in the dashboard, then run init again | | `… isn't a member of acme.` | The repository already records to an organization you aren't in. init offers to ask its admins to let you in; once one approves, run init again | | `… is invited to acme: accept the invitation …` | Accept the invitation in the dashboard, then run init again | | `This repository already uses hushrun: …` | Nothing to set up; init still makes sure the organization has the repository | init changes files only after you agree, or with `--yes`, and only the lines it listed. `git diff` shows them before you commit. ## Still stuck `hushrun doctor` checks the whole setup: git, CI, where results go, and the credential. Its output, with `hushrun whoami` and the run's lines, says almost everything about why hushrun did what it did.