writ — why read the whole diff when only the drift matters?
Today an agent writes code, opens a PR, and a human reads a large diff to decide whether to merge. That does not scale: the diff is too big to read and arrives stripped of the context that produced it.
writ replaces that. A writ is the agreed scope of a piece of work, approved before any code exists: an intent, a set of checkable acceptance criteria, a declared file scope, and a verification command. The agent implements against it. writ then computes what actually changed, compares it against the declared scope, runs the verification, and shows the human only what fell outside the writ - the drift. Zero drift, plus every criterion met, plus a green verification, merges with no human at all.
A writ that declares its scope as the whole repository makes drift meaningless, so writ refuses to accept one - at intake and again before any status or merge decision.
Install • The loop • Agent rules • Completions • Exit codes • FAQ • Contributing • License
brew tap laaaaksh/writ
brew trust laaaaksh/writ
brew install Laaaaksh/writ/writ
The brew trust step is required once: current Homebrew refuses to load formulae from
third-party taps it has not been told to trust.
writ ships for macOS and Linux. Its only runtime requirements are git,
which every command uses to compute drift and merge, and a POSIX sh, which
runs verification commands and $EDITOR launches. Alternatively, with a Go toolchain:
go install github.com/Laaaaksh/writ/cmd/writ@latest
writ generates tab-completion scripts for bash, zsh, fish, and PowerShell;
writ completion <shell> --help prints full per-shell instructions.
# zsh - current session; append the line to your .zshrc to keep it
source <(writ completion zsh)
# bash - current session; append the line to your .bashrc to keep it.
# eval is used because macOS's default bash (3.2) cannot source from
# <(...); results require the bash-completion package.
eval "$(writ completion bash)"
# fish
writ completion fish > ~/.config/fish/completions/writ.fish
1. writ propose - the agent drafts a complete writ and proposes it, before writing any
code:
$ writ propose <<'EOF'
id = "writ-1"
intent = "add a retry to the webhook sender"
base = "main"
created = 2026-01-01T00:00:00Z
scope = ["internal/webhook/**"]
[[criteria]]
id = "retries-on-5xx"
text = "a 5xx response is retried with backoff"
[verify]
command = "go test ./..."
EOF
--file <path> reads the writ from a file instead of stdin. writ validates it and writes it
to .writ/current.toml, unapproved. Criteria must arrive unassessed: a draft carrying met
or an attestation is refused, because claims are recorded with writ attest only after a
human approves. Drafting a concrete proposal - not hand-authoring path globs from a blank
file - is the agent's job, because a lazy scope like app/** makes drift meaningless.
propose also keeps writ's bookkeeping out of your history: when no ignore rule covers
.writ/, it seeds the repo-local .git/info/exclude, so committing your work wholesale
(git add -A) can never track the state file - and if it does end up tracked anyway,
status and merge refuse with instructions to untrack it.
2. writ approve - a human reviews the proposal, opening it in $EDITOR to tighten scope
or criteria before agreeing to them. On save and exit, writ re-validates; if invalid, it
prints the problems and leaves the file in place. --yes approves the proposal as-is,
skipping the editor.
3. Implement - the agent creates a branch off base (git checkout -b <branch>) and
does the work described by the writ there, committing as it goes: merge refuses a dirty
working tree, and commits made on base itself are invisible to drift.
4. writ attest <criterion-id> --note "<how>" - the agent (or a human, with --human)
claims that a criterion is met and records how. This is a claim, not a fact: writ status
renders it as "claimed by agent", visibly distinct from criteria a human confirmed or
evidence that ran and passed. Attesting requires an approved writ - claiming against a
contract nobody agreed to is meaningless. writ unattest <criterion-id> clears it.
5. writ status / writ merge - status computes drift, runs the verification command,
and renders the decision: zero drift, a green verification, an approved writ, and every
criterion attested auto-merges with no human; anything else names the reasons a human is
needed. merge does the same and, if mergeable (or given --approve), merges the writ's
branch into base and clears .writ/current.toml.
Run both from the writ's own feature branch, not from base: writ refuses them while HEAD
is on the base branch itself, where nothing can be merged and commits on base would be
invisible to drift. Linked git worktree checkouts work too, but merge additionally
needs base not to be checked out anywhere else - git allows a branch in only one worktree -
so writ refuses with the command to detach or remove the checkout holding it.
The verification command runs through sh -c in the repo root; its exit code alone decides
pass or fail. It is killed after 10 minutes by default - set WRIT_VERIFY_TIMEOUT (a duration
like 45s or 30m) to change that; invalid or non-positive values fall back to the default.
When a proposal is rejected or abandoned, writ discard removes the open writ so propose
can start fresh; it touches only .writ/current.toml, never branches or commits.
A full loop, as status sees it at the end of a real run - the agent's claim on record,
verification green, nothing outside the declared scope:
$ writ status
writ add a retry to the webhook sender
CONTRACT 1/1 criteria
retries-on-5xx claimed by agent "injected a 500 then watched the sender retry twice before succeeding"
EVIDENCE go test ok
IN SCOPE 1 files
DRIFT none
Auto-mergeable: zero drift, verification passed, all criteria met.
$ writ version
Prints the writ CLI version; writ --version (or -v) prints the same line.
writ only works if the agent actually follows the loop, and agents follow what their
instruction file says. Paste this into your repo's AGENTS.md or CLAUDE.md:
## Writ discipline
For any non-trivial change, drive the writ loop instead of free-form editing:
1. Propose before code. Read the relevant code first, then draft a complete
writ - intent, checkable criteria, the narrowest honest file scope, and a
verification command - and pipe it to `writ propose`. Never invent path
globs from memory: a lazy scope like `app/**` makes drift meaningless.
2. Wait for approval. Never implement against an unapproved writ. The human
runs `writ approve`; proceed only once it succeeds. `--yes` is the
human's call, never yours.
3. Stay inside the declared scope. Work on a branch off `base`. If the work
turns out to need files outside the writ's scope, stop: `writ discard`,
draft a new writ that covers reality, and propose again. Quiet scope
creep is exactly what writ exists to catch.
4. Attest only what you checked. Run each criterion's check yourself, then
record it: `writ attest <criterion-id> --note "<what you observed>"`.
Never pass `--human`: that records a human confirmation, and only humans
may make one.
5. Let exit codes decide. Run `writ status` from the feature branch. Exit 0
means it will auto-merge - say so and stop. Exit 1 means a human is
needed: print writ's reasons verbatim and wait. Never edit
`.writ/current.toml` by hand; use `attest`, `unattest`, or `discard`.For scripting and agents, writ signals its decision on the process exit code:
0- success; forstatus, the writ is auto-mergeable, and formerge, it merged1- a human is needed (always fromstatus; frommergeunless--approvewas given), or any other error occurred2- no writ is open (from any command that reads one:approve,attest,unattest,status,merge,discard)
Output stays script-friendly: ANSI color appears only when stdout is a terminal,
and setting NO_COLOR (to any non-empty value) turns it off even then, so piped
or captured output is always plain text.
Does writ need an AI subscription or API key?
No. writ never talks to a model provider: there is no HTTP client anywhere in it or in its
two dependencies, it reads no API keys from your environment (the only variables it touches
are EDITOR, VISUAL, NO_COLOR, and WRIT_VERIFY_TIMEOUT), and its only child processes
are git and a POSIX sh. All the intelligence lives in whatever coding agent you already
run; writ is just the contract both of you sign and the referee that checks the work.
Which agents can drive it?
Any agent that can write TOML text and run shell commands: Claude Code, Codex CLI, opencode, Cursor, aider, a cron script you wrote yourself. There is no plugin to install - the whole interface is stdin, stdout, and exit codes.
Can several agents work in one repo?
Not on one writ. State lives in a single .writ/current.toml per repository, and a second
propose refuses while one is open. Give each parallel agent its own checkout with
git worktree add ../repo-agent-b; each worktree holds its own open writ.
Should my parallel agents share my API key or subscription?
No. N agents behind one key multiply your request rate N-fold, so when the provider answers HTTP 429 (rate limited), all of them hit the wall at once - and each agent's retry logic then fires together, producing a retry storm that can burn through quota or get the key throttled entirely. Put each worktree on its own key or subscription so agents fail independently instead of failing together.
If writ makes reviewing agent PRs tractable, leave a star - it helps other people find it.
MIT - see LICENSE.