Excubitor — Latin, "one who stands out of bed": the night watch. The Byzantine excubitores guarded the emperor's chambers precisely so that no one else had to stay awake.
Safety fences for autonomous coding agents — mechanical guards, loop discipline, and a falsifiable intent-record system for letting an LLM agent work unattended without trusting it to bless its own work. The watcher that stays awake while the loop runs and nobody else is looking.
Stdlib-only Python hooks + Agent Skills-format capability
packets, each mechanism pinned by executed regressions and shipped with the design rationale that
produced it. Built for and battle-tested
with Claude Code; all four guards now share a model-blind policy core (excubitor/core/) — the
shipped hooks are thin Claude Code adapters over it, and a second, non-Claude-Code adapter drives the
same core with an equivalence test to prove it (SPEC.md), so its portability to any
runtime that can intercept tool calls is demonstrated, not just asserted. Claude Code has the only
available adapter and installer foundation today, but no runtime is yet claimed as verified supported
enforcement: that label still requires a released-package, real-host denial witness.
scripts/demo.sh # needs only python3 + git; installs nothing, touches nothing outside a temp dirIt drives the shipped guard code exactly as Claude Code's PreToolUse dispatch does (a JSON
payload in, a deny/defer decision out), shows a guard turning an unattended, unrecoverable
git clean into a stop, then shows the same act deleting real work with the guard off. The image
above is a static frame of that run, regenerated byte-for-byte with
NO_COLOR=1 DEMO_PAUSE=0 DEMO_STABLE_PATH=1 bash scripts/demo.sh | scripts/render_demo_svg.py > scripts/demo.svg(an animated GIF is available via scripts/demo.tape + vhs).
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. Telling a model to be careful is not a control: it can talk its way past its own instructions, and a production agent did exactly that (see the Replit case study). So I built the control I actually wanted — a guarantee I can test, enforced outside the model, that an unattended loop cannot take an irreversible action on evidence it authored itself — and, just as deliberately, an honest map of where that guarantee stops. If the argument below is right, the code is almost the easy part.
A productive agent workflow ends in an irreversible act — a merge, a branch delete, a push. If a human reviews every step, fine. But run the agent in an unattended loop and a failure mode opens up: the loop decides for itself that the work is complete, and 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.
The fix is not "prompt it to be careful." The fix is mechanical: split the question into two independent axes and enforce the answer outside the model —
- Axis A — done-signal trust. Is "done" decided by a frozen, loop-immutable oracle (a test suite's exit code the loop cannot edit — unforgeable), or by the LLM's own judgment (forgeable)?
- Axis B — blast radius. Is the act reversible and internal (a commit; a
--no-ffmerge into a non-default branch, undoable withgit revert -m 1), or irreversible/external (push, hard-reset, branch delete, default-branch merge)?
An unattended agent may act on its own "done" only when the done-signal is unforgeable and the act is reversible. Everything else is stop-and-surface: keep working, keep committing, then stop and hand the irreversible tail to an out-of-loop reviewer or a human. This repo is that policy, implemented as code you can read, test, and install.
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 — both axes failing at once.
docs/case-studies/replit-2025.md reads that incident through
the two axes (and notes, honestly, where excubitor's git-scoped guards would and would not have
helped).
| Layer | Component | What it enforces |
|---|---|---|
| 1. Declare intent | skills/telos/ |
A repo's purpose as falsifiable claims — each with a decidable one-line contract:, a single discharged-by: path::symbol, and optionally an executable verified-by: witness whose exit code is trusted over the LLM. |
| 2. Audit against intent | skills/audit-telos/ + telos_check.py |
Read-only conformance audit with evidence tiers: a claim marked DISCHARGED without a backing witness is mechanically demoted to SUSPECT. The auditor deliberately has no write access to the record it audits. |
| 3. Fence the loop | hooks/ — four PreToolUse guards |
guard-default-branch.py: blocks the runtime's direct file-edit tools (Edit/Write/NotebookEdit) on main/master — branch first. Bash-driven mutations (sed -i, redirections, formatters) are outside its registration and are a named residual in KNOWN-BYPASSES.md. guard-loop-vc.py: while CLAUDE_LOOP_GUARD is set, block the irreversible VC set (merge/push/branch-delete/hard-reset/git clean/worktree-remove/gh pr merge, plus the origin/HEAD trust-anchor rewrites remote set-head/symbolic-ref); in =yolo mode, additionally allow a --no-ff merge into a confirmed non-default branch (fail-deny on ambiguity). guard-one-unit.py: cap a headless loop worker at one unit of work per session, forcing a fresh-context re-read between units. guard-self-integrity.py: while armed, deny writes to the guards' own kill-switches (the .claude/allow-default-branch marker, the hook scripts, the settings.json hooks block, and the excubitor/ policy-core package the guards import at runtime — so the extraction of the decision logic didn't become a disarm path) — a judge the model can rewrite is not a judge. Every deny is also appended, best-effort after the decision is emitted, to a local JSONL telemetry log (hooks/_denial_log.py, default ~/.claude/excubitor/denials.jsonl) — an audit trail of what an agent tried while fenced; observability, not tamper-evident (see KNOWN-BYPASSES.md). |
| 4. Loop discipline + boundary | skills/ralph-loop/, skills/telos-loop/, skills/leak-guard/ |
The unattended-loop recipes that use layers 1–3: charter-driven iteration, frozen-oracle verification (run_frozen_oracle.py binds the witness verdict to baseline-authored, trusted state — anchor + base pin, executable trust, verdict-surface companions, then precheck, snapshot, shell-less sanitized-env run, recheck; check_oracle_frozen.py is the check-only diagnostic), session-limit suspend, and a fail-closed guard for content crossing a private→public boundary. |
The design docs under docs/design/ are not an afterthought — they are the
recorded deliberations (alternatives, rejected options, honest limits) behind each mechanism.
Start with loop-yolo-verifiable-autonomy.md
(the two-axis model above) and
loop-telos-anchor-deliberation.md (why a loop
must never discharge its own claims).
excubitor/core/ # the model-blind policy core (stdlib-only, no host I/O): events,
# git_state, policies/{loop_vc,default_branch,one_unit,self_integrity},
# dispatch (deny precedence), shell + tests
excubitor/adapters/ # per-runtime adapters: claude_code.py (shared PreToolUse envelope glue)
hooks/ # the 4 Claude Code guard entry points — thin adapters over the core —
# + denial-telemetry log + the differential-oracle tests
skills/
telos/ # intent-record authoring (write side)
audit-telos/ # conformance audit (read side) + strict parser + tests
telos-loop/ # telos-anchored unattended loop recipe
ralph-loop/ # charter-driven loop + oracle freeze/run + suspend scripts + tests
leak-guard/ # private→public boundary guard + leak_check.py + tests
runtime/spec_adapter.py # the generic excubitor.pre_tool.v1 adapter (portability, proven) — see SPEC.md
docs/design/ # design/deliberation records
docs/telos/ # this repo's own intent record (audited by its own tooling)
scripts/install.sh # symlink skills+hooks into ~/.claude, register the hooks
scripts/install_settings.py # tested exact-tuple settings.json registration (used by install.sh)
scripts/demo.sh # 60-second zero-install crash test (drives the real guard)
Requires Python 3.11+ and git. For Claude Code:
git clone https://github.com/ErickShepherd/excubitor.git && cd excubitor
scripts/install.sh # symlinks skills/* and hooks/* into ~/.claude, and idempotently
# registers the four guards in ~/.claude/settings.jsonOr register the hooks by hand in ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{"matcher": "Edit|Write|NotebookEdit",
"hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/guard-default-branch.py", "timeout": 10}]},
{"matcher": "Bash",
"hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/guard-loop-vc.py", "timeout": 10}]},
{"matcher": "*",
"hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/guard-one-unit.py", "timeout": 10}]},
{"matcher": "Bash|Edit|Write|NotebookEdit",
"hooks": [{"type": "command", "command": "python3 ~/.claude/hooks/guard-self-integrity.py", "timeout": 10}]}
]
}
}All four guards are opt-in or inert by default: guard-loop-vc.py and
guard-self-integrity.py do nothing unless CLAUDE_LOOP_GUARD is set in the loop's environment;
guard-one-unit.py does nothing unless a loop driver arms ONE_UNIT_CAP_SCOPE +
ONE_UNIT_CAP_BASELINE; guard-default-branch.py can be disabled per-repo with a
.claude/allow-default-branch marker file or globally with CLAUDE_ALLOW_DEFAULT_BRANCH=1.
Interactive work is unaffected until you explicitly say "I'm looping."
python3 -m venv .venv && .venv/bin/pip install pytest
.venv/bin/pytest -q # the full suite: hooks, installer, audit-telos, ralph-loop, leak-guard,
# runtime (a few audit-telos ledger round-trip tests skip — they need a
# private sibling module that did not ship, see the extraction notes)CI runs the same suite on a stock GitHub runner (.github/workflows/ci.yml), plus this repo's
own telos audit — every claim in docs/telos/app.md must resolve
DISCHARGED at the witness evidence tier, i.e. every safety claim this README makes about the
guards is re-proven by an executed test on every CI run.
Branch-first: never edit or commit on the default branch — create a branch, commit in focused
units as you go, and gate integration on an independent fresh-context review. The guards enforce
the mechanical edges of that workflow (layer 3 above); the review itself is a process convention
(see pre-merge-review in the glossary). Unattended loops get the stricter posture: with
CLAUDE_LOOP_GUARD=1 the loop can work and commit but never integrate; with
CLAUDE_LOOP_GUARD=yolo it may integrate reversibly only when a frozen oracle — not the
model — says the work is done.
These are seatbelts for the default path, not a sandbox — the hooks' own docstrings say so,
and mean it. THREAT-MODEL.md lays out the full picture: three attacker tiers
(sloppy loop → optimizing loop → adversarial model), the controls matched to each, and the
host-runtime enforcement gaps a hook layer inherits (e.g. PreToolUse hooks not firing on subagent
tool calls). In short:
guard-loop-vc.pyparses Bash command strings. A script that calls git indirectly, a shell alias, or apost-commithook firing an external side effect can slip past. Where possible, also simply don't hand the loop a merge capability.- On a local-only repo with both
mainandmaster(or a non-standard trunk), default-branch detection is a best-effort heuristic; YOLO mode compensates by always protecting the literalmain/masternames and failing deny on ambiguity, and the residual exposure is bounded to a revertable--no-ffmerge. - The evidence-tier demotion (layer 2) catches unbacked claims of completion. A loop that authors its own witnesses produces backed claims — which is exactly why layers 3–4 sever the loop's ability to act, rather than trusting any audit of its output.
Every bypass we know about is enumerated in KNOWN-BYPASSES.md — classed
ACCEPTED (a residual we won't chase, pinned by a bidirectional test so it can't silently change) or
CLOSED (fixed because it let a loop rewrite a guard's own judge). A hidden limit would itself be the
kind of forged safety claim this repo exists to refuse. In this project a guard bypass is a
security bug, and so is an overstated safety claim — SECURITY.md says what counts
and how to report it.
RELATED-WORK.md positions excubitor honestly against the fast-growing
agent-safety literature — injection defenses (VIGIL), probabilistic behavioral contracts, policy-DSL
enforcers, verified code generation — including what those systems have that this one does not (formal
guarantees, benchmarks). The short version: excubitor's distinct slice is **completion-claim integrity
- the irreversibility boundary for unattended loops**, enforced deterministically, witness-backed, with the enforcement layer fencing its own integrity — the assumption ("a judge the model can rewrite is not a judge") that most runtime-enforcement work assumes away.
This library is extracted from a larger internal harness; the shipped files occasionally reference siblings that did not ship. One line each, so nothing dangles:
pre-merge-review— spawns a fresh-context, read-only, highest-capability reviewer over a completed branch's full diff; merges only on its explicit sign-off (fail-closed).grill-me— one-question-at-a-time interrogation discipline used to sharpen a vague intent into a decidable contract before it is written down.deliberate— structured multi-perspective deliberation for contested decisions.audit-repo/audit-remediate— whole-repo audit sweep and its serial remediation counterpart (audit-telos is the purpose-conformance member of that family).threat-model— assets/attack-surface/proportional-controls reasoning; leak-guard defers to it for how hard to guard.automated-testing,logging,handoff— testing, no-secrets-in-logs, and session-handoff conventions referenced in passing.check-usage— subscription usage reader; its 87-lineclaude_usage.pymodule is vendored atskills/ralph-loop/scripts/so the session-limit suspend gate works standalone.
This is not a demo repo. Its mechanisms are extracted from the harness I actually run: the git guards
here are the same code as the live PreToolUse hooks in my own Claude Code environment (the three
original guards are symlinked from the upstream harness and fire on every commit my agents make; the
fourth, guard-self-integrity, and the hardening docs were authored for this publication). The telos
intent-record system is likewise in active use across seven of my other repositories, not
just this one; each keeps its own docs/telos/ record audited by this same tooling. To be equally
clear about what this is not: excubitor has no external users, stars, or third-party adoption. The
evidence here is that the author runs it, tests it, and audits it — not that a community has.
This repo is a curated extraction of the agent-governance subset of that harness, with a fresh history, re-published manually when the upstream changes materially. It describes what the harness looked like as of this snapshot rather than pretending to be a live mirror.
MIT — see LICENSE.