# How skipping works

`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).
