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.
Spec: DESIGN-FINDINGS.md · builder conduct: AGENT-INSTRUCTIONS.md ·
progress: PHASE-LOG.md · agent integration: PUSHKIN-AGENT-INTEGRATION.md.
Install
Or from source:
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.
Quick start
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:
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.
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, 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.1.0), 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.
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 mechanical floor (fmt, clippy, tests, deny).