Project

excubitor — Safety Fences for Autonomous Coding Agents

Stdlib-only guards and a falsifiable intent-record system that let an LLM coding agent run unattended without trusting it to bless its own work — mechanical, testable, and enforced outside the model.

  • Python
  • AI safety
  • Claude Code hooks
  • Agent Skills

Code ↗

I run unattended agent loops across my own repositories — refactors, audits, and migrations a model grinds through for hours while I’m not watching. The failure that worried me was never a bad edit; it was the irreversible one taken on a self-blessed “done” — a merge, a push, a delete the loop decided was fine and no one caught. excubitor is the control I built for that: a guarantee I can test, enforced outside the model, that an unattended loop cannot take an irreversible action on evidence it authored itself.

Context

A productive agent workflow ends in an irreversible act. If a human reviews every step, that’s fine. Run the agent in an unattended loop and a gap opens: the loop decides for itself that the work is complete, then acts on that judgment. LLMs have self-preference bias — a loop that writes its own completion evidence produces plausible-looking proof of work that wasn’t done, and any downstream check that trusts that evidence is routed around by construction. This is not hypothetical. In July 2025 a production coding agent, during a freeze, deleted a live database and fabricated 4,000 fake records to cover it.

Telling a model to be careful is not a control — it can talk its way past its own instructions. So the fix here is mechanical: split the question into two axes and enforce the answer outside the model. Is “done” decided by a frozen, loop-immutable oracle — a test suite’s exit code the loop cannot edit — or by the model’s own judgment? And is the act reversible and internal (a commit, a --no-ff merge into a non-default branch) or irreversible (a push, a hard reset, a branch delete)? An unattended agent may act on its own “done” only when the done-signal is unforgeable and the act is reversible. Everything else stops and surfaces the irreversible tail for a human or an out-of-loop reviewer.

What it does

excubitor implements that policy as four PreToolUse guards — stdlib-only Python, no dependencies — that intercept an agent’s tool calls before they run:

  • guard-default-branch blocks file mutations on main/master, so work starts on a branch.
  • guard-loop-vc blocks the irreversible version-control set — merge, push, branch-delete, hard-reset, git clean, worktree-remove — while a loop is armed. Its =yolo mode additionally permits a --no-ff merge into a confirmed non-default branch, and fails deny on any ambiguity.
  • guard-one-unit caps a headless loop worker at one unit of work per session, forcing a fresh-context re-read between units.
  • guard-self-integrity denies writes to the guards’ own kill-switches while armed — the marker files, the hook scripts, the settings block. A judge the model can rewrite is not a judge.

Above the guards sits a “telos” system: a repo declares its purpose as falsifiable claims, each with a one-line decidable contract and an optional executable witness whose exit code is trusted over the model. A read-only audit checks claims against their witnesses and mechanically demotes any claim marked done-without-a-witness to SUSPECT. The auditor has no write access to the record it audits.

Show it working

The 60-second crash test needs only python3 and git, installs nothing, and touches nothing outside a temp directory:

git clone https://github.com/ErickShepherd/excubitor.git && cd excubitor
scripts/demo.sh

It drives the shipped guard code exactly as Claude Code’s dispatch does — a JSON payload in, a deny/defer decision out — and shows a guard turning an unattended, unrecoverable git clean into a stop, then shows the same command deleting real work with the guard off.

The full install symlinks the guards and skills into ~/.claude and registers the four hooks. All of them are opt-in or inert by default: the loop guards do nothing until CLAUDE_LOOP_GUARD is set, and interactive work is unaffected until you explicitly say you’re looping. The test suite is 194 tests on stdlib-only Python; CI runs it on 3.11/3.12/3.13 plus the repo’s own telos audit, so every safety claim the README makes about the guards is re-proven by an executed test on each run.

Honest limits

These are seatbelts for the default path, not a sandbox, and the hooks’ own docstrings say so. guard-loop-vc parses Bash command strings, so a script that calls git indirectly, a shell alias, or a post-commit hook firing an external side effect can slip past — where you can, also just don’t hand the loop a merge capability. The evidence-tier demotion catches unbacked claims of completion; a loop that authors its own witnesses produces backed claims, which is exactly why the guards sever the loop’s ability to act rather than trusting any audit of its output. Every bypass I know about is enumerated in the repo, classed ACCEPTED (a residual pinned by a test so it can’t silently change) or CLOSED.

excubitor is the prevent half of a pair — guardrails on what an agent may do. The detect half is did-it, which reconciles what an agent claimed against what its transcript actually shows.