what an agent may change
An agent can fix your broken pipeline. Interlock decides what it may fix alone, what needs a person, and what it must never touch.
The problem
Without it
Your build goes red at 2am. The agent reads the log, decides the fix is a tweak to the deploy workflow, and opens a PR. It is well written and it explains itself. At 8:40am someone merges it because it looked plausible — and because the other forty agent PRs that month were fine.
With interlock
REFUSE .github/workflows/deploy.yml is denied by scope.deny
Same agent, same patch. The difference is that somebody wrote down, in advance and in a reviewed file, that nothing automatic touches deploys.
Branch protection and CODEOWNERS guard the merge.
Nothing guards the moment a machine decided something was worth changing. For a human
contributor that is fine — they had a reason you can ask about. For an agent that opened
forty PRs this week, "looks plausible" is the entire review.
What you get
Every failure is auto-fixed, held for a person, or refused outright. Exit codes 0 / 75 / 1, so your workflow can tell "waiting" apart from "broken".
Clear a fix once and the same failure stops asking. A different failure never gets covered by it. Change the rules and every past approval dies with them.
Which files a fix may touch is read off the diff interlock built — not off anything the agent claims about itself.
Every decision appends one JSON line: what broke, the verdict, the reason in words, and who signed it. Commit it.
Fourteen failure classes matched by rule. An LLM is optional and plugs in where no rule matched — and whatever it writes still goes through the gate.
No service, no database, no account. One JSON file and one .mjs
file with zero dependencies.
Install
Node 18+. Install from GitHub — the bare name interlock
on npm belongs to an unrelated package, so always go through the repo.
npm install -g github:manpreet171/interlock
Prefer not to install anything? Every command below
also works as npx github:manpreet171/interlock <command>.
interlock init
✔ wrote interlock.json ← review this in a PR like any other code ✔ created .interlock/ ← the ledger lands here. Commit it.
The linter refuses a placeholder owner on purpose. Exit 1 makes it CI-friendly.
interlock lint
✔ interlock.json is valid
mode normal · 1 auto · 7 hold · 6 refuse · rules 0a20347304ad
gh run view 4471 --log-failed > run.log interlock gate run.log --repo acme/api --run 4471
missing-python-module 263819a52f2e evidence ModuleNotFoundError: No module named 'requests' proposal add "requests" to requirements.txt AUTO class "missing-python-module" is pre-cleared and the change is in scope --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ flask==3.0.0 gunicorn==21.2.0 pytest==8.0.0 +requests
One step, only on failure. Exit 75 means held, not broken.
- name: interlock if: failure() run: | gh run view "$GITHUB_RUN_ID" --log-failed > run.log npx github:manpreet171/interlock gate run.log > fix.diff
Set "mode": "shadow" and it records every verdict
while blocking nothing. A week of evidence with nothing at stake, then switch to
normal.
The part you cannot fake
Every failure gets a fingerprint — a hash of the error once timestamps, run ids, runner paths and commit hashes are stripped out. The same break always hashes the same; different breaks never collide. So an approval is bound to one exact failure, not to "this once" and not to "forever".
HOLD set-output-deprecated 1c59f09f5474 → a person decides ✔ cleared by Manpreet Singh · warrant valid 14 days three days later — new run id, new timestamps, new runner path AUTO warrant h-627fdffd — cleared by Manpreet Singh (use 1/5)
And because the fingerprint comes from the log, the thing asking for permission cannot forge it.
Source & docs
Full documentation, the four pillars, the decision order and a LangGraph reference agent live in the repository. MIT licensed.
github.com/manpreet171/interlock→