zti: the Verified Done gate for AI coding agents
Your agent says the work is done. zti proves it. It re-runs the required
checks itself, seals the result in a hash-chained ZTIP
receipt bound to the exact git tree, and blocks the commit or merge when no
passing receipt exists. The agent's claim is never the input.
This repo is the open client layer of ZTI: the CLI, the gate library, and the enforcement hooks. MIT licensed. It reports to a ZTI Core control plane, which stores receipts, verifies them at commit and merge time, and gives you the fleet dashboard and audit trail.
See it work in public: bitscon/zti-verified-done-demo.
PR #2 is a --no-verify bypass caught red in CI and left open on purpose.
Three enforcement tiers, one binding model
A receipt binds to git write-tree: the content address of exactly what would
be committed. Identical at pre-commit and in CI, so nothing can drift between
what was verified and what ships.
| Tier | Where | What it does |
|---|---|---|
| 1. Runtime hook | Claude Code PreToolUse |
Blocks out-of-contract writes and irreversible commands live during the session. Bash is screened for irreversible patterns and forbidden-path tokens; full Bash scoping is on the roadmap |
| 2. Pre-commit gate | .git/hooks/pre-commit |
Blocks the commit unless a passing receipt covers the staged tree |
| 3. CI required check | GitHub Actions | Catches --no-verify at the merge, on the PR head SHA |
Fail-closed on every path: no receipt, a failed receipt, an unreachable plane, and a license-locked plane all block. Agent-neutral: it governs any committer, human or agent, by git content alone.
Install
pip install zti-cli
The package is zti-cli; the command it installs is zti.
Point it at your plane with .zti/config.json in the repo
({"plane_url": "...", "gate_id": "..."}) plus the gate key in
.zti/gate.key, or use the ZTI_PLANE_URL / ZTI_GATE_ID / ZTI_GATE_KEY
environment variables. Keep .zti/gate.key out of version control.
The plane URL must be https://, or plain http:// to a loopback host
(localhost, 127.0.0.0/8, ::1; the default is http://127.0.0.1:8100).
Plain http:// to any other host is refused before the first request, because
that channel carries the gate key, contracts, decisions and the hook session
secret. If you must run a plaintext plane on a LAN, set
ZTI_ALLOW_INSECURE_HTTP=1 to accept that risk explicitly; the client then
proceeds with a warning on every start. Plaintext plane traffic never goes
through an environment proxy.
This check runs on every command that talks to the plane, including
zti verify and therefore the pre-commit gate: with a plaintext LAN plane,
every commit is blocked until ZTI_ALLOW_INSECURE_HTTP=1 is set in the
environment git runs under, or the plane moves to https.
Use
zti receipt # re-run the contract's required checks, mint a receipt,
# ship it to the plane bound to the staged tree
zti verify --staged # exit 0 only if a passing receipt covers the staged content
zti verify <sha> # same check for a commit (what CI runs)
zti install-hooks # drop the Tier-2 pre-commit gate into .git/hooks
Exit codes: 0 pass · 1 receipt exists but not passing · 2 no receipt ·
3 plane unavailable (fail-closed) · 4 usage error.
A contract is a small JSON document. zti receipt re-runs its
required_checks itself and refuses a passing receipt when they fail:
{"work_id": "billing-fix", "allowed_paths": ["src/**"],
"forbidden_paths": ["db/**"], "required_checks": ["pytest -q"]}
Where the contract comes from, in order: the policy your plane publishes for
the repo, then --contract <file>, then .zti/contract.json. The published
policy is the authority. A --contract file that differs from it is refused
and nothing mints, so the governed party cannot swap the operator's checks
for weaker ones; a file that restates the policy exactly is accepted. With no
published policy, the local contract is used. This client-side refusal needs
the plane to be reachable and serving the policy at mint time; when it is not,
the local contract mints offline, and it is the plane's own check at ingest
that refuses a receipt minted under anything but its published policy.
zti receipt also warns when a required check is a constant-true no-op (true, echo ok, pytest || true):
it still mints, but says the check verifies nothing.
For Tier 3, copy zticli/templates/zti-verify.yml into the governed repo's
.github/workflows/ and mark zti-verify as a required status check in
branch protection.
The Claude Code hook (Tier 1)
hooks/claude-code/ holds the pair:
gated.pyruns beside the repo, loads its contract from the plane, and enforces it with the same gate library that mints receipts. If the plane goes down it keeps enforcing on the cached contract. The plane is never in the evaluation path. While it runs it sends the plane a heartbeat, so a gate that is switched off reads as dark on the plane instead of silently looking healthy.pretooluse.pyis the Claude CodePreToolUsehook. It asks the local daemon and blocks on anything but an explicit allow. A dead gate can never silently allow. An allow counts only when it is authenticated with an HMAC under a per-session secret, computed over this exact request and its fresh nonce, so a process squatting the daemon's port cannot answer "allow". An allow with a missing or wrong HMAC, or a secret the hook cannot read, blocks.
Start the daemon beside the repo first, with the Python that has zti-cli
installed (the daemon imports its gate library). At startup it registers with
your plane (set ZTI_ENROLL_TOKEN first if the plane requires an enrollment
token), writes the session secret to .zti/gated.session (mode 0600; set
ZTI_GATE_SESSION_FILE to put it elsewhere, and keep it out of version
control), and loads the contract it will enforce:
python3 hooks/claude-code/gated.py --repo-root . --repo-name myrepo
The honest limit: the hook runs as the agent's own user, so an agent that reads its own session file can still compute a valid HMAC for a forged allow. The per-session secret raises the bar against an impostor that has not read it; it does not stop one that has. Only authenticating off the machine closes that, and the plane's liveness record is what makes a disarm visible.
Then wire the hook in the governed repo's .claude/settings.json:
{"hooks": {"PreToolUse": [{"matcher": "Write|Edit|NotebookEdit|Bash",
"hooks": [{"type": "command",
"command": "ZTI_GATE_SESSION_FILE=/abs/path/to/repo/.zti/gated.session python3 /path/to/pretooluse.py"}]}]}}
Give the hook the session file as an absolute path, as above, so it does not
depend on the directory the hook happens to run in. If the daemon was started
with its own ZTI_GATE_SESSION_FILE, use that same path here.
Upgrading from 0.1.0
0.2.0 is stricter in four ways an existing setup can hit:
- A
--contractthat differs from the plane's published policy is refused and nothing mints. Drop--contract, or have the operator publish the change. - Plain
http://to a non-loopback plane is refused, byzti receipt, byzti verifyand so by the pre-commit gate, and by the hook daemon. On a plaintext LAN plane every commit is blocked untilZTI_ALLOW_INSECURE_HTTP=1is set in the environment git runs under, or the plane moves to https. - The hook pair must be upgraded together, against a plane that issues the
per-session secret; an older plane makes the daemon refuse to start. Add
ZTI_GATE_SESSION_FILEto the hook command as shown above. - The daemon needs
zti-cliinstalled in the Python that runs it; runninggated.pyfrom a bare checkout with nothing installed no longer works.
What you need to run it
A ZTI Core plane. ZTI Core is complete and in early access. Every install includes a free 30-day trial with full functionality, and individual use is free on the honor system. Organizations license it per year; pricing is announced at launch. Start at licensing@zerotrustintelligence.io or zerotrustintelligence.io.
The protocol underneath is open: ZTIP, MIT, with its own spec, schemas, and reference runtime.
Development
pip install -e . pytest
pytest -q
The suites here cover the gate library, the runtime hook and gate daemon
client, the zti CLI (against a stand-in plane), the plane transport policy,
and ZTIP conformance. The plane halves of those behaviours, and the full
product battery, run in the ZTI Core tree.
License
MIT. Copyright (c) 2026 Chad McCormack.
Metadata
Release files for zti-cli 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| zti_cli-0.2.0.tar.gz | 72.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| zti_cli-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 105.5 kB
Release files / zti_cli-0.2.0.tar.gz
| Download URL | zti_cli-0.2.0.tar.gz |
|---|---|
| Size | 72.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5f135c89da649afe6dda0f30e0835b0e75110983f22de56e9241324376e8da03
|
|
BLAKE2b-256 checksum How to use checksums |
ecb566e1e164793b2e2f65676fb24343a1313fa920b5139349147590cf14e575
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency logRelease files / zti_cli-0.2.0-py3-none-any.whl
| Download URL | zti_cli-0.2.0-py3-none-any.whl |
|---|---|
| Size | 32.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3ea9c2b606e67f24218047930d757f69c04b5d6854a6fe2523867c3f5f4e0ac3
|
|
BLAKE2b-256 checksum How to use checksums |
b806d248653254e34af6412ccc396bec262da2e8276d97f2bcb279b80dbf4d59
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.
Transparency log