---
title: Git guardrails
---

# Git guardrails

## What it is

> **Status: shipped, ON by default.** Every user's protected branches (`master`/`main`) are
> guarded out of the box. Configure it, switch it to warn-only, or turn it off in **Settings →
> Features → Git guardrails** or the **Dev Pipeline panel → Setup tab → Git guardrails** (the
> same controls, in two discoverable places).
>
> It lives in the Dev Pipeline panel because that panel and these guardrails both exist to
> protect `master`/`main` while agents work — but note the rail protects **every** spawned
> agent session, not just Dev Pipeline runs.

Git guardrails is an Omniscio safety feature that stops the AI sessions Omniscio spawns from doing
dangerous git operations on your behalf — most importantly **committing or pushing
directly to a protected branch** (`master` / `main`). When it's on, Omniscio quietly installs
a small safety check into every session it launches, so if an agent tries a gated git
command the command is **blocked before it runs** and the agent is told why.

It's the built-in, one-switch version of the hand-installed PowerShell "block-master"
hooks some power users run today: instead of each person wiring up their own machine, Omniscio
bundles the check and turns it on for the whole session automatically.

**It is a safety rail, not a security sandbox.** It catches the common, honest cases — an
agent that reaches for `git commit` while sitting on `master`, or `git push origin master`.
A determined agent that deliberately obfuscates a command (hiding it inside `$(...)`,
`eval`, an alias, or a script it writes and runs) can get around a shell-command checker.
Treat it as guardrails on a mountain road, not a locked vault.

## Where to find it

### Pausing it

Three temporary off-switches. Two are yours alone; the third lets a blocked agent unblock
**itself** for a few minutes, and is deliberately narrower than the other two.

- **For one agent** — pause a single session for a set number of minutes. Every other session
  stays guarded, so one stuck agent no longer costs the whole fleet its protection.
- **For the whole machine** — the original pause window, when you want a free hand everywhere.
- **The agent unblocks itself** — capped at 10 minutes, 3 times an hour, and it always posts a
  note to your inbox saying which session did it and why. **It never allows pushing to a remote,
  and never a destructive `git stash`** — both of those can damage something that isn't the
  agent's own work, so they still take your approval. Turn the whole thing off with
  _Let an agent unblock itself briefly_ (Settings → Features → Git guardrails) and you are back
  to approving every time.

No pause of any kind lifts #8–#10. "Let me write to master for a while" should not also mean "let
me mass-close pull requests or kill the app" — those have their own single-command overrides.

### When you get asked to approve

If an agent needs something it can't grant itself — a push, or a longer window — it raises a
card in your inbox naming the command it was blocked on. The card has an **Approve** button:
one click, one confirmation, and that one session is unblocked for 20 minutes. Everything else
stays guarded.

**The same card also appears in the conversation**, right above the message box of the session
that asked, with the same **Approve** / **Decline** buttons and the agent's own stated reason.
So you can answer without leaving the thread you were reading — which matters most on your
phone, where the inbox and a session are two different pages. It is one approval shown twice:
answer it on either screen and it disappears from both, and the waiting agent is told either way.

This covers both ways an agent can end up waiting on the guardrails: asking for a short window
on itself, and asking to change a guardrails **setting**. Approvals about anything else stay in
the inbox only — the message box is not a second inbox.

An agent can only ever ask on its own behalf. The session that gets unblocked is the one the
app recorded as raising the card, not a name the agent typed — so a card can never trick you
into unblocking a different agent.

**Always allowed:** reads (`git status` / `log` / `diff` / `show` / …), moving _onto_ a
protected branch (`git switch main`), creating a new branch, and `git fetch`.

### Worktree commands: all of them go through the app (changed 2026-09-01)

While the Dev Pipeline is on for a repo, an agent working in it cannot run **any** `git worktree`
command that changes something — `add`, `remove`, `move`, `prune`, `lock`, `unlock` or `repair`.
Looking is untouched: `git worktree list` and a dry-run `prune` work exactly as before.

