Skip to content

Send Claude back once when a turn ends with unverified code - #3

Merged
gnapse merged 3 commits into
mainfrom
agents/2-gate
Oct 26, 2026
Merged

gnapse merged 3 commits into
mainfrom
agents/2-gate

Conversation

@gnapse

@gnapse gnapse commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Stack, 2 of 4. Based on #2 (agents/1-verify). Merge after it.

What it adds

  • The end-of-turn gate. Two Claude Code hooks in .claude/settings.json run scripts/guard.mjs. When a turn starts, it records a hash of every file. When Claude stops, if the turn changed code that pnpm verify hasn't passed, it sends Claude back once, naming the files. The second stop goes through, so Claude can stop on purpose and say what's unverified. Turns that changed nothing, changed only docs, or undid their edits pass. Any error lets the turn through with a message.
  • Ask first: ask rules for git push, gh pr merge and --no-verify. They prompt even in auto mode. They're a speed bump, not a lock.

Evidence

  • 15 tests in scripts/guard.test.mjs, in throwaway repositories, each seen failing against a planted bug.
  • Headless Claude Code sessions:
    • An unverified edit was sent back once, then stopped with an honest "not verified".
    • A verified edit went through.
    • git push, a dry-run push and gh pr merge were refused even with --allowedTools allowing them. A control run without the project settings ran them.
  • The review found that stop_hook_active is set by any Stop hook, which would have turned the gate off under loop plugins. The guard now uses its own once-per-turn record.

Known limits

Claude Code only. Two sessions in one checkout see each other's edits. A turn that moves to another checkout isn't checked, and says so.

cargo fmt --check failed on 8 spots in ai.rs and lib.rs. This commit
only reformats them, so the next one can add the check to
pnpm verify.
pnpm verify runs every check: both TypeScript projects, vitest, the
web build, and cargo fmt, clippy, and tests. It takes about 5 seconds
warm. It records a hash of the tree in .git/undertext/ only when
every check passes and no file changed during the run, so a later
hook can tell whether a change was verified. A docs-only change
needs no run. A change to CI files alone is never recorded.

- CLAUDE.md says what done means and to run pnpm verify.
- .github/workflows/verify.yml runs it on macOS for pushes to main
  and pull requests. The actions are pinned by commit.
- docs/agents.md says what is checked, what is only asked, and the
  known limits.
- scripts/verify.test.mjs runs copies of the scripts in throwaway
  repositories.
- .gitignore now covers .claude/settings.local.json. Claude Code
  writes it, and a change to it would void a pass.
Two Claude Code hooks in .claude/settings.json run scripts/guard.mjs.
When a turn starts, it records a hash of every file. When Claude
stops, it compares: if the turn changed code (not only docs) that
pnpm verify hasn't passed, it sends Claude back once, naming the
files. The second stop goes through, so Claude can stop on purpose
and say what is not verified. Turns that changed nothing, changed
only docs, or undid their edits pass. Any error lets the turn
through with a message.

The same file has ask rules for git push, gh pr merge, and
--no-verify, so Claude Code asks before it pushes or merges, also in
auto mode.

- The guard relies on its own record for "once", not on
  stop_hook_active, which any Stop hook sets.
- Records live in the shared git folder, one per session, with the
  checkout the turn started in.
- scripts/guard.test.mjs runs copies of the guard in throwaway
  repositories. The helpers moved from verify.test.mjs to
  test-repo.mjs; no test changed.
- Proven in headless Claude Code sessions: an unverified edit was
  sent back once, and push and merge were refused even when allowed.
@gnapse gnapse closed this Oct 26, 2026
@gnapse gnapse reopened this Oct 26, 2026
@gnapse
gnapse changed the base branch from agents/1-verify to main October 26, 2026 13:46
@gnapse
gnapse merged commit b070bc1 into main Oct 26, 2026
1 check passed
@gnapse
gnapse deleted the agents/2-gate branch October 26, 2026 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant