Repository navigation
Releases: GreyforgeLabs/slopbrake
Release list
Slopbrake 0.2.3
Faster hooks on every agent tool call, and two recording fixes for the user-level Stop gate (H6, H7).
- Faster:
slopbrake-hookimports only what the common path needs;subprocess,tempfile,shutil,hashlibandargparseload only where git, the Stop gate or the installer use them. Median wall time per call on a busy 12-core host, Python 3.13: a Bash call outside managed repos 48 → 35 ms, an Edit outside 44 → 32 ms, an untouched Stop 44 → 31 ms. Peak RSS 18.7 → 14.8 MB. - Faster: a snapshot of a managed repo is one
git status --porcelain=v2 --branchinstead ofrev-parseplusstatus, and stats each path relative to the repo's directory. PreToolUse in a managed repo 67 → 56 ms; on a repo with 30,000 untracked files a snapshot takes 2.7x less time (546 → 202 ms; the status parser is now linear). - Faster: an Edit in a managed repo reads HEAD only the first time the session changes that repo, with
rev-parse, never a fullgit status: 64 → 32 ms per Edit. - Faster: the project guard (
.claude/hooks/block-dangerous-git.sh) importsgit_guard.pyinstead of running it as a script, so Python reuses its bytecode instead of compiling 900 lines per Bash call, and parses its one option by hand (argparse alone pulled in a dozen modules on Python 3.14): 67 → 31 ms per call with Python 3.14, 17.9 → 12.9 MB. The bytecode lives under${XDG_CACHE_HOME:-~/.cache}/slopbrake/pycache, never in the repo, and the guard runs withpython3 -I, so the session's cwd andPYTHON*variables stay out of its imports. It reads the hook JSON as UTF-8 whatever the locale. - Fixed: an edit made through a symlinked directory was recorded as
../link/...instead of the repo's own path, so the user-level Stop gate skipped a repo whose only change came that way. Edit andapply_patchtargets are now resolved like the repo itself. - Fixed: a worktree rename (
git add -Non the new name) made the snapshot record a garbage path (porcelain v1 puts theRin the second column); the v2 parser reads renames by entry type. slopbrake verifyand Slopbrake's own gate keep the guard's bytecode in their scratch directories.
Full Changelog: v0.2.2...v0.2.3
Slopbrake 0.2.2
- Fixed: the user-level Stop gate held a session red for another session's uncommitted work in the same repo (and spent its whole budget mutating it). It now records the files each session changed and the HEAD it started from, skips repos where nothing of the session's is left, and gates HEAD plus the session's own files in a clean temporary worktree when other uncommitted work is present.
- New:
boundaries_py.py --skip-rule RULElets a repo keep only the D1 rules that fit its architecture (for example, cycles only).
Slopbrake 0.2.1
Found by putting 0.2.0 to work on two more repos and three more harnesses.
Codex and OpenCode (H5, H6, H7)
-
New:
slopbrake user-hooks install|uninstall|status --harness codexmergesslopbrake-hook --harness codex pre-tool-use|post-tool-use|stopinto~/.codex/hooks.json, keeping other tools' entries. It refuses while~/.codex/config.tomllacks[features] hooks = trueand never edits that file;statusreminds you to trust the entries in Codex's/hooksscreen and reports which ones[hooks.state]already trusts. -
New:
--harness opencodeinstalls the OpenCode plugin~/.config/opencode/plugins/slopbrake.js(one dependency-free module; it refuses to overwrite a file that is not its own). It guardsbashbefore it runs, recordsbash,edit,multiedit,writeandapply_patchchanges, and onsession.idleruns the Stop gate for root sessions, sending a red report back as a new prompt at most 3 times in a row. It does nothing whenslopbrake-hookis not onPATH. -
Both installers refuse, and
statusis not ok, unless theslopbrake-hookonPATHanswers--harness codex|opencode pre-tool-use: an older build that rejects--harnesswould make every hook a silent no-op. Codex entries count as installed only when matcher and timeout match too; install rewrites one that differs. Codex keys its trust by entry position, so removing or replacing slopbrake's entries can renumber a later entry in the same event, which then needs trusting again (statussays so). -
Fixed: Grok runs a repo's
.claudehooks with camelCase JSON (toolInput,sessionId), so the git guard saw no command and allowed everything while looking wired. The kit's hooks andslopbrake-hook --harness groknow read both shapes; Grok'swriteandhashline_editedits are recorded. -
Fixed: a
CLAUDE_PROJECT_DIRleaked into another harness made the user-level Stop gate skip the project repo; it is honoured only for Claude Code.slopbrake hook --harness ...(the alias) no longer dies in argparse with exit 2, which every harness reads as a block.
The gate
- Fixed: when only tests changed, mutation mutated every module the changed test files imported, whole; on a large repo one changed allowlist made the Stop gate time out on every stop. It now mutates only the functions the changed test functions call (compared with the base, so a deleted assertion counts), and skips when they call nothing first-party.
- Fixed: the last-green ratchet and the default-branch door (C8) counted untracked files, so a permanent untracked folder froze the ratchet and kept the
prstage red on main. Both now look at tracked files; an untracked file counts for C8 once it is staged. - Fixed: the Python
scripts/checkin a linked worktree tested the main checkout's code (the borrowed.venvholds an editable install of the main tree) and lacked the venv's tools onPATH. The template now puts this tree first onPYTHONPATHand the venv'sbinonPATH. (Repo-owned: hand-merge into an existingscripts/check.) - Fixed: the kit scripts wrote
__pycache__next to themselves, andstatusdid not notice bytecode an older install had committed.
init and status
- Fixed:
initin a linked worktree set the sharedcore.hooksPath, switching the main checkout's hooks before the kit was merged; it now leaves it alone and says to set it after the merge.init --no-trustskips the trusted-repo list, for staging a branch.CLAUDE.mdis named after the repository, not the worktree directory. - Fixed:
core.hooksPath=/dev/nullis reported as disabled hooks, not as existing hooks to chain into. - Changed:
statuscounts only tracked TypeScript sources for its coverage gap, andUNCOVERED=(dir ...)inscripts/checkdeclares sources left out on purpose (vendored code).
Slopbrake 0.2.0
An audit of 0.1.0 in real use found that Slopbrake was installed but never fired, and that several of its guarantees were false while verify and status stayed green. This release makes the gate do what the README says. Rule IDs refer to docs/RULES.md.
Hooks that actually run (H5, H6, H7)
- Broken: Claude Code loads project hooks only from the directory a session starts in. Sessions started in
~never ran the git guard or the Stop gate, and nothing said so. - New:
slopbrake user-hooks install|uninstall|status|trust|untrustwiresslopbrake-hook pre-tool-use|post-tool-use|stopinto~/.claude/settings.json. The guard acts in every repo with.claude/slopbrake.json; the Stop gate runs only for repos the session actually changed and that you trust (inittrusts its repo; a cloned repo's scripts never run until youuser-hooks trustit). Outside managed repos the hooks do nothing.installrefuses while theslopbrakeonPATHis too old, and an older install withoutslopbrake-hookfails harmlessly instead of blocking every command. - Broken: the git guard was a regex with many bypasses (
push -f;,+main,--force-with-lease,restore/checkoutforms,--no-verify,-c core.hooksPath=) and false positives on commit messages andgreppatterns. It failed open withoutjq. - Changed: the guard is now
git_guard.py, which parses the command like a shell (bash -c,eval, wrappers, substitutions, heredocs) and judges git's own argv. It blocks the forms above plus branch, stash, reflog, ref and object deletion, and aliases orrebase --execthat run them. The shell wrapper fails closed without python3. - Broken: the Stop gate looked only at uncommitted code, so committing (or working in another worktree) let an agent stop with an unchecked change.
- Changed: a clean tree with commits no full green run has covered gets the full gate, with sampled mutants and one 540 s budget inside the hook's 600 s timeout. Worktrees of the same repo are gated too.
The gate (L1)
- Broken: stages with nothing to run printed
pass(types was anecho, smoke without a script, pr without a PR), andverifyproved stages by name, so trimmingSTAGESinscripts/checkswitched off the real gate with all proofs green. - Changed: a stage with nothing to check exits 78 and reports
skip <stage>with its reason. The L1 proof fails unless the summary reports every stage the kit'sscripts/checkdefines, andstatusflags missing stages and an unconfigured types stage. - Broken:
MUTATION_FLOOR=0 git pushandTEST_CMD=truelowered the gate from the environment. - Changed:
scripts/checkfixes the floor and the test command; pre-push drops inherited overrides and runs the full gate once per pushed ref, against the commit the remote already has. - Broken: committing straight on the default branch with no remote (both pilots' workflow) measured mutation and the door against HEAD itself: nothing changed, so the floor and the door never ran on committed work.
- New: the last-green ratchet. A full, all-green run on a clean tree records
refs/slopbrake/last-green/<branch>(/in the branch name written as%2F); on the default branch, checks measure from it, or from the merge-base with it after an amend or rebase, or from the empty tree when there is none yet.initstarts it at HEAD. - Changed: pre-commit refuses when a staged file also has unstaged edits, and pre-push refuses a dirty tree or a ref that isn't checked out: the gate checks the working tree, so it must be the tree being committed or pushed. A new branch is measured against what the remote already has.
- Changed: a change that touches only tests is mutation-checked against the modules those tests import (Python), so weakening an assertion no longer slips through. Marking every changed line
# slopbrake: no-mutatefails the stage. - Changed: Python repos get pytest when it's configured or declared anywhere (pytest-style tests were silently skipped under
unittest discover), the project's interpreter (.venv, found at run time, also from a linked worktree), and a CI install that matches. The TypeScript CI is set up for the repo's package manager instead of always pnpm.
Test quality (T1, T4)
- Broken: an assertion-free test cleared the mutation floor when the code used string
+(the mutant crashed) and 0 mutants scored 100%. T1 missed its own headline form,assert total(xs) == sum(xs), and the inlinereduce/Math.*forms in TypeScript. - Changed: T1 flags tests with no assertion (Python check; TypeScript
slopbrake/expect-in-test), the direct aggregate forms and more shapes, and stops flagging determinism, identity and property checks.# slopbrake: allow-no-assert: <why>suppresses one. - Changed: more mutation operators (boundaries, strings, deleted calls), no arithmetic mutants on string concatenation, nothing to mutate is a skip,
# slopbrake: no-mutatefor equivalent mutants, and timed-out mutants no longer leave orphaned processes or scratch copies.
TypeScript (D1, T1, T4)
- Broken: under npm,
npm exec <tool> --flagsswallowed every flag (test-quality always red, Stryker lost--mutate), and with dependency-cruiser missing,npm exec depcruiseinstalled a squatted registry placeholder that let boundaries pass. - Changed: tools run from
node_modules/.binonly; a missing tool fails its stage and names the install command. Boundaries fail when dependency-cruiser read nothing;import typeand co-located tests are checked; the smoke stage runs asmokepackage script.
Boundaries (D1)
- Broken: Python boundaries were enforced only between top-level packages, so a single-package app had no enforcement at all; monorepos under
packages/andimportlibimports were invisible. - Changed: entry points are enforced at every package level, plus
importlib.import_module/__import__, namespace subpackages,packages/*/monorepos and stricterTYPE_CHECKINGcycle handling.
Doors (G1-G4)
- Broken: a PR could lower its own floor: the rules were read from the PR's tree, deleting
door-rules.ymldisabled the check, andSLOPBRAKE_BASEbeat the CI base. Gate configs (Stryker, ESLint rules, dependency-cruiser, the reviewer, skills,CLAUDE.md) classified two-way. - Changed: the base revision's rules count too, and
ignorelists come from the base only; no rules anywhere is an error. The default rules cover many more one-way paths and destructive SQL spellings, with fewer false positives (prose, test fixtures,.env.example). - New: without a PR, the
prstage refuses an uncommitted one-way change on the default branch: commit it on a feature branch for review. The PR-body check is stricter: the Door and Blast Radius must be in prose, not code blocks, and a one-way door's rollback must name a concrete step that isn't negated ("no way to restore" is not a plan).
CLI
- Fixed:
init --dry-runcrashed on fresh Python repos;verifyleaked a worktree when setup failed; environment errors exit 2 with one line instead of a traceback;initin a monorepo subdirectory no longer touches the parent repo's hooks. - Changed:
verifyalso fails when the Claude Code hooks aren't wired or are disabled, proves the committed HEAD only, and shows the failing stage.statusreports a stale kit (by version and by file), unwired hooks and the missing ESLint plugin.inittells you to commit the kit on a feature branch.
Community
Thanks to @nixfred for the first community fixes, all in this release:
- #1: the git guard catches git's global options,
clean --forceandcheckout <ref> -- .. - #2: the diff parser no longer drops added lines that start with
++. - #3: a scalar or empty rule list in
door-rules.ymlis an error, not a silently wrong rule.
Upgrading from 0.1.0
- Install 0.2.0:
uv tool install --force git+https://github.com/GreyforgeLabs/slopbrake(orpipx install --force …). - On a feature branch, run
slopbrake init . --update. Kit-owned files (scripts/slopbrake/,.claude/hooks/,.githooks/, skills, the reviewer) are refreshed. Repo-owned files are kept and listed askept; merge these by hand (the kit's versions are whatslopbrake initwrites into a fresh scratch repo of the same stack):scripts/check:STAGES=(lint types boundaries tests smoke test-quality mutation pr), with smoke; a fixedexport MUTATION_FLOOR=60and a fixedTEST_CMDinstead of the${…:-…}forms; a types stage that runs your type checker or returns 78 (skip); a smoke stage that returns 78 withoutscripts/smoke.sh..claude/door-rules.yml: the newone_waypaths (gate configs,.claude/agents/**,.claude/skills/**,CLAUDE.md, secrets, more infra), the newcontent_patterns, theignorelist and the extracontent_ignoreentries. Keep each key once..github/workflows/check.yml: the setup and install steps for your package manager.
- Commit on the branch: the update touches the gate, which is a one-way door, so the gate refuses it on the default branch. A human reviews and merges it.
slopbrake user-hooks install, so the hooks fire in sessions started outside the repo.slopbrake status .should list no gaps, andslopbrake verify .should report 16 proofs holding.
Slopbrake 0.1.0
Brakes for the software factory. Slopbrake puts Matt Pocock's agentic-engineering ideas into a repo as checks that fail a build, and proves each one bites.
uv tool install git+https://github.com/GreyforgeLabs/slopbrake@v0.1.0
slopbrake init path/to/repo && slopbrake verify path/to/repoIn 0.1.0
- One gate (
scripts/check): lint, types, import boundaries, tests, smoke, a tautological-test check, and a mutation floor on changed lines. Run by pre-commit, pre-push, CI and a Claude Code Stop hook. - Tautology check for Python (AST) and TypeScript (ESLint rule
slopbrake/no-tautological-test). - Mutation floor on changed lines only: a stdlib mutator for Python, StrykerJS ranges for TypeScript.
- Deep-module boundaries: a stdlib Python port of Pocock's dependency-cruiser rules, and his config for TypeScript.
- Door triage: path and content rules compute a one-way/two-way floor the agent can raise but never lower; the PR-body check enforces Door and Blast Radius.
- A reviewer agent in fresh context: two parallel axes (Standards, Spec), then a single fix-mode pass that commits
review:fixes. - Git guardrails and a Stop-only-when-green hook for Claude Code.
slopbrake verify: 15 proofs in a throwaway worktree. Each rule must fail on a planted violation, name itself, and pass again once it's removed.- Pocock's skills vendored unmodified at
c55ee46(MIT), with credits for every idea in the README.
See docs/RULES.md for all 33 rules.