Tycho — a free, offline, open-source local verifier that proves an AI agent actually did what it claims, from git, the filesystem, process exit codes, and the harness event stream. No account, no LLM in the trust path.
Project description
"never accept the model's summary as evidence, only artifacts it can't fake by narrating"
Tycho runs as a completion hook on your agent's harness. It records what every turn actually did — files touched, commands run and what they returned, what the agent claimed — and checks that against the artifacts: git, the filesystem, exit codes, the harness event stream.
Only a hook on the event stream sees the process. A diff sees a result. CI sees a result. A
second model sees a diff. Tycho was there when the agent ran pytest, got exit 1, and then said
tests pass.
Instant, free, silent, always-on — no API key, no second session, no tokens, no context burned. Nothing leaves the machine (no LLM anywhere in the trust path). Stdlib only, Python ≥ 3.11. Claude Code today; Codex and other harnesses in development. Linux, macOS, Windows — WSL2 counts as Linux, install inside the distro.
Install
curl -fsSL https://swail.dev/tycho/install.sh | sh # standalone binary, no Python
brew install swail-labs/tap/tycho # same binary, upgradable
uv tool install tycho-cli # have Python? (pipx works too)
npx @swail-labs/tycho <command> # npm wrapper, same binary
The PyPI package is tycho-cli (tycho was taken); it installs the tycho command.
Pick one channel per machine — npm -g and brew both want to own bin/tycho.
The installer sets Tycho up as it finishes, so there is nothing to run afterwards. If you installed another way, or said no:
tycho install # once, for every repo on this machine
That writes two things — the completion hook, status badge and slash commands into your
harness's user-level config, and one line (.tycho/) in your global git ignore file. It names
every path before it writes any of them.
The second write is what lets Tycho stay out of your repos: with .tycho/ ignored
machine-wide, it never edits any repo's .gitignore, so git status stays clean everywhere.
It only runs inside git repos, and defers to any repo that has its own install.
That's it. Every repo you open is verified from the next turn on, and Tycho says so once, the first time it meets a repo.
tycho off # stop verifying this repo (tycho on to resume)
tycho uninstall # remove it from this machine
TYCHO_AUTO=0 # only verify repos where you ran `tycho init`
Adopting a repo
tycho install deliberately writes nothing into your repos. When you want a repo to carry
Tycho with it — shared with your team, checked in CI:
tycho init # in the repo
That adds a .tycho.toml to commit (scope, relay — settings the whole team gets) and a
prepare-commit-msg hook stamping a Tycho-Attestation: trailer onto commits an agent worked
on. It merges with hooks you already have, backs up what it changes, and refuses to touch a
config it can't parse. On a machine where you never ran tycho install, tycho init also
wires that repo's hooks, so a single repo works on its own.
Upgrading from 0.1.x
Your per-repo installs keep working untouched, and take precedence over a machine-wide one.
Two things changed names: tycho uninstall now removes Tycho from the whole machine — it
used to mean this repo, so it refuses and asks when you run it in a repo that has its own
install. Use tycho off to stop verifying one repo, or tycho uninstall --here to remove its
hooks. tycho init --global still works as a spelling of tycho install.
After a turn
Most turns print nothing. Tycho speaks when a check says the turn isn't proven, when the prose claims done but the evidence doesn't reach it, when a run of proven turns breaks, or when a turn touches far more files than this repo's turns usually do. A condition that fired the last two turns goes quiet, so a standing failure doesn't repeat at you forever.
🔍 Tycho: FAILED — file_state: docs/product-direction.md — missing
✓ attempted · executed ✓ artifact_changed ✓ claim_supported
2 files · pytest -q… failed · +7 more
`tycho show` for the full receipt
The second line is the acceptance ladder: attempted → executed → artifact_changed → claim_supported. Unreached rungs render ·, not ✗ — a gap in the evidence isn't a failure.
Verdicts are VERIFIED · FAILED · STALE (evidence exists but predates the code) ·
INDETERMINATE (ran, couldn't conclude) · UNSUPPORTED · OVERRIDDEN.
What you can do with it
tycho show # the full receipt for the last turn (or `show <id>`)
tycho log # what agents did here, newest first (--verdict, --since, -n)
tycho blame src/app.py # which turns touched this file, what they claimed, what backed it
tycho review # which changed hunks no test covered and no command exercised
tycho backfill # record what agents did here before Tycho was installed
tycho verify # re-run the engine over a session on demand
tycho count --ledger # catch and blind rates, per model and per check
tycho exec -- pytest -q # run it and put the real exit status on the record
tycho attest --verify # check a commit's trailer against the record
tycho scope add 'src/**' # bound where the agent may edit (opt-in; `--exclude` denies)
tycho relay --on # let the agent see its own verdict and keep working (off by default)
tycho rewrite --on # route a piped runner through `tycho exec` for you (off by default)
tycho off # stop verifying this repo (`tycho on` resumes)
tycho doctor # is the hook installed, current, and firing?
tycho help lists everything, including hook, install, init, on/off, uninstall,
statusline, run, override and update.
Exit codes — one per adverse kind, so a gate picks what blocks. 1 FAILED, 3 STALE
(verify); 6 unexercised (review --exit-code); 7 trailer mismatch (attest --verify); 5
installed-but-broken (doctor); 2 usage, 4 couldn't complete. The hook itself always exits 0
and never blocks your agent.
What Tycho won't claim
Tycho answers can't tell rather than guessing, so there are places it deliberately says less than you might expect:
blame PATH:LINEis file-level. The record stores which turns touched a file, never which lines. Joining throughgit blamewould put a confident wrong name on a line.reviewspeaks about the record, not execution. "No command Tycho recorded ran after this hunk" — never "this line never ran". There's no per-line coverage here.- The attestation proves consistency, not authenticity. Unkeyed SHA-256 over a record this
machine wrote: tamper-evidence against accidents, not a defence against a motivated agent.
--verifycan answer cannot tell, and a pruned record must never read as a forged one. tool_call_provenanceis advisory. Two claim families (web, issue trackers), matched on family presence only. Tuned to never emit a false FAIL, at the cost of recall.- A backfilled turn has no verdict, and never will.
tycho backfillreplays transcripts written before Tycho was here, but most checks are functions of state that no longer exists — whether an edited file is on disk now, an mtime now against a run three weeks ago. So those rows record what the transcript proves (files, commands, claims, when) and readUNVERIFIED. They're excluded from every rate incount --ledger. Tycho will not tell you what it "would have caught". verifier_integrityraises the cost of tampering; it doesn't make it impossible. It reports a turn that edits.tycho/,.tycho.toml, a harness hook config or a git hook — via the edit tools, a shell redirect, or a mutating command. It cannot see a write smuggled through an interpreter (python -c "open('.tycho.toml','w')") or a path the shell expanded from a variable, and a process running as you can always reach the file. Tycho's own state is judged inside this repo only — another tree's.tycho/isscope_drift's business, not a verdict here. It is the check[checks].disablemay not switch off.- A piped runner's exit status is gone before Tycho can read it.
pytest | tail -20hands the harness tail's status, so the verdict falls back to reading pytest's summary line — inference, and only as good as the vocabulary inengine/runlog.py.tycho exec -- pytest | tail -20fixes it at the source, andtycho rewrite --ondoes that for you. Rewriting is opt-in because Claude Code matches yourBash(...)rules against the rewritten command, so an allow rule you already granted can stop covering it; under--permission-mode bypassPermissionsthere are no rules to void and it always applies. - Nothing polls in the background. A hook that died goes undiagnosed until someone runs
doctor.
Configuration
Settings in .tycho.toml (scope, relay, override, rewrite), state in <repo>/.tycho/. Command
strings, evidence and agent prose are pattern-filtered for secrets before hitting disk — a match
becomes a visible [REDACTED] — but it's best-effort, so keep .tycho/ out of git regardless.
TYCHO_HOME · TYCHO_CLAUDE_HOME · TYCHO_AUTO · TYCHO_STATUS · TYCHO_RELAY_MAX ·
TYCHO_TURNS_MAX · TYCHO_COMMANDS_MAX · TYCHO_NO_UPDATE_CHECK · TYCHO_INSTALL.
The one thing that touches the network: a SessionStart hook asks pypi.org once a day whether a
newer Tycho exists, disclosing your version and IP. TYCHO_NO_UPDATE_CHECK=1 turns it off.
License
Apache-2.0. Contributing: CONTRIBUTING.md.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
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 tycho_cli-0.2.0rc2.tar.gz.
File metadata
- Download URL: tycho_cli-0.2.0rc2.tar.gz
- Upload date:
- Size: 719.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c5be5d6c01a456fe8c7c2732a94d090dd452cdd5ec93dfa9ec2856f779cd8669
|
|
| MD5 |
128302ecad69c38eb4f58f87abc13499
|
|
| BLAKE2b-256 |
b8e495d5da0455c9854a0fc004fd62fd78ed552a976469e8eadbecf160a3863a
|
Provenance
The following attestation bundles were made for tycho_cli-0.2.0rc2.tar.gz:
Publisher:
release.yml on swail-labs/tycho
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tycho_cli-0.2.0rc2.tar.gz -
Subject digest:
c5be5d6c01a456fe8c7c2732a94d090dd452cdd5ec93dfa9ec2856f779cd8669 - Sigstore transparency entry: 2281703194
- Sigstore integration time:
-
Permalink:
swail-labs/tycho@b3e2657fc7573b784cfbcf560f21639dc180a0ec -
Branch / Tag:
refs/tags/v0.2.0-rc.2 - Owner: https://github.com/swail-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b3e2657fc7573b784cfbcf560f21639dc180a0ec -
Trigger Event:
push
-
Statement type:
File details
Details for the file tycho_cli-0.2.0rc2-py3-none-any.whl.
File metadata
- Download URL: tycho_cli-0.2.0rc2-py3-none-any.whl
- Upload date:
- Size: 222.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c5eec54f853b6ca9124b4b831ad19606d6f8b1358a689ac8c0203f718cc2f58
|
|
| MD5 |
fa433e131225a44ce9b6c6c205847b88
|
|
| BLAKE2b-256 |
62210ca48fe172babdb6dde96599fc5b988d745462633aeb685fcb9939096e20
|
Provenance
The following attestation bundles were made for tycho_cli-0.2.0rc2-py3-none-any.whl:
Publisher:
release.yml on swail-labs/tycho
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tycho_cli-0.2.0rc2-py3-none-any.whl -
Subject digest:
3c5eec54f853b6ca9124b4b831ad19606d6f8b1358a689ac8c0203f718cc2f58 - Sigstore transparency entry: 2281703314
- Sigstore integration time:
-
Permalink:
swail-labs/tycho@b3e2657fc7573b784cfbcf560f21639dc180a0ec -
Branch / Tag:
refs/tags/v0.2.0-rc.2 - Owner: https://github.com/swail-labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b3e2657fc7573b784cfbcf560f21639dc180a0ec -
Trigger Event:
push
-
Statement type: