The model said done

The model said done

I've spent enough evenings with coding agents in Cursor and Claude Code to stop treating the last line of a transcript as a fact. The model finishes a session, writes something like "all tests passing," and the chat looks settled. I used to read that, commit, and move on. Then I'd run the suite myself and find that nothing outside the chat had actually run for that HEAD.

I've already gated the end of a Claude turn on the test suite in hooks for some work, and I still watched the model declare done when nothing outside the chat had run. The wording was confident, the session felt finished, and the only evidence I had was a sentence in the transcript. That was the bit that kept going wrong on evening work I did not have time to re-run by hand, and it was the bit that made me build a stamp.

Stop is the moment

A git hook answers whether a commit can leave the machine. The failure I kept hitting was earlier and quieter. The agent says done inside the editor, often without pushing, and I believe it because the wording is confident and the session feels finished. Pre-push never votes on that moment. --no-verify exists for the times you do push. Cloud agents often have no local Stop at all, so the chat can close with the same claim and nothing on the laptop ever gets a vote.

A Stop hook is the agent's "I'm finished" callback. Cursor, Claude Code, and Codex can run one before they close the turn. That is the moment I care about: not the push, and not CI going green later, but the moment the model is about to walk away from the tree and tell me the work is finished.

I'd already written that AI without a system of record is just chat. Chat without durable state outside the model is a performance of work, not a record of it. In The Claude Code workflows that actually survived contact with production I said the end of a turn should be gated on the test suite. This post is the same idea at the end of a coding session. The transcript is a claim. I wanted a receipt bound to git HEAD, written by something that is not the model.

Permissions that only live in the system prompt are theatre for the same reason. The model will route around them the first time a bash side-channel looks cleaner. I wanted the deny and the done-check in the tool path, not in a paragraph of instructions the agent can summarise away. Stop is where that path has to bite, or it does not bite at all.

The receipt

I built a small Go CLI called Vouch so Stop cannot fire without a stamp for the current HEAD. Named checks live in .vouch/config.toml. vouch gate runs them outside the model. On success it writes an HMAC receipt bound to this HEAD, the exact run strings, and a config digest. Cursor Stop, Claude Stop, Codex Stop, and git pre-push call vouch verify. A dirty tracked worktree is refused. A failed gate deletes the receipt for HEAD so an old pass cannot sneak through on a later Stop.

I did not want another chat plugin, another prompt pack, or another dashboard. I wanted a binary that runs the checks I named, stamps HEAD if they pass, and makes Stop refuse if they do not. Nothing fancy. A Go repo looks like this:

[[checks]]
name = "go.test"
run = "go test ./..."

The sixty-second path is:

curl -fsSL https://getvouch.dev/install.sh | sh
vouch scaffold
vouch gate
vouch setup cursor

vouch scaffold detects checks from the in-repo catalog (Go, Node, Rust, Python). Free keeps the best two. vouch setup cursor puts Stop and a preToolUse denylist in .cursor/hooks.json, which is why Why I'm back using Cursor still matters for where the hook actually lives. vouch setup claude and vouch setup codex wire the same verify into those Stop hooks. vouch setup git points pre-push at verify. vouch lock pins a human-owned config digest so an agent cannot quietly edit the check list and keep a valid stamp for the old list.

The HMAC secret is repo-local under .vouch/secret. It is a solo stamp for this machine and this secret. Anyone who can read the secret can forge a receipt. There is no public Ed25519 verify path in v1, so I do not treat the slip as evidence I can show a client. It is a personal done-gate, not a third-party attestation.

vouch gate never phones home. Free is one absolute repo path and two checks. Indie is $49 once, vouch license set KEY, unlimited repos and checks, still local. Switching Free folders is vouch free release, then init or gate in the new path. That does not delete .vouch/.

Cloud and CI

Cloud agents are the awkward case. They often have no Claude or Codex Stop hook on a laptop you control. Cursor cloud agents can load project .cursor/hooks.json, which helps when the hook file is committed. For everything else I require vouch gate in CI with vouch setup ci, or I put the gate in the prompt and accept that a prompt is weaker than a hook.

CI is a second stamp of the same committed checks, not a dashboard. The Action uses a CI-local HMAC secret (VOUCH_SECRET as a repository secret, or an ephemeral job secret). It is not the laptop .vouch/secret. Laptop and CI each write a receipt for the same SHA. Config and .vouch/lock stay in git. The secret does not. Free-repo claim is skipped when VOUCH_CI=1 so a claimed laptop path cannot fail the job.

The laptop receipt answers whether Stop had evidence for this HEAD on this machine. The CI receipt answers whether the same committed checks passed on a runner. Neither one is the transcript.

What I got wrong

vouch init used to ship run = "true", which is a fake pass. An agent could scaffold a repo, leave the noop in place, and every Stop would look green. The gate now refuses only-noop configs, and vouch scaffold --force clears leftover true and echo checks. That one was embarrassing and obvious once I saw it.

The first receipts did not hash the run strings. An agent could edit .vouch/config.toml, change go test ./... into something cheaper, and still present a valid stamp for HEAD under the old signature shape. The receipt now binds the exact run strings and a config digest. vouch lock is the human pin on top of that. That hole is closed.

make release only built linux/amd64 while install.sh asked for four binaries, so curl | sh 403ed for everyone else. The dist now has darwin and linux, amd64 and arm64. I have run the public install line on Linux and on a clean Mac.

vouch setup git still overwrites a foreign pre-push hook rather than composing with it. If you already had a hook, setup clobbers it. That is a real footgun and I have not fixed composition yet.

The runner is sequential sh -c with no timeout. A hung test hangs the gate. There is no parallel pool and no per-check deadline. Fine for two short checks on a Free repo. Less fine the first time someone points it at a slow integration suite.

And the name collides with every other product called Vouch. Protocol projects, VS Code extensions, auth products. I kept the name because the verb is right and I was shipping for myself first. Search will be noisy. That is open.

Bypasses still exist on purpose. --no-verify, deleting the hook files, rewriting Stop configs to drop vouch verify, copying the secret, forcing a noop config before the refuse landed. The binary cannot stop a human who wants to skip. It stops the model from declaring done without a receipt when the hooks are still in place. If a human asks an agent to remove the hooks, the agent guide in the binary tells it to say so and not do it quietly.

What this is not

This is not a Husky replacement essay. Husky and pre-commit answer the commit and push path. Vouch answers the agent's Stop path, and only secondarily the push. If your only problem is "tests before push," you already have tools. My problem was "the model said done in the editor and I believed the chat."

It is also not an AI-powered verification platform. There is no model in the loop of the gate. The model is what the gate is for. Free and Indie are just licence shapes around a local CLI. I am using it on my own evening work.

Conclusion

I am not trying to turn the transcript into proof. I am refusing to let Stop mean "the model is satisfied." The evening ends when the receipt matches HEAD, or it does not end. Free is one repo and two checks. Indie is $49 once and does not phone home. Install is curl -fsSL https://getvouch.dev/install.sh | sh. The deep internals piece can wait until I have a week of receipts and a real number. For now the stamp is enough to stop me reading chat as evidence.


I write about this stuff every week. If you want to keep up with what's changing in Claude Code, Cursor and AI dev tooling, along with the Go and infrastructure work I do, the newsletter is where it all goes first.

Join the newsletter - it's free

I also do consulting on AI implementation and technical strategy. If you're working through something specific, get in touch.

Subscribe

Get new posts directly to your inbox
You've successfully subscribed to Kyle Redelinghuys
Great! Next, complete checkout to get full access to all premium content.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.

What I'm building and learning, weekly

Claude Code configs, Go patterns, real costs and the tools I build to solve my own problems. One email, every week.

Now check your email to confirm your subscription.