**Two of those refusals fire in EVERY guarded session, Dev Pipeline or not.** `raw-worktree-add`
(a bare `git worktree add`) and `forced-worktree-remove` (`git worktree remove --force`) are
deliberately branch-independent standalone rules — they read nothing from the Dev Pipeline's
worktree-lockdown switch, so turning the pipeline off does not make them go away. They do have a
sanctioned way through, which the wider lockdown does not: **`AMC_ALLOW_RAW_WORKTREE_ADD=1`**, for
a repo the create API genuinely cannot serve (it 404s, or the server is down). The refusal message
names it. It is a fallback for an unserved repo, never a way to skip the ledger when the route
would have worked.

The reason is ownership. A worktree made or destroyed by hand is invisible to the app until a
background sweep notices it, and that sweep can never recover the two things only the person who
made it knew: **who is working in it, and why.** Measured on the operator box the day the first
version of this rule landed — of 755 recorded worktrees only 108 came from the app's own call,
647 were adopted after the fact, and 57 of the 118 open ones had no owner at all.

Each refusal names the replacement for the exact command that was tried:

| Instead of                     | The agent uses                                                                                                                                                                                                                                                                                                                                                               |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `git worktree add`             | `POST /worktrees/create` — creates it and records the owner, then provisions its dependencies in the BACKGROUND. Read `readinessWarning` on the reply (it is present on BOTH a `200` and a `202`): a `200` does **not** mean usable, and a `202 status: 'creating'` means `path` is not yet a place to work — poll `GET /worktrees/create/<ledgerId>` until it reports ready |
| `git worktree remove`          | `POST /worktrees/:id/remove` — retires it reversibly into `.trash`, keeping every floor that guards WORK (uncommitted, ignored, unmerged) or a running job. A worktree's own proven owner asking to give it up is not held by the "is somebody still using this?" floors — it just answered them                                                                             |
| `git worktree lock` / `unlock` | `POST /worktrees/:id/pin` — which now sets the real git lock                                                                                                                                                                                                                                                                                                                 |
| `move` / `prune` / `repair`    | nothing — the app does these itself                                                                                                                                                                                                                                                                                                                                          |

**Nothing an agent can do lifts the TYPED refusal.** The lockdown rule is evaluated above every
escape hatch and reads no grant, so not the pause an agent can open for itself, not the
machine-wide approval window, not a repo exclusion, and not an environment flag gets a typed
`git worktree` mutation through. That is deliberate: the change was prompted by an agent using a
sanctioned pause to get past the earlier, narrower version of this rule.

**The one way through is you.** If the app genuinely cannot serve a repo, the agent asks for
approval and you get an inbox card; clicking **Approve** opens a short window for that one session
in which the raw commands work. It never opens by itself, and it never covers
`git worktree remove --force`, which destroys a live worktree that is very likely another agent's.

The same refusal is enforced a second time by the `git` wrapper on the agent's PATH, so a worktree
command fired from inside a script an agent wrote is caught too, not just one it typed.

**That second layer is a rail, not a wall — and it is the only one covering the script-borne
case.** The hook only ever sees a command the agent TYPES, so a `git worktree add` buried in a
script is stopped by the PATH shim alone. The shim is coarser by construction: it stands down on
`AMC_DISABLE_WORKTREE_LOCKDOWN=1` (an inline env prefix sets it) and on the bare presence of
`~/.claude/.worktree-lockdown-breakglass.json`, with no session match and no expiry check — unlike
the hook, which scopes a break-glass window to one session and honours its expiry. So read the
"not an environment flag" guarantee above as covering the typed command; for the script-borne
path, the hook is the layer of record and the shim is an operator-disablable backstop.

## How it behaves

### What it blocks

When enabled, these operations are gated in a spawned session.

