cryptographically verifiable CI

Stop paying CI to re-run tests you already ran.

Every push re-runs the whole suite, even the tests a developer or an AI agent just ran on the same code. vci proves which tests passed, so CI skips them: up to 90% less CI time per pull request, with a signed record for every skip and never a false one.

  • up to 90%less CI time when the suite was run locally first
  • 0known false skips after adversarial testing of every adapter
  • 4 to 15 sto verify and plan a 2,078-file Rails suite on a GitHub runner, measured over 266 CI jobs
  • 6test runners: Vitest, pytest, Go, Cargo, Rails, RSpec
A laptop and an agent in a sandbox send a signed test result through git to a CI server, which checks it and accepts it

See it in motion

A change made in an agent's sandbox, attested, pushed through git, and skipped by CI. Press play, or step through with the arrows.

What it saves

Only attested tests that still match are skipped; everything else runs as before, plus the plan step: 15 seconds median on a 2,078-file Rails suite, 4 seconds on a quiet job.

CI time for one pull request, full suite versus vci, in five cases

The chart above is a model. The measured numbers: on vex's 2,078-file Rails suite, from 266 CI jobs over 23 to 25 October 2026, a job where little changed took 2.9 minutes and a job where everything re-ran and was attested 5.8 minutes (9.3 two days earlier); see Measured on vex's Rails tests. They were first set against a plain job of 3.5 minutes, which could not be reproduced: read again from GitHub's jobs API, plain jobs took 4.5 to 5.3 minutes, and on 25 October every case with vci took longer than that (benchmarks). For this repository's own demo projects, where a commit with nothing to run costs vci about two seconds, see Measured on this repository; for a 253-file node:test suite that goes from 17 seconds to 8, see Measured on vex-web. Every measurement, with its source and date, is on the benchmarks page.

How it works

An attestation is a signed record of one test run: what ran, a hash of every input it used, the result, and who ran it. vci makes it on your machine and checks it in CI.

A document under a magnifying glass, with fingerprints linked to its lines 1 · record

Run and record locally

vci run runs your tests with a collector switched on. The collector notes every file, directory listing, missing-file probe, package version and env var each test used.

An envelope closed with a wax seal, next to a key 2 · sign

Sign and push through git

vci signs that record with your SSH key. It travels as git-meta metadata on refs/meta/main: vci push and vci fetch are git push and fetch, with no server or database. Since 2.8.0 it can live in a SQLite file, PostgreSQL or Turso instead (choosing a store).

A checklist with every item ticked, next to a shield with a check mark 3 · verify

CI re-checks everything

vci plan checks the signature against the signers on the base branch (the branch the pull request merges into). It re-hashes every recorded input from its own checkout and skips only on an exact match.

The default verdict is always RUN: any error or doubt means the test runs. Here is the whole loop in a terminal:

$ vci run src/b.test.ts ran, collected inputs, signed, stored $ vci push # later, in CI $ vci plan --base-ref origin/main SKIP src/b.test.ts (attested by alice@example.com) RUN src/a.test.ts (no attestation) RUN src/c.test.ts (inputs changed) 1 to skip, 2 to run

Works with your test runner

A unit is the smallest piece of a test suite vci can skip. Each runner has its own, and one repository can mix several runners.

Six test units in a row, all leading to a single check mark

Logos are trademarks of their respective projects and are used only to identify them.

What you can rely on

Fail open

A missing, expired, tampered or unreadable attestation only ever makes a test run, because nothing in the store is trusted on its own.

Trust comes from the base branch

CI reads the allowed signers and the policy from the base commit, so a pull request cannot add its own key or loosen the rules.

Observed inputs, not guesses

Collectors record what each test really read at run time, including files it looked for and did not find.

Laptop to runner

An attestation made on macOS arm64 can be accepted on a Linux x86_64 runner when the toolchain and inputs match.

In CI, every unit goes through these checks, and a single "no" means it runs:

The five checks a unit must pass to be skipped: signed by a key in main's allowed_signers, not expired, same project, runner and toolchain, every recorded input re-hashes to the same root, and the hashed env vars (set, hash, built-in) hash the same. Any no, or any error, means RUN.

Know the limit. An attestation proves who made a claim, not that the test ran. A trusted signer can lie and a stolen key can sign. Keep the signer list short and keep full runs on your default branch. See the threat model.

Try it in five minutes

# install (or, without Homebrew: see Get started)
brew install pandelisz/tap/vci

# in your repository
vci init --adapter pytest --key ~/.ssh/id_ed25519.pub
git add vci.toml .vci/allowed_signers .git-meta
git commit -m "Enable vci"
# merge this commit to main: CI trusts only the base branch

vci run tests/test_b.py --key ~/.ssh/id_ed25519
vci plan --base-ref origin/main
vci push