English · עברית
Formerly Grove. Renamed in 0.2.0 — see Upgrading from Grove.
The safety and orchestration layer for AI agents in multi-repo environments.
An AI agent (like Claude Code) is a brilliant tool when operating within a single project folder (Repository). However, in the real world, tasks cross boundaries: shipping a feature requires a code change (App), a data schema update (Hub), and editing a marketing page (Web).
The moment the agent "reaches out" to a foreign directory, it loses touch with reality. Because the git status command is limited to its current working directory, the agent might blindly overwrite another developer's open files, push code to the wrong branch, or perform a destructive blind commit.
RepoGuards was built to solve exactly this. It is a set of guardrails that run inside the session, at the moment of the tool call: they detect the anomaly, refuse the two actions that cannot be undone, and hand the agent back the picture it was missing so it can correct itself.
Built for Claude Code, and tested only there. The guards are Claude Code hooks:
they read its hook protocol and its tool names (Bash, Edit, Write), and
close.py reads the journal those hooks write. The idea carries to any agent with a
pre-tool hook; the code does not, yet.
The system provides 3 critical solutions:
- Blocking Blind Commits (
scope_foreign_commits): If the AI stands in directory A and attempts to rungit commit -ain directory B, the system blocks it. Instead of a generic failure, it injects the true state of the foreign directory (including the branch name and open files), instructing the AI to commit only specific files. - Honoring Boundaries (
honor_ownership): If a file is claimed by another project (via.claude/owns.json), the hook refuses to let the AI edit it, redirecting the agent to the proper location. The file's format, with an example, is intools/README.md .
An agent updating 3 projects simultaneously leaves a trail of changes. Standard Git commands break down here.
The script $REPO_GUARDS/tools/close.py --commit reads which files this specific session wrote across every directory, and stages only those — never a sweep. What it claims is deliberately narrow: a path counts only if a writing tool of this session named it and it is still dirty, so a parallel session's work can never be swept into your commit. The cost of that choice is stated plainly under Known limits.
The brain that tracks which projects exist on your machine, and which "Skill Tracks" each project is allowed to load. This prevents an agent in an iOS app from Android deployment commands. (The system syncs perfectly with other skill libraries, such as the App Factory).
Because RepoGuards manages your environment, it is usually the first product you install, sitting alongside your other projects (typically under a <base> directory like ~/Developer).
You need: git, Python 3.9 or later, and Claude Code (claude on your PATH). Nothing else is installed — the tools use only Python's standard library.
BASE=~/Developer # wherever your repos live
cd "$BASE"
git clone https://github.com/yybd/repo-guards.git
claude plugin marketplace add "$BASE/repo-guards"
python3 "$BASE/repo-guards/tools/deploy.py"
Every command below uses $BASE the same way, so set it once in the shell you paste
into.
Give marketplace add an absolute path — that is what "$BASE/…" is for. A bare repo-guards or dev/repo-guards is read as a GitHub owner/repo, fails with an "access rights" error, and every step after it fails with it.
deploy.py is the whole installation: it creates your tools/registry.json from the shipped template, installs the plugin, and verifies. It prints what it did — read that rather than assuming. Then open a new session, because hooks and skills are read at session start.
What it writes outside this repo: one env variable per place in
~/.claude/settings.json — $REPO_GUARDS, $DEV_ROOT, and every registered project by
its own name — so a skill can say $SITE/content instead of a path on your machine.
Nothing else in that file is touched. The enabledPlugins and extraKnownMarketplaces
entries you will also find there are Claude Code's own, written by the claude plugin
commands above.
claude plugin list shows RepoGuards with a commit hash where other plugins show a
version. That is deliberate: plugin.json declares no version, so update follows the
commit; releases are git tags.
No session needed. Make two throwaway repos and hand the guard the exact payload Claude Code would send when an agent standing in one pushes the other:
mkdir -p /tmp/rg-demo && cd /tmp/rg-demo && git init -q mine && git init -q theirs
echo '{"tool_name":"Bash","tool_input":{"command":"git -C ../theirs push"},"cwd":"/tmp/rg-demo/mine"}' \
| python3 "$BASE/repo-guards/plugins/repo-guards/hooks/scripts/scope_foreign_commits.py"
It answers "permissionDecision": "deny" with the reason the agent would read. Change the command to git -C ../theirs add a.txt and it allows, with the other repo's state attached; to git push with "cwd" set to theirs, and it says nothing — that one is yours to make.
Optional, and reported by every deploy until you do it or decide not to — the working rules, loaded in every session in every repo:
ln -s "$BASE/repo-guards/rules/global-CLAUDE.md" ~/.claude/CLAUDE.md
If you already have a ~/.claude/CLAUDE.md — or want rules of your own on top of these, such as the language you are answered in — do not replace it. Keep it, and add one line to it that imports the rules instead — with your $BASE written out, because an import line does not expand variables:
@<base>/repo-guards/rules/global-CLAUDE.md
The shared rules stay shared, your additions stay yours, and deploy accepts either shape. Nothing else depends on this step.
For the guardrails and skills to activate in a specific project (e.g., your frontend repo), enter the project and run:
/repo-guards:register-project
This command reads the project, writes its entry in RepoGuards' registry.json, and — when the repo draws at least one skill track — writes the project's own .claude/settings.json. RepoGuards ships no tracks of its own beyond itself, so until you add a marketplace that does (such as App Factory), registration creates no file in the project.
Without the slash command, an entry in tools/registry.json is three lines — a path relative to $BASE, a role, and the tracks — followed by python3 "$BASE/repo-guards/tools/deploy.py". Add it inside the projects object the file already has, beside the _example_* entries: those are ignored (so is any name starting with _), and can stay or go.
"projects": {
"my-app": {"root": "my-app", "role": "the iOS app", "tracks": []}
}
What registration does and does not change. The guards are always on — they ship with the plugin, which is enabled for your user, so they run in every repo whether or not it is registered. Registration decides two other things: which skill tracks load there, and whether the end-of-turn guard reports work you left uncommitted in it. So an unregistered repo is not protected-by-being-ignored; it is a repo that quietly loads no tracks. (Since 0.3.0 that is true of the commit-and-push guard too: any git repository other than the one the session stands in is foreign, registered or not. Before, only registered repos counted.)
That is why an unregistered repo says so, once per session. It asks three times and then stops interrupting — after that it is a single line, still said every session, because the thing it reports does not go away on its own. Two answers end it: register the repo, or list it under ignored. On a machine full of repos that are nobody's business, REPO_GUARDS_OFFER=off in the environment, or "offer_registration": false in the registry, ends it everywhere.
While working on a project in parallel with others, these are the rules RepoGuards enforces:
- Read the Injected Intelligence: The moment you touch a foreign directory, the system injects a
[foreign]message containing the remote status (including the Branch name). The agent must read this before continuing its edits. - Never
pushremotely: Commands likegit -C ../other-repo pushare completely blocked. Push publishes an entire branch, which is too aggressive when executed "through the wall." - Do not collect garbage: Never run
add .on a foreign directory. Work surgically with exact paths, or letclose.pydo the heavy lifting for you. - Turn Completion Report: At the end of any prompt where the agent left foreign files open without committing them, RepoGuards' Stop Guard lists each place under "Work left open outside this session's own
git status", with what this session changed there, and points atclose.py. It speaks only when something changed since it last spoke.
A guard that pretends to full coverage is worse than one whose edge is known. These are the edges, and they are tested:
- What a script writes from the inside is out of reach.
python3 build.pycan write anywhere, and nothing in the command line says so. The guards read tool calls and shell lines; they are not a file-system watcher. - A command whose target is computed cannot be judged.
git -C $DIR pushor a path built by$(…)resolves at run time, inside the shell — so the guard sees a variable and, by design, allows it. Everything it cannot parse with confidence, it allows. - It fails open, always. A malformed payload, an unreadable registry, a missing file — every one of them allows the operation. A guard that blocks by mistake stops work; a guard that crashes stops everything.
close.pyunder-claims rather than over-claims. A file written through a shell line is reported as changed but is never staged for you, and the report says so.
Up to 0.1.0 this was called Grove. The marketplace, the plugin and the commands are renamed, so an installed grove@grove is not upgraded in place — it is replaced:
git -C <your clone> pull
claude plugin marketplace remove grove
claude plugin marketplace add "<your clone, absolute>"
python3 <your clone>/tools/deploy.py
claude plugin uninstall grove@grove # only after deploy reports repo-guards installed
/grove:deploy is now /repo-guards:deploy, and $GROVE is now $REPO_GUARDS. For one release the old names keep working: deploy still writes $GROVE, every GROVE_* variable is still read, and grove_repo can still be imported. The old GitHub URL redirects. Your clone's directory name does not matter.
Want to know how the Hooks intercept commands or how the Plugin Cache works? Read the full documentation:
- How It Works (HOW-IT-WORKS.md) — Under the hood: Cache, Hooks, and Registry mechanics.
- CLAUDE.md — The constitution and guidelines the agent reads when operating inside this specific Repo (for developing RepoGuards itself).
- Docs & Reasoning (docs/) — Every architectural decision (what was measured, why alternative solutions were rejected) is logged there.
License: MIT