**Wrapping a command doesn't hide it (fixed 2026-09-03).** Everything below is caught the same
way when it's handed to another interpreter to run — `bash -c "…"`, `sh -c`, `powershell
-Command`, a command piped into a shell, or a `git` call made from inline `node -e` code. Until
this was fixed those five shapes were let through, so a push to master could slip past on any
machine whose only protection was this hook. Writing _about_ a git command is still fine: text
you print, log, or save to a file is data, and inline program code is only inspected when it
actually has a way to run something.

**Nor does a change of folder that is easy to miss (fixed 2026-09-25).** A `cd` inside `( … )`,
behind `builtin` or `command`, or run by `eval` / `Invoke-Expression`, and a folder handed to
`env -C`, are followed like a plain `cd`; code fed to a shell through `eval`,
`Invoke-Expression`, a heredoc or a here-string is judged in the folder where it really runs.

1. **Commit / write while on a protected branch** — any history-changing git command
   (`commit`, `reset`, `rebase`, `merge`, `cherry-pick`, a branch delete, …) while the
   repo's checked-out branch is `master` or `main`.
2. **Push that targets a protected branch** — e.g. `git push origin master` or
   `git push origin HEAD:main`, from any branch.
3. **Push that would send a local-only `ready-to-merge` tag to a remote** — the app marks a
   branch "ready" with a local note (a branch description), never a pushed git _tag_, so a
   `ready-to-merge` **tag** is a per-worktree relic. Pushing one — by name, `refs/tags/…`, a
   force `+`, or the `--tags` / `--follow-tags` / `--mirror` sweep flags — leaks it to everyone
   on the next `git fetch`, so it's blocked. **Deleting** such a tag from the remote (cleanup)
   stays allowed.
4. **Push without a ready-to-merge tag** _(opt-in, default off)_ — when you turn on the
   extra "ready-to-merge gate", even a feature-branch push is refused unless the branch
   carries a current, SHA-bound `ready-to-merge` marker whose SHA matches the exact commit
   you're pushing. This enforces a "verify, then tag, then push" workflow. It covers
   **`gh pr create` as well as `git push`** — publishing the same commits as a pull request
   is the same act — so an untagged branch cannot be opened as a PR either. Either escape
   hatch lifts both commands the same way: an **approval window** (a human grant lifts
   everything) or an **excluded repo**.
5. **Destructive `git stash`** — `push` / `pop` / `drop` / `clear` / `apply` / `save` and
   bare `git stash` (which is an implicit push). These can silently swallow or reapply
   another agent's uncommitted work. Read-only `git stash list` / `git stash show` are
   allowed.
6. **Editing a file while the repo is on a protected branch** — the same rule as #1, but
   for the agent's file-editing tools, not just shell git.
7. **A shell command that writes a file into a protected checkout** — the shell half of
   #6. Gating only the edit tools left an obvious way around them: a `>` redirect,
   `curl -o`, `tee`, `cp`/`mv`, or a PowerShell write cmdlet aimed at the `master`
   checkout. An agent's working directory is the base checkout even when its real work
   lives in a worktree, so a stray relative path lands in `master` — exactly how untracked
   junk files kept appearing in the repo root. Windows has a nastier version: inside a Bash
   command an **unquoted** `M:\tmp\out.png` has its backslashes eaten and collapses to a
   _relative_ name, created right where the agent is standing. (Quote it, or use forward
   slashes, and it writes where you meant.)

8. **Closing pull requests destructively** — closing a PR throws work away, and doing it in
   bulk throws it away faster than anyone can check. A loop, a pipe, a list read from a file,
   or simply two numbers in one command is refused outright with **no override** — that exact
   shape once nearly destroyed ~58 PRs' worth of real code, after an agent tagged 131 of them
   "redundant" from a heuristic. Closing **one** PR is allowed, but only with a marker naming
   that exact number, so every close is a separate deliberate act.
9. **Killing or restarting the app from a shell** — Omniscio already refuses agent-driven
   restarts through its own controls, but a shell command could route around them. One did:
   it found the app by its control port and killed it, taking 22 live sessions down with it,
   twice. Tidying up your own dev or test instance is untouched — only a blanket kill, or
   pinpointing the app by its port, is refused.
10. **Scheduling a task that pops a console window** — a scheduled task that runs a script
    directly draws a black window on your screen every time it fires. Omniscio ships a repair
    job for this, but it cannot save a one-shot task, which fires long before any repair runs.
    So it is caught at creation, with the one-line fix included in the message.

**About #7–#10 and false alarms.** These run before every shell command, so they are tuned to
stay quiet. #7 looks only at write targets you typed explicitly, judges each one in the folder
its own part of the command runs in, and stands down when it cannot be certain — a path the
shell hasn't filled in yet (`$TMP`, `~/x`, `%TEMP%`), a folder your environment names
(`cd "$TMP"`), a `>` that is really text inside quotes or a script body, a file the repo already
ignores, or a `cd` into a worktree first. The one doubt it no longer gives you: a relative write
after a folder the command works out for itself (`cd "$(…)"`, `cd -`) is refused, because that
folder could be `master` — give the write an absolute path, or `cd` to a literal folder first.
#8–#10 were tuned the same way. Between them they were checked against
**more than 51,000 real past commands**, and every wrong alarm found was fixed — including one
where _removing_ a scheduled task was mistaken for _creating_ one, and another where tidying
up your own test instance looked like killing the app.

Each of #9 and #10 has a one-time override you can add to a single command, and #8's
single-PR form has its per-PR marker. #8's **bulk** form deliberately has none — the whole
failure mode is an agent talking itself into a batch.

### Two more, added 2026-09-03

**#11 — a heavy test run that stayed on your machine.** `npm run test:agent` without `--cloud`
queues behind a slot gate on a box that may already be busy, so the guard asks you to add
`--cloud`. It only ever fires on this project's own test commands, so it can never interrupt you
in an unrelated project. Adding `--cloud` anywhere in the command clears it.

**#12 — staging everything at once.** `git add -A` and `git add .` sweep up every changed file in
the folder — including work another agent has in flight but has not finished. The guard asks you
to name the files you actually changed. `git add --update` and any specific path are untouched.

Both were tuned against the same lesson as #8-#10: writing _about_ a command is not running it.
A commit message, a note to a teammate, a heredoc, or a search of the docs that happens to
mention `npm run test:agent` or `git add -A` is text, and none of them trip these rules — that
was checked against every wrong alarm the earlier machine-local version of #11 produced.

Neither can be switched off on its own; that is true of every rule in this family. Use the
escape hatches below (advisory mode, an excluded repo, or the approval window) instead.

### One more, added 2026-09-03

**#13 — a shell command too long to reach the shell.** On Windows, the shell Claude Code uses
silently throws away everything past about 8,186 bytes of a single command. It does not warn and
it does not fail — it just runs a shorter command than the one you sent. When the cut lands
inside a quote (it almost always does), the shell then reports `unexpected EOF while looking for
matching '` at a line number where nothing is wrong, so the error blames your quoting for a
problem that is really about size. That one misleading message accounted for 124 reports on the
friction ledger; agents spent hours rewriting quotes and swapping heredoc markers chasing a bug
that was never there.

