---
title: Developer Guardrails
---

# Developer Guardrails

## What it is

> **Status: shipped, OPT-IN (default OFF).** Turn it on in **Settings → Features → Developer
> guardrails (machine-wide)**. Because it edits your GLOBAL `~/.claude/settings.json`, it stays
> opt-in — nothing is installed until you flip it on, and turning it off removes it cleanly.
> Cross-platform (Windows / macOS / Linux).

Developer guardrails is the **machine-wide** sibling of [Git guardrails](git-guardrails.md). Where
Git guardrails protects the sessions **Omniscio spawns** (wired per-session at launch, on by
default), Developer guardrails protects **every Claude Code session on the machine** — including
ones Omniscio never launched (a plain `claude` you start in a terminal, another tool's session,
a cron job). It does this by installing **one** safety hook into your GLOBAL `~/.claude/settings.json`,
which every Claude Code session reads.

It's the built-in, one-switch, cross-platform version of the hand-installed "block-master" hooks
some power users wire into their global config by hand: instead of maintaining your own scripts,
Omniscio deploys the check and keeps it healthy for you.

**It reuses the Git guardrails engine** — it does not re-implement the rules. The same
classifiers that power the per-session feature are shared, so the two behave identically on the
rules they both enforce.

**It is a safety rail, not a security sandbox** — the same caveat as Git guardrails. It catches
the common, honest cases; a determined agent that deliberately obfuscates a command can evade a
shell-command checker.

## Where to find it

### Escape hatches (shared with Git guardrails)

The guard reads the **same** four override files as Git guardrails (the hand-installed PowerShell
guards read the approval window and the exclusion list), so one grant covers all of them:

- **Pause** — a time-boxed window in `~/.claude/.master-approval.json`. While active, everything is
  allowed. A window counts only in the shape its two writers produce — the app's Approve and
  `approve-master.ps1` — stamped, with its grant time and a length of 1 to 240 minutes. A longer or
  hand-shaped window is refused outright, never shortened to fit.
- **Scoped to one agent (optional)** — the marker may name a single agent session. That agent gets
  through; every other agent on the machine stays blocked for the window's whole life.
- **Pause one agent** — a human's per-session pause in `~/.claude/.guardrail-session-pause.json`
  (the inbox Approve) lifts the rules for that one agent only, read with the same shape test.
- **Exclude a repo** — a per-repo list in `~/.claude/.master-block-exclusions.json`
  (`{"repos": ["C:/path/to/repo", …]}`). A listed repo (or one nested under it) is never guarded.
- **Grant one push** — a PR-push grant in `~/.claude/.push-approvals.json` lets one plain,
  non-force push of one branch at one commit through without a ready-to-merge tag, exactly as Git
  guardrails honours it. The hand-installed PowerShell guards do not read this file.

> **These files are guarded against the agents they constrain.** An agent tool call that writes
> any of them — or the deployed bundle in `~/.claude/hooks/developer-guardrails/` — is refused,
> through the edit tools and the shell alike, and refused even while a window is open so one
> approval cannot authorize the next. Reading is still allowed. Added 2026-08-25 after a blocked
> agent was found writing its own approval window and retrying the edit: `~/.claude` is not a git
> repo, so the protected-branch check never applied to it.
>
> **What this does not close:** a write the guard cannot see — a script or helper program that
> writes the file itself — can still leave one of these files behind, and the stamp on a window is
> its shape, not proof of who wrote it. The write rule covers the one-command routes an agent
> uses; it is a guardrail, not a sandbox.


## How it behaves

### What it blocks

When enabled, these operations are gated in **any** Claude Code session on the machine:

1. **Commit / write while on a protected branch** (`master` / `main`) — `commit`, `reset`,
   `rebase`, `merge`, `cherry-pick`, a branch delete, etc.
2. **Push that targets a protected branch** — e.g. `git push origin master`, from any branch.
3. **Destructive `git stash`** — `push` / `pop` / `drop` / `clear` / `apply` / `save` and bare
   `git stash`. Read-only `git stash list` / `show` are allowed.
4. **Editing a file while the repo is on a protected branch** — the same rule as #1, for the
   agent's file-editing tools.
5. **A shell command that writes a file into a protected checkout** — the shell half of #4
   (a `>` redirect, `curl -o`, `tee`, `cp`/`mv`, a PowerShell write cmdlet). Gating only the
   edit tools left an obvious way around them, and it is how stray untracked files end up in
   a repo root. Tuned to stay quiet — it stands down on anything it cannot resolve (`$TMP`,
   `~/x`, a `>` inside quotes or a script body) and on files the repo already ignores.
