The problem
You stopped watching. The agent did not stop working.
A coding agent keeps going after you look away. Somewhere in that run it changes a thing you had already settled: a fee, a schema, a sentence the client signed off. You find out later, if at all.
Standpat is a brake for coding agents, run as a hook by the agent app. It holds any change to the lines you named until you type a word to let it through, and it keeps a log of the decisions you typed.
How it works
Name it, brake, hold, stamp, park.
Every reply below is Standpat's own, copied from a rehearsal. Lines marked › are what you type in the chat. Your times and versions will differ.
-
Name what must not change
Each repository gets a handbrake file,
.standpat/handbrake. You list the lines the agent may not change: a path, or a phrase.{ "standpat_format": 1, "project": "Play", "must_not_change": [ {"id": "app", "label": "The app", "matcher": "path", "pattern": "app.py", "class": "other"} ], "done": "", "who_may_widen": ["owner"] } -
braketurns it on in a chatEvery chat starts free and holds nothing. Type
brake, name a goal, accept the handbrake's version. None of these words reaches the agent.› brake Standpat: the handbrake is on in this chat from the agent's next action; type `free` to turn it off. Type `goal <name>` to say what this chat is for, then `handbrake accept <version>` to accept what must not change, then your message for the agent. Nothing was sent to the agent. › goal demo Standpat: goal demo is open. Type `handbrake accept <version>` to accept the handbrake, then your message for the agent. Nothing was sent to the agent. › handbrake accept <version> Standpat: handbrake version <version> accepted. Type your message for the agent. Nothing was sent to the agent.
-
The change is held, with its diff
Ask the agent to change
app.py. Its edit is not run. The agent's output shows what it tried, the line it crosses and the change itself.Standpat: hold 1, held for a stamp: Edit on app.py. Lines it crosses: The app. - print("hello") + print("hello, world") Cost: 1 handbrake move. Handbrake moves today so far: 0 of 3. Type `stamp` to allow exactly this, once, or `park` to stop.
-
stamplets exactly that through, onceOne stamp allows that one action, for 30 minutes. Send the agent any message and it runs it. Nothing else gets through on it.
› stamp Standpat: stamped Edit on app.py, held at <time>, crossing The app. The action is allowed for 30 minutes. Send the agent any message and it will run it. -
parkwhen you walk awaySafe to leave. Anything held is not run.
› park Standpat: parked; anything held for a stamp was not run, and this sitting used 1 handbrake move. When you are back, send your next message as usual and the card comes with it. Nothing was sent to the agent.When you come back, your next message goes through and the agent is told to show you the return card first. It runs from
Goal: demothroughAgent may not: The appand the budget toDone while parked: nothingandRefused while parked: nothing.
Also in 0.15.0
Slices, the reserve, the ledger, the letter.
Slices and review stops
Split a goal into slices, each with what done means. When the agent says a slice is done, it stops for your review, and any further change it tries is not run. accept closes the slice; redirect <what instead> sends the agent another way, and those words reach it.
› slices
Standpat review: slice 1 of 2, docs.
Why: the agent says the slice is done
Done when: notes.md says what app.py prints
Changed: notes.md
Shell commands run: 0
Stamped: nothing
Refused: nothing
Type `accept` to close the slice, or `redirect <what instead>` to send the agent another way.
The reserve
reserve 25 keeps 25% of the five-hour window for you; background jobs may use the rest. A job runner can ask before it starts. The usage reading comes only on a Pro or Max account, after the chat's first reply.
The decision ledger and load bar
Decisions, not hours. Each word you type is weighed and counted in the sitting; the status line shows the load. It counts decisions, not you, and refuses nothing. garage lists them.
The letter
The letter shows what was held, what was stamped, and what was refused. It checks both logs first, asks git what changed over the period, and lists every gap in the gate it finds. It is a personal record, not a client document.
python3 ~/standpat/standpat/standpat.py letter ~/standpat-play
A line whose changes were all stamped reads changed by stamped writes (see Stamped); any other change reads changed between the two dates.
On your machine
What it changes, and how it comes out.
Install changes three things, and only these:
~/.claude/settings.json, the agent app's settings file: one block of entries is added after yours (eight hook entries and a status line).- A hook file and a copy of the command line, in
/Library/Application Support/Standpat/<version>/, a folder owned by root, so the agent, which runs as you, cannot change the gate it has to pass. - A folder
~/.standpat, with Standpat's state and your user log.
Before it writes, install proves that taking its own lines back out of the new settings file gives your file back byte for byte. The one byte it adds to your part is a comma after the item its block follows. uninstall takes the entries out again under the same proof, and prints settings: restored byte for byte. You can check it yourself:
cp ~/.claude/settings.json ~/settings.before-standpat.json
# install, use it, then:
python3 ~/standpat/standpat/standpat.py uninstall
cmp ~/settings.before-standpat.json ~/.claude/settings.json
cmp prints nothing if you changed nothing else in the file. Your logs and the hook copy stay until you remove them; the install page says how.
Download
Standpat 0.15.0, a preview.
For a Mac with git and Python 3.9 or later. Install takes 5 minutes, and a walk through every step above takes 20.
Known limits
What it does not do yet.
- Linux is untried. No one has run Standpat on Linux yet.
- The always-on guard has blind spots. In a free chat, deleting by name a folder that holds Standpat's state is not refused. A second agent session started by a script in another language, or by a command built at run time, is not caught.
- Merge reviews. Some ways of calling
git merge(throughenv,command, its full path orsh -c) are not recognised as merges and run with no review stop. The handbrake's lines still hold. - The garage during a park prints the park twice on its Goal line.