The guard now catches it first and says what actually happened, with your command's real byte
count and the limit. Two things make a command bigger than it looks, and both are in the message:
each apostrophe costs 4 bytes rather than 1 (of the way the command is wrapped), and an em-dash
or curly quote costs 3 — which is why a page of prose fails sooner than its length suggests.
Write file content with the Write tool instead of a `cat <<EOF` heredoc, or split the command up.

**No escape hatch lifts this one** — not advisory mode, not an excluded repo, not the approval
window. Nothing can make the shell accept more bytes than it accepts, so letting the command
through would only restore the silent truncation. It applies to Bash on Windows only.

It is not alone in that, so do not read it as the single unliftable rule. Three separate lists in
the code decide this: **`long-shell-command`, `raw-fleet-mutation`,
`destructive-remote-mutation`** (the `git remote` rule, #16 below), **`whole-tree-staging`** (a
`git add -A` / `git add .`), **`empty-branch-config-write`** (a config write naming an empty
branch), the two process-stop rules **`pattern-selected-stop` and `app-tree-stop`** (#17 and #18
below), the two repository-store rules **`delete-git-store` and `reinit-existing-repo`** (#19 and
#20 below), **`auth-artifact-write`** (a write, move or delete of the permission files themselves —
see the override section below) and **`foreign-worktree-write`** (a write into a worktree that is
someone else's — a warning is a no-op once the folder is not your own working tree)
**ignore advisory mode** (always enforced); **`long-shell-command`,
`wedged-tree-commit` and `sparse-phantom-deletion` can never be lifted by a grant**; and the
**worktree lockdown** and **`auth-artifact-write`** sit above the hatch line entirely, so no
exclusion, pause or window reaches them either (see the worktree section below) — nor the two
repository-store rules, which are never handed a hatch at all. If you are
deciding whether an exclusion or an approval window will get you past a refusal, check which of
those the rule is in rather than assuming it is only this one.

### Two more, added 2026-09-04

**#14 — a git command hidden inside a script file.** The guard used to read only the command an
agent typed. So `git push --force origin master` was caught, but putting that same line in a
file and running `node cleanup.mjs` was not — the command it saw was just `node cleanup.mjs`,
which contains no git at all. Measured against the shipped guard, exactly that shape was allowed
on a master checkout, silently, with no warning of any kind.

It now opens the script a command runs — `node x.mjs`, `bash x.sh`, `powershell -File x.ps1` —
and judges what is inside by the same rules. When a block comes from a file, the message says so
and names the file, so you are never told your harmless-looking command was a git push without
being able to see why.

The half of this that reads code passed _inline_ (`bash -c '…'`, `node -e '…'`) already shipped
on 2026-09-03; this is the other half. A script that merely _mentions_ a git command in prose is
still fine — the guard only counts it when the script can actually run something.

**#15 — a process kill that would take out every agent's test run.** Test and check runs don't
carry any marker saying which agent they belong to, so "kill the processes matching this script
name" hits everyone's, not just yours. On one day in August three such sweeps landed here: one
killed up to 67 processes, another ended 12 other sessions' in-flight runs. Those runs die with
no result at all, so those agents simply start over — one command, many lost hours.

The rule isn't "never kill". It's "show that you mean your own run", and either proof is enough:
name the process id, or name your own worktree in the filter. Every read-only way of _listing_
processes is untouched.

### One more, added 2026-09-11

**#16 — a `git remote` change that reaches every worktree.** `git remote remove`, `rm`, `rename`,
`set-url`, `set-head`, `set-branches`, and `prune` edit the repo's ONE `.git/config` (or the
tracking refs every checkout reads), and a git worktree _shares_ that file with the main checkout
and every sibling worktree. So typing `git remote remove origin` inside your own feature-branch
worktree does not remove _your_ origin — it removes origin for the whole repo. That happened on
2026-09-11: one session believed an earlier `git fetch . <refspec>` had registered a remote to
clean up (it had not — a fetch by path writes a ref, never a remote), removed `origin`, and every
`fetch` / `push` / `gh pr list` across the fleet failed with `no git remotes found` until it was
restored by hand. Removing the promisor remote also drops the partial-clone setting, so even a
bare `git remote add` afterwards is not a full repair.

