Skip to main content

zti: the Verified Done gate for AI coding agents

CI PyPI License: MIT

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.py runs 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.py is the Claude Code PreToolUse hook. 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 --contract that 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, by zti receipt, by zti verify and so by the pre-commit gate, and by the hook daemon. On 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.
  • 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_FILE to the hook command as shown above.
  • The daemon needs zti-cli installed in the Python that runs it; running gated.py from 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)

Source distribution for zti-cli 0.2.0
File Size Uploaded
zti_cli-0.2.0.tar.gz 72.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for zti-cli 0.2.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page