Dev tooling & process · In use across 35 projects · Private
Agent governance framework
A file-based governance framework for the AI coding agents I direct: one gated verb, autonomy that changes with operator presence, rulings you can look up by ID, and a pre-execution hook that refuses private material before it reaches a public repo or a deploy.
Status
In use; its files carry dates from 25 May to 10 Oct 2026, and its record is a dated, append-only history file (5,428 lines).
Version
Unversioned (not in git) · ClaudeBar v0.2.0
Licence
None granted; private and unpublished. ClaudeBar’s licence is undecided.
Runs on
Claude Code. The hook is registered at user level on the Mac, so it reaches every project there; the canon also carries Windows-specific procedure.
Stack
Markdown canon read by reference · Node.js tools (3, 1,002 lines) · Claude Code pre-execution hook and skills · git hooks · GitHub CLI · ClaudeBar (Swift/AppKit, launchd)
An agent session starts with no memory, so the documents are its memory. A working rule once dropped out of context during a compaction, so the rules moved into files read at the start of every session, kept outside every repo.
At a glance
projects governed: folders that carry their own project rules
35
synthetic hook events judged as designed: 8 refused, 5 allowed
13/13
rulings with stable IDs; the ID tool’s self-test holds 9 of 9 guarantees
489
the defect process: a 77 KB file read every session, now an 11.5 KB skill loaded on demand
−85%
How these were measured
35: a count of folders that carry their own project rules file, 34 at the top level of the projects folder and 1 nested.
13/13: 13 synthetic pre-execution events piped to tools/public_gate.mjs against throwaway scratch repositories. The set ran twice, the second time for this page; every verdict matched its design both times, at 45–153 ms per check.
489: node tools/rule_ids.mjs --audit over the portable rules (119 IDs; 37 of 37 sections with labelled history) and every project rules file (370 IDs in 9 files). --self-test exits 0 and writes nothing.
−85%: wc -c on the retired process file (77,335 bytes) and the skill that replaced it (11,526 bytes).
All run on 12 Oct 2026, read-only.
hook · public gate
# 8 of 13 events · 45–101 ms eachALLOWgit statusALLOWecho "… run deploy.sh later"↳ mentions a deploy; is not oneALLOWgit commit -m "Add greeting"↳ clean; repo assumed publicDENYgit add leak.txt &&
git commit -m "wip"↳ stages and commits in one callDENYgit add -A && git push↳ stages and pushes in one callDENYgit push --tags origin↳ bulk push: refs nobody namedDENYgit push origin archive-…↳ archive refs are refusedDENYgit commit -m "<name> …"↳ message names a protected namePUBLIC GATE: this call stages AND
commits, so the gate cannot see what
it would stage — the hook reads the
index before the command runs.
Nothing was checked and nothing ran.
Stage in its own call, read what
it staged, then commit.
Fig. 1Eight of the 13 synthetic events from a 12 Oct 2026 re-run against throwaway repositories, re-rendered from the gate’s output with commands abridged; all 13 verdicts came out as designed. The message at the foot is the gate’s refusal of the first DENY. The private name is replaced and the archive ref truncated.
The model
Three ideas carry the rest. Most of the model is instruction, not code; the next section shows which parts are enforced.
01 · Authorization
One gated verb
Commit freely, push on judgement, ship only on an explicit human decision. Ship means a deploy, any distributable build or a storefront upload: it reaches an audience and depends on machine state only the person at the keyboard can see. The yes is needed every time; a prior yes is never standing authorization, and the agent says back what it will publish first. One exception: an unattended run where deploys were explicitly authorized.
Four version fields each have a named owner. Five triage questions come before a risky change: what else reads it, can it fail silently, is it reversible, does it reach a user, does it touch hand-tuned work?
Attended, the agent asks and waits. Unattended, it never stalls on a question: it makes the call, records the decision and its reasoning where I will see it first, and takes the most reversible path when a fork is too big to commit to.
An unclear handoff defaults to attended. The prohibitions hold in both modes: no ship without an explicit yes, no destructive git operations, no skipped hooks. One stop word makes the agent read-only.
When a safeguard fails, a sharper version of it rarely helps. Vigilance becomes a rule, and a rule that can fail silently becomes structure.
The shared canon lives outside every repo and is read by reference, because git can’t stage what was never in the tree; a project’s own private notes still sit in its working tree, kept out of git by ignore rules. Where structure can’t reach, a fail-closed check refuses a file by where it sits.
Most of the framework works because the agent is told to follow it. Read from the settings files and the code, this is where instruction ends and enforcement begins.
Advisory
Instructions the agent reads and follows
The authorization model and the ship decision
Autonomy modes and the stop word
Risk triage before acting
Version ownership
Decision tiers in code
0 deny or ask rules across 25 harness settings files; nothing in code requires a human yes before a deploy.
Mechanical
Code that refuses
The pre-execution hook on every agent shell command
A git pre-push allowlist in one repo, whoever pushes
The rule-ID tool’s refusal to change more than its own marker
The checker’s refusal of a tracked or staged defect tracker, in every repo
13 of 13 synthetic verdicts as designed; 9 of 9 self-test guarantees; 0 of 11 private defect trackers are tracked by git, each in its repo’s working tree behind an ignore rule.
Structural
Facts about where things live
The canon lives outside every repo and is read by reference, never copied
The canon folder is not a git repository, and no project holds a copy of the current rules.
Case 1 of 5
A gate that refuses a command shape it cannot check
The hook runs before the command does, so a call that stages and commits at once is refused: the gate can’t see what that call would stage.
The public gate is a Claude Code PreToolUse hook, which runs before a tool call executes. It is registered at user level on the Mac for the shell tool, so every shell command an agent runs on that machine, in every project, passes through it. For a public repo it scans the lines and messages of outgoing commits and pushes. For a deploy it scans every file the project serves, whatever the repo’s visibility; a project that hasn’t declared what it serves counts as serving everything. In every repo it refuses a committed defect tracker.
The hard problem was timing. The hook reads the staged index before the command runs, so git add X && git commit was checked against an index that didn’t contain X yet. The gate scanned nothing, allowed the call, and a third-party identifier reached a public repo. A probe showed the patterns were right; the gate simply couldn’t see the change.
Simulating git add would mean reimplementing pathspecs, and scanning the working tree would flag scratch work nobody was committing. So the gate refuses the shape: any call that stages and also commits or pushes is denied, and the refusal names the remedy (Fig. 1). It also refuses bulk pushes and archive refs, and if its own check throws, or GitHub can’t confirm a repo’s visibility, the command is held.
How we know
tools/public_gate.mjs: the stage-and-commit refusal (lines 66–78 and 120–127), bulk and archive refusals scoped to the push’s own arguments (80–119), deploy detection that ignores quoted text and heredocs and honours a project’s own listed deploy commands (42–63), and a deny on any exception (163–165). tools/check_public.mjs: a tracker refused in every repo and every mode, staged deletions excluded, a third-party text carried verbatim exempted one file path at a time, and unknown visibility treated as a block. Re-run 12 Oct 2026: 13 synthetic events, 8 refused and 5 allowed, every verdict as designed.
Case 2 of 5
Scrubbing a public history without trusting the scrub
Three public repos were recreated rather than force-pushed, and the removed history was confirmed in an outside archive before the last local copies were deleted.
In September 2026 private material reached public repos: committed defect trackers carrying a third party’s name and words, and a third-party name in project files. Force-pushed commits survive as unreachable objects that can still be retrieved, so rewriting history in place was ruled out. Each affected repo was deleted and recreated with fresh history at the same URL, and its prior history went to an archive outside the project.
That left a quieter hazard. In one repo the removed commits still lived locally on nine tags and three archive branches, so one git push --tags would have republished them all, and the only guard was a sentence in a notes file.
The order mattered. Every removed commit was first confirmed in the outside archive by hash; then the local refs were deleted and the object store pruned; then two mechanical guards went in: the gate’s bulk-push refusal, and a pre-push hook in that repo that lets only the main branch and version tags leave, whoever pushes.
The affected public repos were recreated with fresh history at the same URLs; the removed history went to an outside archive.
Re-checked: four removed commits are absent from the project and present in the archive; no archive branches remain; only version tags remain.
How we know
Re-checked 12 Oct 2026, read-only: git cat-file -t on four removed commit hashes fails in the project and returns commit in the outside archive; git branch -a --list '*archive*' returns nothing; git tag lists only version tags. The repo’s pre-push hook allows only its main branch and v<semver> tags and exits 1 for anything else. The nine tags and three archive branches are as recorded at the time, before those refs were deleted.
Case 3 of 5
Vigilance, then a rule, then structure
When a safeguard failed, the fix was to change what kind of safeguard it was, not to sharpen it.
The containment model came out of a run of failures; it wasn’t designed up front. In June 2026, preparing a private repo for public release turned up private notes that had built up in its git history, caught before it went public. The safeguard had been a habit, checking what’s staged before each commit. Its replacement was a rule: ignore private files by location, and audit with git ls-files.
Two days later the rule failed silently. A .gitignore line written with an inline comment became a literal pattern, the private folder was no longer ignored, and git add -A staged it. Only a manual audit caught it before the commit.
So the safeguard changed kind. The canon moved out of every repo and is read by reference, never copied. The sync machinery built that morning to keep copies current was deleted the same day: there were no copies left to keep current.
Where structure can’t reach, the ladder steps back down to a fail-closed check. One project’s dev-only tooling needs a bundler config outside the folder the project ignores, and it passes every content scan, so the checker refuses that path in a public repo by where it sits, not by what it contains.
Fig. 2Each step replaced the one above it after it failed. Both June failures were caught before anything went public; the public leaks came later, in September (Case 2).How we know
The canon folder is not a git repository, and no project holds a copy of the current rules (hash comparison, 12 Oct 2026); projects reference the canon by path. tools/check_public.mjs refuses the dev-tooling path in a public repo by location. On 12 Oct, check_public.mjs --tree returned clean on all four public repos. Three older projects still hold mid-June copies of canon files, untracked and ignored; see the honest notes.
Case 4 of 5
Decisions that carry their own provenance
Every ruling has an address that never moves, and decisions in code are marked with who made them.
Two questions come up whenever an agent edits a governed codebase: may I change this, and why is it this way?
In the rules, a small tool stamps an append-only ID on each ruling without reading the prose. A ruling inserted mid-section takes the next free number, so existing citations never move; stripping the markers restores the file byte for byte (Fig. 3). History headings carry their section address, so --why 4.2 narrows a search to that section’s history and ranks lines by keyword. Where a section has to be inferred from its title, the audit says so: a missing label becomes a finding, not a silent guess.
In code, a decision comment carries a tier marker, most of them dated: owner-decided, agent-decided (also how an unmarked comment is read), unsettled, or retired but kept on record. The tiers were designed to be safe in public repos; what the gate flags is wording that attributes a decision to a person.
File headers state what a file does, what calls it and what it writes. That rule came from an incident: a stale header described an old design, and a later session ran a middle pipeline step that overwrote a hand-verified data table. All 244 of FIRST LIGHT’s source files now carry one.
rule-ID tool · self-test
$ node rule_ids.mjs --self-testPASS stamps anchors, and only those
PASS skips front matter and fences
PASS skips an indented sub-bullet
PASS numbers a bare ## when asked
PASS id goes after the glyph run
PASS strip restores byte for byte
PASS re-run assigns nothing
PASS mid-section: next free id
PASS refuses edits past the marker
all 9 guarantees holdexit 0
Fig. 3The tool’s self-test, run 12 Oct 2026 and re-rendered from its output, wording abridged; its check marks are shown as PASS.
Decision markers in code by tier: 457 in 99 source files across 8 projects (grep, 12 Oct 2026)
Owner-decided
330
Agent-decided
66
Unsettled
59
Retired, kept on record
2
How we know
tools/rule_ids.mjs (parse, assign, emit, history scope, lookup, self-test, audit). On 12 Oct 2026 --self-test passed 9 of 9 (exit 0), and --audit found 119 IDs and 37 of 37 sections with labelled history in the portable rules, plus 370 IDs across 9 project rules files. A grep for the tier markers over source files, excluding dependencies and build output, found 457 in 99 files across 8 projects; 405 of them have a date within four lines of the marker. FIRST LIGHT’s scripts/check_headers.mjs: 244 of 244 files have a header.
Case 5 of 5
Context is the budget
Whatever loads at session start is paid for in every session, so procedure loads when it’s needed, and a menu-bar gauge shows the number that forces a compaction.
Procedures that matter only when something specific happens moved into Claude Code skills: the harness keeps a one-line description in view and loads the full text only when it matches. Of the canon’s files, 160,633 bytes load at session start and 563,884 do not: 122,428 bytes of on-demand procedure (including 1,289 lines of postmortems from third-party platforms), the 364,121-byte dated history and the retired 77,335-byte process file.
The trade-off cut the other way once. Build procedure, deferred until a build came up, got read late in a long session right before a ship, so it moved back into the bootstrap and was trimmed.
The defect process as a file read at every session start, and as a skill that loads only when a defect comes up: 77,335 and 11,526 bytes (wc -c, 12 Oct 2026; smaller is better)
Process file (retired)
77.3 KB
On-demand skill
11.5 KB
The instrument
In use · v0.2.0 · private
ClaudeBar: a menu-bar gauge for context fill
Claude Code’s own usage dial is coloured against rate limits, but the limit long sessions actually hit is the context window. ClaudeBar is a native macOS menu-bar app: 334 lines of Swift on AppKit and Foundation only, built with swiftc and no Xcode project.
It reads the tail of each live transcript and sums the input and cache tokens of the latest assistant turn, so the number is read rather than counted, parsing whole JSON lines because pattern-matching the nested usage object would fail silently. The menu bar shows the most recently active session, green below 80%, amber at 80% and red at 90%. It stays visible when healthy, because a gauge that goes quiet when all is well looks exactly like a broken one.
launchd restarts it only after a crash, so a deliberate Quit stays quit. On 12 Oct it had run for 16 days, since 27 seconds after boot, without exiting once; a --once flag prints its readings for verification.
Fig. 4The gauge’s bands, drawn rather than captured, because the live menu shows session names. On 12 Oct claudebar --once read the 8 live sessions on a 1M-token window at 32–64%; others had no usage yet or sat below 200k tokens. The fill marks the fullest.How we know
wc -c: the tracker skill is 11,526 bytes against the retired 77,335-byte process file, and the canon’s files split into 160,633 bytes loaded at session start and 563,884 not (122,428 on-demand procedure, 364,121 dated history, 77,335 the retired process file). ClaudeBar: wc -l on its one Swift file (334 lines), whose only imports are AppKit and Foundation; its build script writes a LaunchAgent with RunAtLoad and KeepAlive only on an unsuccessful exit. On 12 Oct 2026 ps showed the process started 26 Sep at 07:37:16, 27 seconds after kern.boottime, and launchctl print reported runs = 1, never exited. The 1M window is inferred from readings above 200k and was cross-checked once, on 1 Sep 2026, against the desktop app’s own display (recorded: 654,131 tokens measured against its 654.1k of 1M).