# Evidence from agents and laptops

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:<digest of the image CI uses>
    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 <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:<name>`, 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 |
