pushkin
Schema-first enforcement harness that gates AI coding agents' file writes against project contracts, deterministically. An agent write that violates a contract, touches protected surface, or edits a committed test suite is denied at the hook — with the reason, the fix hint, and an auditable event trail. Humans stay un-gated where it matters; everything fails open loudly, never silently.
Documentation map
| document | what it covers |
|---|---|
DESIGN-FINDINGS.md |
canonical spec, the research it rests on, and the F-Ledger |
docs/TESTING.md |
testing design and infrastructure — the layers, the red→green protocol, read-only suites, fixtures, the floor and its accounting, CI, and the evidence for why it is shaped this way |
docs/TESTING-RUST-FOR-PYTHON-ENGINEERS.md |
the toolchain and test model taught from a Python baseline; start here if cargo and clippy are new |
docs/RESEARCH-SOURCES.md |
every external figure this repo cites, with URLs and verification dates — and an honest list of the claims that still have no source |
AGENT-INSTRUCTIONS.md |
builder conduct — the N-rules and the pass protocol |
docs/STANDING-RULES.md |
rules that apply to every pass (V2, V3, V5, N10/§4.1) |
docs/CONTINUATION-GUIDE.md |
area-by-area orientation for someone picking the repo up |
docs/ROADMAP.md |
current state, open findings, what is queued |
PHASE-LOG.md |
append-only record of every pass |
PUSHKIN-AGENT-INTEGRATION.md |
per-agent hook mechanics for all five adapters |
Install
cargo install pushkin
Or from source:
cargo install --path crates/pushkin-cli
No Rust toolchain? The same binary ships on PyPI as pushkin-cli (macOS and
Linux wheels, nothing but the executable inside):
uv tool install pushkin-cli # or: pipx install pushkin-cli / pip install pushkin-cli
Use the installed binary for hooks, not uvx pushkin-cli — uvx resolves an
environment on every call, which the warm-path latency gate does not budget for.
Requires the binary on PATH (hooks resolve pushkin from PATH by
design — nothing bakes an absolute path). pushkin doctor names it if
resolution would fail.
After any change to gate behaviour, reinstall. A binary older than the manifest schema fails every manifest-reading verb at exit 1 before any gate logic runs, and a hook exiting 1 is non-blocking — so version skew silently disables the write-time gate while
doctorstill reports healthy. Tracked as F71; seedocs/ROADMAP.md.
Quick start
cd your-repo # carrying a pushkin.toml manifest
pushkin init # Claude Code hooks + versioned consent
pushkin init --agent git # native pre-commit shim (no other tools needed)
pushkin doctor # verify the install; --repair fixes what it owns
pushkin floor # run the repo's declared mechanical floor
Which manifest governs
Resolution is pinned to the repository, not to your shell's working directory:
PUSHKIN_MANIFEST, if set — that file, wherever it is. If it names a file that cannot be read, pushkin fails with a named error; it never falls back to the repo's manifest behind your back.- Otherwise
<git rev-parse --show-toplevel>/pushkin.toml— the repository root's manifest, whatever directory you invoke from. - Otherwise
./pushkin.toml, and pushkin says so on stderr.
Case 3 is the honest edge: where git is missing or the tree is not a repository, pushkin cannot
tell whether a root exists, so it uses the working directory and tells you it did. In that
situation a pushkin.toml in a subdirectory still governs — pushkin doctor names every nested
manifest it finds, and carries a standing info line when git is unavailable.
Every verdict names the manifest it was decided under: repo-relative in the agent-facing prose,
absolute in --json and in the pushkin floor header.
Adapters: --agent claude | codex | auggie | hermes | opencode, plus two
pre-commit floors and a managed instructions block (--agent agents-md).
--remove-agent <name> uninstalls only what pushkin owns.
The pre-commit floors
Two interchangeable floors run pushkin check --staged --json before every
commit; both carry the same guard: fail open only on positively probed
absence (no binary, or no pushkin.toml), with a loud actionable notice —
a teammate who never installed pushkin is not blocked by a hook they never
opted into, and a check that RUNS is never second-guessed.
pushkin init --agent lefthook— merges a marker-bracketed block intolefthook.yml, preserving every foreign command byte-for-byte. Any prior pushkin marker generation upgrades in place; uninstall leaves no litter.pushkin init --agent git— writes.git/hooks/pre-commitdirectly, no hook-manager dependency. Detectscore.hooksPathoverrides and foreign hooks and prints the integration snippet instead of clobbering.
Team enforcement: CI is the backstop
The hooks above bind only machines that opted in. A teammate who never
installed pushkin (or lefthook) commits ungated — the fail-open floor is
deliberate, so local hooks alone cannot enforce the gate team-wide. The
backstop is CI: run the gate as a required status check with the
pushkin-gate composite action this repo ships
(.github/actions/pushkin-gate), and unprovisioned pushes are caught at
the pull request instead of landing silently.
name: pushkin-gate
on: [pull_request]
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # affected mode needs history (F19)
- uses: acoletti/pushkin/.github/actions/pushkin-gate@<TAG-OR-SHA>
with:
mode: affected # or `check` for the full repo
base-ref: origin/main
pushkin-ref: <TAG-OR-SHA> # pin; a moving ref is non-reproducible
affected gates only the diff against base-ref; check sweeps every
mapped file. Optional db-drift / db-rls inputs add the schema gates
(see the action's own docs for their prerequisites). Make the job a
required check on the default branch and local provisioning becomes a
latency optimization, not the enforcement boundary.
What the gate enforces
- Contract conformance at mapped boundaries (
contract.boundary.*). - Protected paths (
pushkin.protected_path): the manifest, schemas, CI, the floors — agent-denied at write time, unwaivable by construction. Staged protected changes draw a loud advisory at the floor, never a block (the floor runs for humans too). - Committed tests are read-only (
pushkin.read_only_path): files in git HEAD underread_only_pathsglobs deny agent edits; NEW test files and uncommitted iteration flow freely. Unwaivable. - Suppression comments (
pushkin.suppression.new) per the manifest. - Waivers (
pushkin waive): scoped, expiring, decision-logged — for rules that allow them; doctor lints stale grants.
Read-only test surfaces are not a stylistic preference. Public measurement of
frontier models finds test modification is one of four systematic cheating
strategies, and that "Read-only access provides a middle ground: it restores
legitimate performance while preventing test modification attempts" where hiding
the tests degrades performance on the original benchmark
(ImpossibleBench, arXiv:2510.20270).
docs/TESTING.md §2 collects that evidence alongside this repo's own recorded
incidents; docs/RESEARCH-SOURCES.md carries the full citations.
The mechanical floor
pushkin floor runs the [floor] table declared in pushkin.toml — one
committed list of commands that pushkin floor, the Makefile,
scripts/floor.sh and CI all read, so "CI runs the same checks you do" is a
fact rather than a promise someone has to remember.
make floor # the canonical green — cite this, not a cargo count
pushkin floor --skip bench # named omission; prints EXCLUDED, NOT a full floor
Three properties worth knowing:
- Ignored tests are accounted for. A command declaring
reconcile_ignoredmust have its ignored count matched exactly by a command declaringcovers_ignored_of, or the floor is RED. Self-coverage is rejected at parse time, and a coverer excluded by--skipcovers nothing. This exists because a cited floor number once silently omitted two#[ignore]d benchmarks — one of which was red at the time. - The exit contract is inverted relative to ESLint/ruff, and disclosed in
--help: 0 clean, 2 findings, 1 the gate itself could not run. - Network-dependent verdicts are disclosed.
inputs = "network"on a command (e.g.cargo deny check, which consults the RustSec advisory DB) prints a note that the verdict is not a pure function of the commit.
docs/TESTING.md §6–§7 documents the table, the accounting and the exit codes
in full.
The schema-only tier (no JS runtime)
Gate-time enforcement is pure Rust. A repo that commits its canonical
schemas needs no bun, node, or JS toolchain to be gated: install the
binary (cargo install pushkin, or uv tool install pushkin-cli with no
Rust toolchain either), run pushkin init, done. Bun is a compile-time tool only —
pushkin compile regenerates canonical schemas and bindings from Zod
sources, and repos that author schemas directly (or vendor them) never
need it. If you never run compile, you never need a JS runtime.
Doctor
pushkin doctor verifies every installed surface (adapters, both floors,
binding epochs) and exits 1 on findings; --repair regenerates only what
pushkin owns — foreign configs are reported, never rewritten, and
advisory findings (like an unresolvable binary) never trigger a rewrite.
Repair exit code: 0 when every repair that could run succeeded; deferral
messages name the manual remedy without failing the run.
Release status
pushkin is published on crates.io — cargo install pushkin installs the
latest release. All four crates version in lockstep with exact internal pins
(=0.2.1 at the current workspace version), and Cargo.lock is a release
artifact: cargo install --locked reproduces the exact dependency graph a
release was tested against.
Published versions are immutable. A correction to shipped content — including
this README, which renders on the crate page from the published tarball —
requires a new patch release; it cannot be fixed by editing develop.
Versioning policy:
- Patch (
0.1.x) — fixes that do not change emitted output: docs, CI, the gate action, internal refactors. - Minor (
0.x.0) — new schema capability or changed emitter output. Any change undercrates/pushkin-compiler/src/**is epoch-sensitive and bumpsschema_epochinpushkin.toml, so it is at least a minor. - Tags — every published version carries an annotated
vX.Y.Ztag on the exact commit whose tree was published.
Contributing
Tests are written first, committed before the implementation, and read-only
once committed. make floor must be green at every commit. The full protocol,
the fixture patterns, and the reasoning are in docs/TESTING.md; if Rust is
new to you, read docs/TESTING-RUST-FOR-PYTHON-ENGINEERS.md first.
Phase 0 reference implementation
The original Bun/TS spike (Claude Code gate) still lives in src/ and
tests/: make test && make lint. The Rust workspace is the product;
make floor runs its full declared mechanical floor — currently seven
commands: fmt, clippy, test, bench, deny, tsc, bun-test.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pushkin_cli-0.2.1-py3-none-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: pushkin_cli-0.2.1-py3-none-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 5.5 MB
- Tags: Python 3, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c5ce6f65641dbe01b0670c8793d25118a8892363a628c9e9a617d0946304c415
|
|
| MD5 |
637fd847cfd635b4d0a18b355be2b302
|
|
| BLAKE2b-256 |
74f9abbadd52e8ce39fab31b5868349906da53b46f90b07d74afd927c539f666
|
File details
Details for the file pushkin_cli-0.2.1-py3-none-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: pushkin_cli-0.2.1-py3-none-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 5.1 MB
- Tags: Python 3, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3329523ac53b14a5e8fe7ca7fc8a8c312f23d29de299d49534ec129f17eeb45d
|
|
| MD5 |
ea898c7bd72f6914df28c3b4c6fe9405
|
|
| BLAKE2b-256 |
6e240d5d573c564b4abe09dc890f20e81d722dea1a87cbd86383379760b37351
|
File details
Details for the file pushkin_cli-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: pushkin_cli-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 5.4 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5e27f6f39bb5f5b537752c382056e7e8500998e41a505eb7235d4e486eedeea6
|
|
| MD5 |
1f1f6174f1999cb0869f95def5a1bbf2
|
|
| BLAKE2b-256 |
18b4ee9b2aac95b30d5ab6a133737feca5377a7e7732be7c8aca8324cd2a2159
|
File details
Details for the file pushkin_cli-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: pushkin_cli-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 5.0 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3bd6cb098389187c582c6ddf33986d682d431c3d8fea20d2336eda7ff4f5824e
|
|
| MD5 |
2055517222f79bcf6c39f309ea0e290e
|
|
| BLAKE2b-256 |
8ca41d1d6615d20aba981490ff23fb20fa68e52791a5b8691ae7ef3fc860f4d5
|
File details
Details for the file pushkin_cli-0.2.1-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: pushkin_cli-0.2.1-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 5.0 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cbe96f706895ecb6c5ea2f4aca28a10a9f8a539f15c628d4291e2d833af860c2
|
|
| MD5 |
b8d33d7f5d98d6dbcc9c8b2426b7c198
|
|
| BLAKE2b-256 |
fdfc16151beaa68b8e7abf488244415f30f1f66ce9fa81ce80b6d73a7c0ce40c
|
File details
Details for the file pushkin_cli-0.2.1-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: pushkin_cli-0.2.1-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 5.2 MB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee06dc4bc2cb0d769f3ea556fdd068d54e1e6085526cc087858cd2845f02ba09
|
|
| MD5 |
02007bfd6fde5fc18b3de523509776f2
|
|
| BLAKE2b-256 |
88a5accfae45561a93e322c3c98a4c9ed3714a0f29313f67f30657194cc55dfc
|