Developer Guardrails
The machine-wide sibling of Git guardrails: a set of rules written into your global Claude Code settings so every session on the computer — not just the ones Omniscio spawns — is held to them, with escape hatches and a warn-only mode.
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. 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 andapprove-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. - An agent's own short pause —
~/.claude/.guardrail-self-pause.json, written when an agent callsPOST /git-guardrails/self-pause. Capped at 10 minutes and budgeted to a few per hour. It is deliberately NARROWER than a human grant: it does not lift agit push, a destructivegit stash, a forced worktree removal, or a destructivegit remoteverb, and it can never reach the approval files themselves. The guard passes the grant's kind to the engine's classifiers rather than a yes/no, which is what keeps that narrower scope intact — see git-guardrails-invariants-self-pause-grants-and-override-contract.md. (Honoured here since 2026-10-02; before that the app granted the pause and this guard ignored the file it wrote, so the pause appeared to do nothing.) - 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.jsonlets 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:~/.claudeis 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:
- Commit / write while on a protected branch (
master/main) —commit,reset,rebase,merge,cherry-pick, a branch delete, etc. - Push that targets a protected branch — e.g.
git push origin master, from any branch. - Destructive
git stash—push/pop/drop/clear/apply/saveand baregit stash. Read-onlygit stash list/showare allowed. - Editing a file while the repo is on a protected branch — the same rule as #1, for the agent's file-editing tools.
- 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. - Destructively closing pull requests — a bulk or scripted close is refused outright with no override; closing one needs a marker naming that exact PR.
- 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.
- Scheduling a task that pops a console window — caught at creation, because the repair job cannot save a one-shot task.
- Printing — or overwriting — the full-trust CLI key — the master key at
~/.amc/cli-tokenmust never reach a tool result, and must never be clobbered. AReadorGreppointed at that file (or at the.amcdirectory holding it) is refused, as is aWrite/Editonto 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 nameamc-cli, so a bareget-secret.ps1 amc-cliis 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. A loop around the send is fine:TOKEN=$(cat ~/.amc/cli-token)followed byfor k in a b; do curl -H "Authorization: Bearer $TOKEN" …; doneis allowed, because the loop's ownfor … in …,doanddonerun nothing — its body is held to the same rule, and a loop header whose word list expands anything ($T,$( … ), a glob) is refused. The client may be spelled however the shell runs it —curl,curl.exe,/usr/bin/curl, or PowerShell'sirm. 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)"oncurl,wgetorInvoke-RestMethod. A client is only trusted when the key is actually sent — the header form above; merely carrying it, as incurl $(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, socurl -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,envwith no argument) is refused too.curl -vis refused: its debug output prints the outgoing request line, header and all. This one exists because the key reached a dozen transcripts through a barecat, acat … | 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 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
nodeon 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 (protectmaster/main, block, ready-tag on). Exit code2blocks;0allows. 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)
deploys the bundle and injects ONE marked
PreToolUseentry (matcherBash|PowerShell|Edit|Write|MultiEdit|NotebookEdit|Read|Grep, commandnode) 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 +
.ps1files are removed and replaced with the single Node entry.
The full behavior lock lives in .claude/memory/contracts/developer-guardrails-contract.md.
Related
Git guardrails is the per-session sibling: same idea, but it protects the sessions Omniscio spawns rather than every session on the machine.
Last verified 2026-10-02