6. **Destructively closing pull requests** — a bulk or scripted close is refused outright with
   no override; closing one needs a marker naming that exact PR.
7. **Killing or restarting the app from a shell** — the shell path around the restart gate the
   app already enforces. Tidying up your own dev instance is untouched.
8. **Scheduling a task that pops a console window** — caught at creation, because the repair
   job cannot save a one-shot task.
9. **Printing — or overwriting — the full-trust CLI key** — the master key at `~/.amc/cli-token`
   must never reach a tool result, and must never be clobbered. A `Read` or `Grep` pointed at that
   file (or at the `.amc` directory holding it) is refused, as is a `Write`/`Edit` **onto** it (a
   model that hit a 401 and decided to "repair" the token file would otherwise break every agent's
   auth on the box), and so is a shell command naming it **outside** a `$( … )`. **The same value
   has a second door**, and it is closed too: the DPAPI vault holds the identical token under the
   name `amc-cli`, so a bare `get-secret.ps1 amc-cli` is refused as well — the capture forms the
   repo's own docs use (`TOKEN=$( … get-secret.ps1 amc-cli … )`) stay allowed, because putting the
   value in a variable is not printing it. The form every overseer and agent actually uses — `-H
   "Authorization: Bearer $(cat ~/.amc/cli-token)"`, or assigning it to a variable inside the same
   command — prints nothing and stays allowed; a substitution whose value is only passed to a program
   is allowed too. **Inside a command the key may only be CAPTURED or SENT — and once it is captured,
   that is the only thing the REST of the command may do with it.** A capture followed by an unrelated
   command (`TOKEN=$(cat ~/.amc/cli-token) && git status`) is refused, because an arbitrary command
   after a captured key cannot be shown to be safe; send the value in a header instead. The client may
   be spelled however the shell runs it — `curl`, `curl.exe`, `/usr/bin/curl`, or PowerShell's `irm`.
   Anything else is
   refused — `echo`, `printf`, `sed`, a display cmdlet, an interpreter, a debug flag, a verb the
   guard does not recognise — because a form that cannot be shown to be safe is not assumed to be.
   The two shapes that work, and every refusal names them: `TOKEN=$(cat ~/.amc/cli-token)`, and
   `-H "Authorization: Bearer $(cat ~/.amc/cli-token)"` on `curl`, `wget` or `Invoke-RestMethod`.
   A client is only trusted when the key is actually **sent** — the header form above; merely
   carrying it, as in `curl $(cat ~/.amc/cli-token)`, is refused, because curl echoes a URL it cannot
   resolve back into the model's view. **A flag elsewhere in the command is not the same as the key
   being in it**, so `curl -H "Accept: x" "$(cat ~/.amc/cli-token)"` is refused too: the key has to
   sit in the flag's own argument. A **quote, a backslash, a PowerShell backtick or bash's ANSI-C
   operator** (`$'\x63'`) inside the path is caught too — the guard reassembles the word the way the
   shell will before it matches, and it does that on every call, with no cheaper pre-check that a
   spelling could slip past. The debug flags that echo a request are refused per client
   (`curl -v`, `wget -d`, `Invoke-RestMethod -Debug`), and a captured key that a later command dumps
   back out (`printenv`, `set`, `env` with no argument) is refused too.
   `curl -v` is refused: its debug output prints the outgoing request line, header and all. This one
   exists because the key reached a dozen transcripts through a bare `cat`, a `cat … | head -c 20`
   "to check it exists", and a Read on the file. **The value is guarded too, not just the file:** a
   call carrying the key's actual value — `curl -H "Authorization: Bearer <the value>" …` — is
   refused as well, because a call's own arguments are recorded even when the command prints nothing,
   and nothing in that text names the file the guard was watching. It applies to every tool, so a
   command, a file body being written, or a search pattern all count. The guard reads the key itself
   to do this, in its own process, and never puts it in a result. **One known over-refusal, on purpose:** PowerShell's natural capture, `$token = Get-Content
   ~/.amc/cli-token`, assigns rather than prints and so is refused too — the guard masks bash's
   `$( … )` and PowerShell needs no subexpression for a capture. Use `$(Get-Content ~/.amc/cli-token)`
   instead; [docs/cli-control.md](../../docs/cli-control.md) teaches that form.

Rules 6–9 are branch-independent and are NOT lifted by a pause — each carries its own one-time
override instead, so lifting one is a single deliberate act. Rule 9 has no override at all: it also
runs *before* the pause and exclusion files are read, so neither can stand it down.

**Always allowed:** reads (`git status` / `log` / `diff` / …), moving _onto_ a protected branch,
creating a branch, `git fetch`, and — critically — **`git worktree add`** (the sanctioned way off
`master`).

**The ready-to-merge push gate is on here.** A push of a feature branch needs the branch's current
SHA-bound ready-to-merge tag (the one `/ready-to-merge` stamps after a real test run); a tagged push
goes through, and an untagged push — or one whose branch moved after it was tagged — is refused,
unless the owner granted that push in `~/.claude/.push-approvals.json` for that branch at that
commit: a granted plain, non-force push goes through too. The tag is read in the repo the push
actually runs in. The gate covers **`gh pr create`** as well as `git push` — publishing the same
commits as a pull request is the same act — and an **approval window** or an **excluded repo**
lifts both commands the same way. Git guardrails applies the same gate, and honours the same
grants, per session. The protected branches are fixed at `master` / `main`.

### Developer guardrails vs. Git guardrails

|                     | Git guardrails                    | Developer guardrails                 |
| ------------------- | --------------------------------- | ------------------------------------ |
| Scope               | Sessions Omniscio spawns          | EVERY Claude session on the machine  |
| Wiring              | Per-session `--settings` at spawn | The global `~/.claude/settings.json` |
| Default             | ON                                | OFF (opt-in)                         |
| Ready-tag push gate | Per-session (team default on)     | On                                   |
| Rule engine         | `git-guardrails.mjs`              | Reuses `git-guardrails.mjs`          |

Running both is harmless — on an Omniscio-spawned session they simply double-check the same rules
(first to block wins).

### Limits (v1)

- Protected branches are fixed at `master` / `main` (not yet configurable, unlike Git guardrails).
- It edits your global config, so it stays opt-in; a determined agent can still evade a
  shell-command rail (see "not a sandbox").
- Requires `node` on PATH for the session's hook execution (the same assumption Git guardrails
  already ships with).

## For agents

### How it works (for agents with repo access)

- **The check is a Claude Code PreToolUse hook** deployed to
  `~/.claude/hooks/developer-guardrails/` as a two-file Node bundle: a thin entrypoint
  (`developer-guardrails.mjs`) plus a **co-versioned copy of the Git guardrails engine**
  (`git-guardrails.mjs`) it imports as a sibling. The entrypoint reuses the engine's exported
  classifiers with a baked-in rule config (protect `master`/`main`, block, ready-tag on). Exit
  code `2` blocks; `0` allows. **Fail-open** — any error allows the operation — with one
  deliberate exception: a tool-call payload still arriving after 30 s cannot be checked, so it is
  **refused** (`payload-incomplete`), never waved through. The hook's budget is 60 s. Every refusal
  is recorded as one line in `~/.amc/guard-decisions.log` (rule and git verb, never the command).
- **The machine-local PowerShell guards still get their say.** On a machine that has them, a
  Bash/PowerShell call a guard could fire on is handed to `hookguard.exe`, which serves it from its
  resident guard host rather than starting PowerShell cold.
- **Omniscio installs it into the GLOBAL config** (not per-session): when the setting is on, the
  manager ([developer-guardrails-manager.ts](../../src/main/services/developer-guardrails/developer-guardrails-manager.ts))
  deploys the bundle and injects ONE marked `PreToolUse` entry (matcher
  `Bash|PowerShell|Edit|Write|MultiEdit|NotebookEdit|Read|Grep`, command `node`) into
  `~/.claude/settings.json`. It writes atomically with a one-time backup, self-heals on a ~15-min
  tick, and reverts cleanly when the setting is turned off — leaving every other hook untouched.
  The matcher is part of what "already installed" means: a matcher that predates a later widening
  is treated as STALE and re-injected, because a matcher decides which tools the hook is even
  spawned for, and a stale one leaves the guard it was widened for dead on arrival.
- **Self-test before wiring.** Before it commits the settings change, the manager runs the deployed
  entrypoint against a benign payload (must allow) AND a deterministic `--selftest` (must block),
  so a broken engine import can never silently leave you unprotected.
- **Migration.** A machine that ran the earlier Windows-only PowerShell version is migrated
  automatically on the next tick: the stale PowerShell hook entries + `.ps1` files are removed and
  replaced with the single Node entry.

The full behavior lock lives in
[.claude/memory/contracts/developer-guardrails-contract.md](../../.claude/memory/contracts/developer-guardrails-contract.md).

## Related

[Git guardrails](git-guardrails.md) is the per-session sibling: same idea, but it protects the sessions Omniscio spawns rather than every session on the machine.
