Standpat How it works Install

Standpat

The result can keep moving. The plan cannot.

Stand pat.

Put it through Standpat

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.

  1. 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/handbrake
    {
      "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"]
    }
  2. brake turns it on in a chat

    Every chat starts free and holds nothing. Type brake, name a goal, accept the handbrake's version. None of these words reaches the agent.

    In the chat
    › 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.
  3. 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.

    The agent's output
    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.
  4. stamp lets exactly that through, once

    One stamp allows that one action, for 30 minutes. Send the agent any message and it runs it. Nothing else gets through on it.

    In the chat
    › 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.
  5. park when you walk away

    Safe to leave. Anything held is not run.

    In the chat
    › 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: demo through Agent may not: The app and the budget to Done while parked: nothing and Refused 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.

In the chat
› 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.

Status line, endsused 62% · yours 25% · jobs 13% left

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.

Status lineStandpat · demo · moves 0 of 3 today · 0 of 240 min this sitting · nothing held · load 4 ■□□□□

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.

Your terminal
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:

Your terminal
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.

  1. Linux is untried. No one has run Standpat on Linux yet.
  2. 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.
  3. Merge reviews. Some ways of calling git merge (through env, command, its full path or sh -c) are not recognised as merges and run with no review stop. The handbrake's lines still hold.
  4. The garage during a park prints the park twice on its Goal line.