Get started
Install vci, enable it in one repository, sign your first test run and see the plan skip it.
vci lets CI skip a test that a trusted person or agent already ran against exactly the same inputs. You will:
- install the
vcibinary; - set up your repository, once;
- run and sign tests as you work;
- let CI skip what is already signed (GitHub Actions).
Key ideas
These four words come up throughout the docs:
| Term | Meaning |
|---|---|
| unit | The smallest piece of your test suite vci can skip, such as one test file or one Go package. |
| attestation | A signed record of one unit's run: what ran, a hash of every input it used, the result, and who signed it. |
| collector | The plugin vci loads into your test runner to record what each unit reads while it runs. |
| base branch | The branch a pull request merges into, usually main. CI takes its trusted signers and policy from there, never from the pull request. |
What counts as a unit depends on the test runner:
Requirements
| Tool | Needed for |
|---|---|
git | everything |
ssh-keygen (OpenSSH 8.1+) and an Ed25519 SSH key | signing attestations |
Rust toolchain (cargo, 1.85+) | building the vci binary (not needed with Homebrew) |
Node 22.15+ and Vitest >=3.2 <6 | Vitest projects |
uv and pytest 9 | pytest projects |
| Go (tested with 1.26.2) | Go projects |
cargo/rustc installed through rustup | Rust projects |
Ruby (the exact version in .ruby-version), Bundler, Rails 8 with Minitest or RSpec | Rails projects |
macOS and Linux are supported.
Install
With Homebrew (macOS and Linux). This installs a prebuilt binary with the collectors included, so there is nothing else to set up:
brew install pandelisz/tap/vci
Upgrade later with vci update (it runs brew update and brew upgrade pandelisz/tap/vci), or brew upgrade vci.
Without Homebrew
The install script clones the repository to ~/.local/share/vci and builds it with cargo, so it needs a Rust toolchain:
curl -fsSL \
https://raw.githubusercontent.com/PandelisZ/cryptographically-verifiable-ci-runner/main/scripts/install.sh \
| sh
To pin a version, set VCI_REF to a tag or commit before running the script. Or build by hand:
git clone \
https://github.com/PandelisZ/cryptographically-verifiable-ci-runner.git \
~/.local/share/vci
cargo install --locked --path ~/.local/share/vci/crates/vci-cli
Then tell vci where its collectors live. Add this to your shell profile:
export VCI_PY_PLUGIN="$HOME/.local/share/vci/py/pytest-plugin"
export VCI_JS_PLUGIN="$HOME/.local/share/vci/js/vitest-plugin"
export VCI_RUBY_COLLECTOR="$HOME/.local/share/vci/ruby/vci-collector"
Check the install
vci --version
Keep it current
vci update # install the latest release the way vci was installed
vci update --check # only compare: exit 0 when current, 1 when there is an update
vci update uses Homebrew for a Homebrew install and re-runs the install script at the release's tag for the script's install. A release binary downloaded by hand is replaced in place after its SHA-256 is checked; collectors installed beside it are not updated.
Set up a repository
Do this once, in the repository you want to speed up. Pick your test runner in the first step; the other tabs on these pages follow your choice.
-
Create the config and trust file
vci init --adapter vitest --key ~/.ssh/id_ed25519.pub --no-installvci init --adapter pytest --key ~/.ssh/id_ed25519.pubvci init --adapter go --key ~/.ssh/id_ed25519.pubvci init --adapter cargo --key ~/.ssh/id_ed25519.pubvci init --adapter rails --key ~/.ssh/id_ed25519.pub# RSpec runs under the Rails adapter; the runner is detected vci init --adapter rails --key ~/.ssh/id_ed25519.pubThis writes three files:
File What it is for vci.tomlPolicy and environment settings (reference). .vci/allowed_signersWho may sign attestations. Your key is the first entry. .git-metaWhere attestations are exchanged: your originremote, or the URL you pass with--meta-url. Without either, it is not written.By default attestations travel through git, on
refs/meta/main, and need no server. To keep them in a SQLite file, PostgreSQL or Turso instead, see choosing a store once this setup works.Add
--project DIRwhen the project is not at the repository root, and--principal you@example.comto name the key's owner. For Vitest,--no-installstopsvci initfrom runningnpm installfor the@vci/vitestcollector. -
Add every person or agent who may attest
One line each in
.vci/allowed_signers:alice@example.com namespaces="vci-attest" ssh-ed25519 AAAAC3Nza... build-agent namespaces="vci-attest",valid-before="20270101" ssh-ed25519 AAAAC3Nza...Give agents their own key, not a person's key, and an expiry (
valid-before). -
Merge the files to your default branch
git add vci.toml .vci/allowed_signers .git-meta git commit -m "Enable vci"This step matters. CI reads policy and trusted signers from the base commit, never from the pull request. Until these files are on
main, nothing is skipped. -
Pin your toolchains
An attestation is only accepted where the toolchain matches. Pin the same versions locally and in CI:
Runner Pin Vitest Node major version, package-lock.jsonpytest full Python version in .python-version(a uv-managed build such as3.14.4),uv.lock, the uv version,UV_PYTHON_PREFERENCE=only-managedGo exact Go version ( go versionmust print the same on both sides),go.sum,[env] set = { TZ = "UTC", CGO_ENABLED = "0" }in vci.toml (vci sets them on both sides)Rust exact release in rust-toolchain.toml(arustupbuild, not Homebrew's),Cargo.lock, nomode = "loose"in[env]Rails exact Ruby in .ruby-version(andruby-version:inruby/setup-ruby),Gemfile.lockwith thex86_64-linuxplatform (bundle lock --add-platform x86_64-linux),gem "tzinfo-data",[env] set = { TZ = "UTC" }in vci.toml, andCIleft out of[env](so the generator'sconfig.eager_load = ENV["CI"].present?is false on both sides)RSpec everything in the Rails row, plus options in .rspec(vci never reads~/.rspec,$XDG_CONFIG_HOME/rspec/optionsor.rspec-local), andSPEC_OPTSlisted in[env] hash(orset) if you use itEnvironment variables are three lists in
vci.toml's[env]:set(vci sets them in every test process, on every machine:vci initwritesset = { TZ = "UTC" }, so nothing needs exporting),hash(taken from the machine; a change re-runs the test) andignore(visible, never hashed). Every other variable is removed before the tests run, apart from a short built-in list per runner.vci envprints the effective lists. Details: [env]; per runner: test runners.
Daily use
Three commands cover the daily loop:
# Run some tests locally and sign what was run.
vci run tests/test_b.py tests/test_c.py --key ~/.ssh/id_ed25519
# See what CI would do.
vci plan --base-ref origin/main
# Share the attestations, then push your branch as usual.
vci push
git push
vci run with no paths runs and attests every unit. Paths are relative to the current directory, and what you pass depends on the runner:
| Adapter | Unit | Written as |
|---|---|---|
vitest | test file | src/b.test.ts |
pytest | test file | tests/test_b.py |
go | package | ./b |
cargo | test target | crates/b#lib, crates/b#test:name, crates/b#doc |
rails | Minitest or RSpec file | test/models/b_test.rb, spec/models/b_spec.rb |
Set VCI_SIGNING_KEY=/path/to/key to leave out --key. A .pub path works when the private half is in ssh-agent, hardware keys included.
When a test is not skipped
Ask why:
vci explain src/b.test.ts --base-ref origin/main
src/b.test.ts: RUN (no valid attestation (1 candidates; best failed inputs: entry:fixtures/b.json))
candidate 1: git-meta path:src/b.test.ts vci:attestation:3b9f…:b6c35e2a042def43:2002a8ce…
signer: alice@example.com SHA256:tsNeKgQt70OZTfF07nIc2YrOeC6pHt7EvMF3r8USWEs
issued 2026-09-28T00:26:18Z, expires 2026-10-12T00:26:18Z
failed check: inputs
entry:fixtures/b.json
expected: file exec=false size=24 blake3=b67128f7…
actual: file exec=false size=25 blake3=342a1f57…
expected is what the attestation recorded. actual is what this checkout has now. Here a fixture changed after the test was attested, so the test must run again.
For agents
An agent uses the same commands as a person. Give it:
- its own Ed25519 key, listed in
.vci/allowed_signerson the default branch; VCI_SIGNING_KEYpointing at that key;VCI_PY_PLUGIN,VCI_JS_PLUGINorVCI_RUBY_COLLECTORin its environment (not needed with Homebrew);- push access to
refs/meta/mainon the metadata remote (usually the repository itself).
Next
Set up GitHub Actions
One step after your toolchain setup.
Your test runner
What each adapter records, refuses and needs pinned.
Worked examples
A full round trip with real output, and a setup for each runner.
Configure vci.toml
Policy, environment variables, several projects.
Choose a store
git-meta, SQLite, PostgreSQL or Turso, and how to set each up.