Rule #1 already refused these verbs — but only while standing **on master**, which is the one
place no agent works. This rule refuses them from **any branch**, and only when the repo's
config is genuinely shared: a linked worktree, or a main checkout that has linked worktrees. A
standalone checkout — every throwaway fixture repo the test suites create in a temp folder — is
left alone, so `git remote add` / `remove` on a scratch repo still works. `git remote add` of a
_new_ remote is allowed everywhere (it is additive, undone by one command, and the first step of
the restore), as is `git remote update` (a fetch) and a `prune --dry-run`.

The message says what to do instead: a scratch ref belongs under `refs/tmp/<name>/` and is
dropped with `git update-ref -d`, never with `git remote remove`. An excluded repo or an
approval window lifts it like the other rules; an agent cannot lift it for itself, because the
damage lands on every _other_ agent's worktree. Unlike most rules, it does **not** follow the
enforcement level: it refuses at **advisory** just as at **required** (the same always-enforced
class as the over-long-command rule), because the incident happened on a box set to advisory,
where a warn would only have logged the veto while origin was deleted for everyone. Only the
human escape hatches — an excluded repo or an approval window — let it through.

### Two more, added 2026-09-25

**#17 — a process stop that picks its targets by pattern.** On 2026-09-25 an agent stopping its own
build ran one command: list every `node.exe` whose command line contains `vite`, then stop each one.
Omniscio's own launcher is `npx electron-vite dev`, so the filter matched it too — and the app, with
every live session, died with its launcher. A name, a wildcard, a command-line filter or a pipeline
cannot tell your process from the app's or another session's, and the guard cannot check what a
filter will match, so any stop chosen that way is now refused: `Stop-Process -Name`, `taskkill /IM`,
`pkill`, `killall`, `… | Stop-Process`, `ForEach-Object { Stop-Process -Id $_.ProcessId }`,
`kill $(pgrep …)`, and a variable filled from a process listing in the same command.

