A permission gate for Claude Code and Codex. It runs as a PreToolUse hook:
every tool call the host exposes is parsed, judged against declared knowledge
of what programs do, and answered before it runs.
Claude Code supports vouch's native allow / ask / deny response. Codex does
not currently support ask from PreToolUse, so vouch blocks the first
attempt and routes the human decision through Codex's native approval prompt
for a local MCP broker. An approved broker call grants one exact retry. An
allow emits nothing to Codex, leaving its native sandbox and approval policy
fully authoritative.
The judging is not pattern matching on the text of a command. vouch has three scanners (bash, PowerShell, python) and walks what the command actually does: which program runs, from which directory, what it writes and where, what it hands to another interpreter, and which parts of it could not be read at all. A line with several commands in it gets the strictest answer any one of them earns.
The decision set has a fourth member, abstain — emit nothing and let the
harness's own rules decide. It is deliberately unreachable while vouch is
gating a tool call: saying nothing is not neutrality, it is the permissive case
wearing a different hat. Anything vouch cannot read prompts instead.
Permission prompts that you cannot turn off train you to approve without reading. So: every prompt vouch raises names the setting that turns it off. A prompt with no named setting is a bug in vouch, whatever else is true about it.
One exception is deliberate. The protected paths — vouch's own config.toml
and the hook registration in Claude's settings.json or Codex's hooks.json
— always prompt, and no
write.allow_paths entry can open one however broadly it is written. The
protected list is checked first and wins. That list is itself the setting, and
the prompt says so: removing a line from [protected] paths removes the
protection. A prompt that claimed to be unsettable while being settable would
be worse than one that admits where its off-switch is.
A command is allowed because everything in it is recognised, not because nothing recognised was found.
The difference is in how each kind of tool fails. A deny-list's error mode is silent: when it misses something, a miss looks exactly like "nothing to report", and you get no signal at all. An allow-list's error mode is a prompt you can see, read, and switch off by name. That is the whole trade — more prompts up front, in exchange for the failures being visible.
So: an unknown program asks. An unknown subcommand of a known program asks. Code vouch could not parse asks, rather than being called probably fine. An unrecognised harness tool asks, because emitting no decision would leave the harness deciding alone, which is the permissive case wearing a different hat.
This project got that wrong once, and the reversal is worth stating plainly. The original design said absence of knowledge must never be the permissive case. Measuring the cost — with almost no knowledge written yet, "unrecognised asks" would have prompted on 96.4% of real recorded commands — the setting was flipped to allow unmodelled commands, and a principle was written afterwards to justify it. That number was not a reason to abandon the invariant; it was a measurement of how much knowledge was missing. Every hole later found by review was that one decision. The default is back, and the knowledge file is where the cost gets paid down.
For each command the scanners find, in plain words:
- Recognition. Is there an entry describing this program, and does it
cover this subcommand? Recognition is per command, not per program name:
kubectl get podsandkubectl delete podare different amounts of trust and are settable separately. Recognising a program claims only that vouch knows what it is — never that it is harmless. - Guards. Named effects declared in the knowledge — the shipped file
declares the common ones (recursive deletion, history rewrite, process
control, privilege escalation, and the rest), and you add or override your
own. Every rule says where it came from, and the prompt prints it as a
rule source:line:declared,requestedif someone asked for it, orinferredif it is a guess, which it then says out loud. A guard asks every time on purpose, and approving one never creates a rule. - Constructs. Things vouch could not see through — a here-document it was not given, a command whose text is computed at run time, a snippet in a language it has no scanner for. Each is named, and each has its own setting.
- Place. Where the command runs: the hook's directory, advanced by the
cdwalk and by run-directory flags such asgit -C <dir>. Trust and distrust zones, and guard overrides scoped to a tree, are decided from that. - Writes. Where a write would land, resolved through quoting, variables
assigned on the same line, and relative paths. The protected list is checked
first; then the write walls (
ask_paths,deny_paths); then the allow rules.
Then the answer, with its reason and the setting behind it. Here is a real one, from a command that names no program vouch has been told about:
$ vouch explain --cwd 'C:/Users/dev/project' 'frobnicate --now'
judged from: C:/Users/dev/project
ASK
vouch stopped on: unmodeled_command
no description of: frobnicate
what that means: vouch has no entry that covers it
to recognise one, use the vouch-trust skill — it proposes the narrowest entry,
shows exactly what that entry would trust, writes it only on your accept (it
drives `vouch trust`, whose usage `vouch trust` alone prints), and proves it
fires. The narrowest entries here:
frobnicate — an entry would recognise every operation of `frobnicate` — or,
narrower, exactly this flags-only shape: `subcommands = []` with
`standalone_flags = ["--now"]` and `case_sensitive_flags` stated
to stop checking for unknown programs entirely, set
lang.bash.constructs.unmodeled_command = "allow" — that allows every program
vouch has never heard of, not just this one
A guard ask has the same shape and ends the same way — setting: guards.delete_recursive (currently "ask") — so the way out is always on the
screen, next to what it would cost.
All three live in ~/.config/vouch/.
| File | What it is | Who writes it |
|---|---|---|
knowledge.toml |
What programs and harness tools are and do: ls reads, rm -r deletes recursively, git takes -C with a value. Every entry is a claim that must be true |
Ships in the release bundle; from source, scripts/install-knowledge.sh --force replaces it wholesale. Your own entries do not belong here |
my-knowledge.toml |
Your own descriptions, laid over the shipped ones piece by piece: what you set wins, what you leave unset keeps what ships | You, or vouch trust on your explicit accept. Nothing overwrites it |
config.toml |
What you have told vouch to do: guard actions, construct actions per language, trust and distrust zones, write rules, the protected list | You. vouch review --accept is the only command that writes this file, and only on that accept |
Every key in the first two and every setting in the third is documented in
docs/reference/reference.md, which is generated
by vouch schema config --write from the same structs the loaders read — so it
cannot describe a shape vouch would refuse. vouch.example.toml is a working
config.toml to start from. Both binaries and knowledge.toml are version-gated
against each other: a mismatched pair refuses loudly rather than deciding
half-blind, so they move together.
Three pieces make a working install, and they arrive differently:
- The gate —
vouch,vouch-codex-broker, andknowledge.toml, from one commit. - The wiring — Claude uses four entries in
~/.claude/settings.json; Codex uses two in~/.codex/hooks.jsonplus the local approval broker. Hook documents are always saved by a human; see below. - The procedures — the skills, including
vouch-setup, which walks a machine through the rest.
The repository is its own single-plugin marketplace:
claude plugin marketplace add <owner>/vouch
claude plugin install vouch@vouch
Then run /vouch:setup. That skill surveys the machine, replays that machine's
own recorded session history against a candidate config so you can see what
would actually fire before accepting anything, and writes nothing without an
explicit accept per change.
The plugin carries the consumer skills — machine setup, status, update, and
program trust — and the /vouch:setup, /vouch:update, and /vouch:status
commands; the development repository's own working procedures stay behind in
its tree. It deliberately
does not carry the hooks: the live gate must not move when a plugin cache
refreshes, and hook registration is a human save whatever ships.
Keeping it current is a step someone has to run — a new version in the marketplace moves nothing by itself, because the plugin cache is keyed by the version installed:
claude plugin update vouch
then restart Claude Code.
That updates the procedures only. /vouch:status reports what is installed —
binary, knowledge, and plugin versions against the newest release — and writes
nothing. The gate — both binaries and
knowledge.toml — updates through /vouch:update: it compares the installed
version against the newest release, downloads the platform bundle, verifies it
against SHA256SUMS before unpacking, installs binaries and knowledge
together, and re-probes, all on an explicit accept. Run both update halves
when a release lands, so the skills and the binary they describe move in step.
The same repository is a Codex marketplace:
codex plugin marketplace add <owner>/vouch
codex plugin add vouch@vouch
Start a new Codex thread and run $vouch:vouch-setup; updating an installed
gate later is $vouch:vouch-update, and checking without moving anything is
$vouch:vouch-status. The plugin carries the
same procedures as Claude Code; it deliberately carries neither live hooks nor
an executable, so plugin refreshes cannot silently replace the gate.
Prerequisites: Claude Code or Codex, and a Rust toolchain at or above the floor
Cargo.toml names in rust-version.
git clone https://github.com/<owner>/vouch
cd vouch
cargo build --release && scripts/install-binaries.sh && scripts/install-knowledge.sh
cp vouch.example.toml ~/.config/vouch/config.toml
scripts/install-skill.sh
Even on a development machine this path proves and tests the build; the released artifacts remain the primary install (see the delivery-route note below).
That copy matters: with no config.toml at all, nothing has been allowed, so
every command asks and every prompt carries a banner saying why. The example
file is a working starting point — the binary's own message points at it.
/vouch:setup builds one from your machine's evidence instead, if you took the
plugin path.
Then generate the hook wiring, which no agent writes for you.
For Claude Code:
mkdir -p ~/.local/state/vouch
~/.config/vouch/bin/vouch install > ~/.local/state/vouch/claude-settings.json
vouch install reads your existing ~/.claude/settings.json, merges in four
hook entries pointing at the binary's own path, and prints the merged document
— it writes nothing. Read the file it produced, then move it into place
yourself. Two reasons this is a human step and stays one. The program that
gates an agent's tool calls is not a file to let that agent rewrite, so vouch
prints and you save; src/install.rs records the same reasoning. And the
printed document is your entire settings file, which can carry credentials in
MCP server headers — so it belongs in an editor, never in a chat transcript or
a captured command output. If you want to inspect what changed locally without
opening the whole settings file, vouch install --print narrows the document
to the hooks-only view; redirect that form to private scratch too.
The four events, and why each is registered:
| Event | For |
|---|---|
PreToolUse |
the decision — this is the gate |
PostToolUse |
recording that it ran |
PostToolUseFailure |
recording that it errored or was interrupted |
PermissionDenied |
recording that you refused it |
The last three decide nothing. They exist so vouch review draws its
candidates from what actually happened rather than from absent signals.
For Codex, choose the shell Codex uses on this machine:
mkdir -p ~/.local/state/vouch
~/.config/vouch/bin/vouch install --host codex --shell powershell \
> ~/.local/state/vouch/codex-hooks.json
# or: --shell bash
codex mcp add vouch_approval -- ~/.config/vouch/bin/vouch-codex-broker
Review the private codex-hooks.json, then save it as ~/.codex/hooks.json. Codex gets
PreToolUse for decisions and PostToolUse for outcomes. The broker is what
turns an approved native MCP prompt into one exact retry; it stores hashes and
a short reason category, never raw command text or session IDs. Keep
approval_policy = "on-request" and approvals_reviewer = "user" at the top
level, and add default_tools_approval_mode = "prompt" inside the existing
[mcp_servers.vouch_approval] section. That native MCP prompt is the human
decision: denying it leaves the Ask blocked, while approving it lets the broker
validate the pending request and mint the bound one-use grant. There is no
second, nested elicitation.
To observe Codex while leaving approvals_reviewer = "auto_review" and the
native policy in charge, use passive shadow instead:
~/.config/vouch/bin/vouch install --host codex --shell powershell \
--shadow --state-dir ~/.local/state/vouch \
> ~/.local/state/vouch/codex-hooks.json
# or: --shell bash
This route needs no vouch approval broker. PreToolUse evaluates and appends a
host = "codex", mode = "shadow" row while emitting nothing; PostToolUse
appends the matching host-attributed outcome. The explicit absolute state
directory keeps both rows in one durable journal even when Codex starts in a
different repository. New outcomes correlate by host plus tool-use id, so a
Codex id cannot attach to a Claude row.
Codex hooks cover shell commands, apply_patch, MCP tools, and most local
function tools. Hosted tools and some specialized paths can bypass the local
hook path, so vouch is a guardrail over observed calls, not a replacement for
the native sandbox.
Add --shadow to register vouch beside a gate you
are still running: it evaluates and journals every call in full and emits no
decision, so you can measure what it would have done before it does anything.
Every machine — the one that develops vouch included — runs the released
plugin and the released binaries as its primary install; a dev build proves
the install path and may run mid-development, but the machine settles on the
release. Keep one skill route at a time: the plugin cache is version-keyed,
so holding plugin skills and scripts/install-skill.sh copies together means
two different texts of the same skill under two different names. Use
install-skill.sh only where unreleased skills must load.
Version numbers and CHANGELOG.md are generated from structured feat/fix
entries recorded in each release-bearing commit. The private subject summarizes
the changeset; one nested entry names each independently visible public outcome.
Each release lands as one reviewed change, and the public-mirror tag that
follows triggers the build workflow to attach the binaries.
The mirror publisher scans the configured Git identity and the exact candidate
message, metadata, paths, patch, and files before it pushes a review branch.
After review, --land rescans that branch and fast-forwards mirror master to
the same commit; the forge merge button is not used because it creates a new,
unscanned object. --verify also audits published author, committer, and tagger
identities before --tag can start a release build.
gh release download fetches a per-platform archive holding both binaries,
knowledge.toml, and vouch.example.toml from a single commit, laid out to
match where they are installed.
The harness calls vouch --hook, which reads hook JSON on standard input.
Everything else is for you:
| Command | What it does |
|---|---|
vouch explain '<cmd>' |
Decides a command now and prints the verdict and the whole reason, without running it. Says which directory it judged from; --cwd <dir> sets that directory |
vouch why |
The last recorded decision — verdict, reason, mode — then re-decides that same command under the current config, so you can see whether the two disagree. vouch why '<cmd>' decides that command instead |
vouch trust <program> [<verb>…] |
Writes a recognition entry to my-knowledge.toml; running it is the explicit accept. Verbs are named one by one, --all-subcommands covers a whole program, --whole-server a whole MCP server — the wide ones have to be said out loud |
vouch doctor |
What vouch could not read or describe: place rules that can never fire, my-knowledge.toml lines the merge discarded, commands it could not parse, programs it has no description of, and undeclared options on directory-changing programs, by count and by spelling |
vouch review [--accept <name>] |
Rule candidates drawn from recorded outcomes, each with the counts behind it, including the ones it will not propose and why. Prints only; --accept is the one thing that writes, and it never proposes a guard |
vouch import [file] |
Translates a cc-allow config to standard output and lists on standard error what did not translate. Writes nothing |
vouch install [--host claude|codex] [--shell bash|powershell] [--state-dir <absolute>] [--shadow] [--print] |
Prints the selected host's merged hook document for redirecting and saving. Codex requires an explicit shell; --state-dir is Codex-only and makes decision/outcome journaling durable. Live Codex notes include the broker route; passive --shadow notes leave the native reviewer unchanged and require no broker. --print narrows output to the hooks-only view. Writes nothing |
vouch schema <config|knowledge> [--write] |
Prints the JSON Schema generated from the structs the loaders actually read; --write regenerates the committed schemas and the reference page |
Four invariants are not up for negotiation, because each one is what makes the rest worth trusting:
- Nothing is ever written to a config or knowledge file automatically. Rules change on an explicit accept, and no other way.
- An approval is never a policy. Approving a guard prompt lets one command through; it does not create a rule. That is why guards ask every time.
- A guard never hides where it came from. The shipped knowledge declares the common effects, and a number of those rules are inferred rather than asked for — so every prompt names which, and deciding that a shape is dangerous stays your call. Where something is a write channel rather than a write, vouch reports it and lets you decide.
- Absence of knowledge is not permission. The one exception is a trust zone, which you have to write out loud in your own config, and which grants recognition only — guards and write rules still apply to whatever runs there.
Maturity, plainly: this is one operator's tool on one machine. Its knowledge
file, its defaults, and its measurements all come from that machine's own
recorded history, so a second machine will find gaps — that is what
vouch doctor and /vouch:setup are for, and an unrecognised program is a
prompt to be closed by describing it, not a bug. Releases build for Windows
(x86-64), Linux (x86-64) and Apple silicon macOS; there is no Intel macOS
build.
Licensed under the WTFPL — see LICENSE.
This repository carries the product: everything needed to use, build, and release vouch. Development itself — the ordered roadmap, the approved designs, the process docs, and the measurements over real recorded traffic — lives in a private companion repository, because vouch's method is measuring against real machine history and that history never leaves the machine that produced it.
Two things to know before running anything here:
- The committed test corpus is hand-authored and invented, so a fresh
clone is green:
cargo test --releaseruns the full property suite over it. The measurement tests that need real recorded history skip when it is absent rather than falling back — a count over invented commands would be a fabricated number. - Nothing personal or secret goes in this repository. The development side enforces that with git hooks and a six-place history audit before anything is published here.