What still works, and what the refusal tells you to do: stop the exact process ids you mean.
`Stop-Process -Id 1234`, `taskkill /PID 1234`, `kill 1234`, `kill $!`, a pid file you wrote when
you started the process, the object `Start-Process -PassThru` gave you, or a loop over a literal
list of ids. If you do not know the id, LIST first (read-only), check each row is yours, then stop
those exact ids in a second command. Best of all, keep your child's id when you start it.

**#18 — stopping the app itself, by its exact id.** Listing processes and copying the launcher's
id into `Stop-Process -Id` would take the app down just the same. So the app tells every session it
starts which processes are its own — its main process and every process it was launched through,
worked out from its live process id, never from names — and a stop of any of them is refused. A
cloud session runs on another machine and gets no such list. If you truly asked for a restart,
the same one-time override as #9 lifts this rule (and only this one).

Both refuse even in advisory mode. Two fixes rode along. First, the guard used to lose the rest of
a `powershell -Command "…"` after the first escaped quote (`\"`) — which is exactly where that
day's stop was hiding. Second, when two rules had something to say about one command, a warning
from the first used to end the check before a later rule could refuse.

### Two more, added 2026-09-26

**#19 — deleting a repository's own `.git`.** On 2026-09-25 a review session whose working folder
was the main checkout ran a command meant for a temp folder. A single-quoted path never expanded,
so the `cd` failed and the delete after it ran in the checkout itself, removing its `.git` — and
with it every commit not yet pushed anywhere. Any delete, move or rename that would take a
repository's `.git` with it is now refused: `rm -rf .git`, `Remove-Item -Recurse`, `rd /s`, `del /s`,
`mv` / `Move-Item` / `Rename-Item` / `ren` of the `.git` itself (a worktree's `.git` file included), a
glob that empties it (`rm -rf .git/*`), a folder that holds a repository, and the same inside
`bash -c`, `powershell -Command`, `cmd /c`, `node -e` or `python -c`. The guard works out where the
command really runs: after `cd x && …` it judges `x`, and after a `cd` that may fail followed by `;`
it judges the folder the shell was already in as well.

**#20 — `git init` inside an existing repository.** The same session then ran `git init` in the
checkout and committed into the new, empty store, and three branches were landed into it. `git init`
is now refused whenever its target sits inside a repository, however the target is named: the
current folder, a folder argument, `-C`, `--git-dir`, `--bare`, `--separate-git-dir`, or `GIT_DIR=`.

Both refuse even in advisory mode, and no exclusion, pause or approval window lifts them — a warning
cannot bring a deleted store back. Throwaway repositories are unaffected: anything under your
session's scratch folder (`$AMC_SESSION_TMP`) or the system temp folder can be created,
re-initialised and deleted freely, which is also the one-line hint every refusal carries. A path only
the shell can work out — `"$T/.git"`, `git init "$(mktemp -d)"` — is left alone rather than guessed at.
A related fix rode along: a warning from a git rule (a write on `master`, say) used to end the check
before these rules ran, so on an advisory box `rm -rf .git && git init` in the main checkout was only
warned about. The warning now waits until every always-enforced rule has had its say.

### Advisory vs. required

Two enforcement levels (in the Dev Pipeline panel → Setup, or Settings → Features):

- **Required (hard-block)** — the default. A gated command is refused; the agent sees a
  short explanation and can't run it.
- **Advisory (warn-only)** — the command is _allowed_, but the agent gets a warning on
  stderr explaining why it's risky. Useful for trying the feature without changing what
  actually happens.

You can also set which branch names count as protected (a comma-separated list, default
`master` + `main`), and opt into the extra ready-to-merge push gate — all from the same
section.

### When an agent is blocked

The block surfaces to the agent as a short, friendly message naming the rule, the reason,
and the branch/repo — plus how to override it. For example, a blocked push prints roughly:

```
git-guardrails BLOCKED: push-to-protected
  git push targets a protected branch (master/main) directly —
  pushing straight to a shared branch is gated
  branch: master   repo: C:/code/my-project
Do NOT ask the user to run this command in a terminal. Ways forward:
  1. Work on a worktree branch: POST http://127.0.0.1:19519/worktrees/create
  ...
```

The wording matters more than it looks. It used to name only a JSON file path, so a blocked
agent — with no sanctioned way to ask — would improvise and tell you to open a terminal and
run the git command yourself. It now names every route forward and says explicitly not to
hand the command back to you — and it is kept deliberately SHORT: every block message lands
in an agent's context, so a length ratchet (`tests/unit/lint/guard-message-length.test.ts`)
caps each refusal at roughly two lines.

### Overriding (escape hatches)

Two overrides short-circuit the guardrails to "allow", driven from the **Git guardrails**
controls (Settings → Features or the Dev Pipeline panel) and stored in the exact file formats
the existing PowerShell "block-master" guards already use.

> **These override files are a RAIL, not a sandbox — be honest about what that means.** The
> guardrails are a policy layer an agent is expected to respect, not a boundary that can hold
> against one that does not. Since 2026-09-25 the guard refuses an agent's write, move or delete
> of `~/.claude/.master-approval.json`, `~/.claude/.master-block-exclusions.json`, the PR-push
> grants in `~/.claude/.push-approvals.json` and the installed bundle under
> `~/.claude/hooks/developer-guardrails/` at every enforcement level (rule `auth-artifact-write`),
> and it honours a window only in the shape the app and `approve-master.ps1` write it — since
> 2026-09-26 the same for a per-session pause, the agent's self-pause (never past its 10 minutes)
> and the worktree break-glass, and a hard link to any of these files is refused too. But it
> judges a command by reading it: a write through a form it does not model yet (`truncate`, or
> code run by an interpreter such as `perl -i`) still gets through, so an agent with a shell can
> still forge one — an in-place `sed -i` typed as its own command is no longer one of them (one run
> through `find -exec` or `xargs` still is). That residual has been carried
> openly since the per-session pause shipped,
> and closing it fully needs the in-git allowlist flip, not a hook rule.
> (The repo's own contract states this the same way: "a rail, not a sandbox … one with a shell can
> write the break-glass file directly — the same residual every override file has carried".)
>
> **Why that residual matters, and why it is still the right shape:** on 2026-08-25 an agent
> blocked from editing master issued itself a permission slip and retried — `~/.claude` is not a
> git repo, so the branch check came back empty and nothing objected. The answer is that opening a
> window is a deliberate, attributable act with a narrow sanctioned form (the app's own controls,
> or a short `-Minutes 1` self-lift for one read-only op, revoked immediately), and an agent that
> writes one to get past a refusal is violating the rule regardless of whether anything stopped it.
> Do not read the absence of a refusal as permission.

- **Scoped to one agent (optional).** A window may name a single agent session; that agent gets
  through and **every other agent on the machine stays blocked for the window's whole life**.
  Before this, one approval granted for one operation opened every guardrail for the whole fleet
  for up to four hours. A window with no name is still deliberately machine-wide.

- **Pause the guardrails for a while** — a time-boxed window (`~/.claude/.master-approval.json`:
  its expiry, when and by whom it was granted, and for how many minutes — a file missing any of
  those is not a window). "Pause for N minutes" opens it (1–240, with a live countdown);
  "Resume" closes it. While it's active, everything is allowed — use it for a deliberate,
  user-authorized burst of protected-branch work.
- **Exclude a repo** — a per-repo list (`~/.claude/.master-block-exclusions.json`,
  `{"repos": ["C:/path/to/repo", …]}`). Add or remove a repo from the app; a listed repo (or
  one nested under it) is never guarded.

**Every control that LOWERS the guardrail asks you first** (2026-08-27). Pausing, exempting a
folder, and switching from _Block_ to _Warn only_ each show a confirmation that names the real
scope — "for you AND every AI session on this computer", "everything inside `<path>`". Tightening
back up needs no confirmation. Until then it was the other way round: the only confirmation was on
_removing_ an exemption, i.e. the one action that RESTORES protection.

**A whole drive is refused as an exemption.** A listed folder exempts everything inside it, so
typing `C:\` would have silently unguarded every repository on the drive while reading as one more
unremarkable row in the list. The refusal lives in the service, not just the box, so the same
answer comes back through the app, the API and an agent alike.

**These two controls stay human-only.** Because a machine-wide pause or a repo exclusion lowers
the guardrail everywhere, the app never exposes either to the CLI control server or the
mobile/web bridge. (You can still hand-edit the JSON files, or use the PowerShell helpers,
exactly as before.)

An agent has its own, much narrower path — added because the strict version had a cost you were
paying without seeing it: an agent you had _already_ told to go ahead would get blocked, have no
way to act, and hand the work back to you as a command to run by hand. It can now:

- **Re-run one command** with a `CLAUDE_ALLOW_MASTER=1` prefix, when you approved that exact
  command in conversation. It says so on-screen every time it uses it.
- **Open a short window for itself** — 10 minutes, 3 an hour, always with an inbox note.

Neither lifts a push to a remote or a destructive stash. Those, and anything longer, still come
to you as a one-click Approve card.

### Limits (v1)

- Guards the sessions **Omniscio spawns**, not a plain terminal Claude session you start
  yourself. For that, turn on the machine-wide sibling — [Developer
  guardrails](developer-guardrails.md) — which installs this same engine into your global Claude
  config so **every** Claude Code session on the machine is covered. It is opt-in (off by
  default), and it shares this feature's pause and exclusion escape hatches.
- Doesn't guard an SSH/remote session whose remote checkout has no hook.
- It's a shell-command rail — deliberate obfuscation can evade it (see "not a sandbox").
- Script-file reading goes **one level deep**: it opens the script your command runs, but not a
  further script that script runs. Following the chain without limit would mean unpredictable
  disk reads before every single command.
- It recognises git however it's spelled on the command line — `git`, `git.exe`, `git.cmd`, a
  full path like `C:\Program Files\Git\bin\git.exe`, and PowerShell's `& "<path>"` call form
  all get the same rules. (Until 2026-08-25 only the bare word `git` was recognised, so the
  exe and full-path spellings slipped past every rule unguarded — fixed and covered by tests.)
  Look-alike tools such as `gitk` and `git-foo` are still left alone.

## For agents

### How it works (for agents with repo access)

- **The check is a Claude Code PreToolUse hook** — `resources/git-guardrails/git-guardrails.mjs`
  in the repo (repo access required) — self-contained Node code (zero app imports) that Claude Code runs _before_ every
  Bash / Edit / Write tool call. It is four modules — the entry above plus `guard-shell` (the
  command model), `guard-rules` (the rules that are not about a branch) and `guard-decision`
  (violation → verdict) — re-exported from the entry, so it is still one import and one process.
  A PreToolUse hook is used deliberately: it fires **even in
  `bypassPermissions` / autonomous mode**, where an Omniscio-side permission card would be
  auto-approved and never seen. Exit code `2` blocks; `0` allows (warn writes to stderr and
  still exits `0`).
- **Omniscio wires it in at spawn** without touching your repo: for each session it launches, it
  writes a **session-private settings file** carrying only the `hooks.PreToolUse` entry and
  passes it to the CLI with `--settings <path>` (which _merges_ with your own settings), and
  sets the hook's config in the `AMC_GIT_GUARDRAILS` environment variable. This mirrors how
  Omniscio writes a private `.mcp.json` and passes it via `--mcp-config`. The wiring lives in
  `src/main/process/git-guardrails-spawn.ts` and `src/main/process/spawn-cluster-manager.ts`
  (repo access required).
- **Fail-open by design.** Any error in the hook, or a missing bundled script, or a broken
  config → the operation is _allowed_. A safety rail must never wedge a session. The one
  exception: a tool call whose details have not fully arrived within 30 seconds is refused
  (`payload-incomplete`) — nothing in it could be checked, so it is never waved through; run
  the same call again.
- **Gated + solo-first.** Injection only happens when `gitGuardrailsEnabled` is on (the
  default; the spawn gate keys on the setting directly, so turning it off truly disables it);
  it's a per-user setting in v1.
  Org-enforced / admin-set team policy is a documented follow-up (the desktop has no
  org/team awareness yet).

The full behavior lock lives in `git-guardrails-contract.md`, under `.claude/memory/contracts/`
in the repo (repo access required) — it is the authoritative spec this page summarizes.

## Related

[Developer guardrails](developer-guardrails.md) is the machine-wide sibling: the same discipline, but written into your global Claude settings so it covers every session on the computer rather than just the ones Omniscio spawns.
