Prevent Unsafe AI Edits: Guardrails That Actually Hold

Polite CLAUDE.md rules don't stop an agent from touching .env or prod config. The enforced layers that do: deny rules, ignore files, sandboxing, hooks, branch protection.

The first time an AI agent edits prod.config.ts, deletes .env.local, or “fixes” a migration file, you learn that a polite line in CLAUDE.md is a suggestion, not a fence. The agent that wrote the line forgot it three turns later. This guide separates the rules a model tries to follow from the boundaries your tooling enforces — and shows the exact config, as of June 2026, for Claude Code, Cursor, and Codex.

TL;DR

  • A rule in CLAUDE.md / AGENTS.md shapes intent but enforces nothing. The model can ignore it; only the harness blocks tool calls.
  • Real enforcement lives in deny rules (Claude Code settings.json), ignore files (.cursorignore, .codexignore), sandboxing, PreToolUse hooks, and server-side branch protection + push protection.
  • Layer them: ignore files keep secrets out of context, deny rules block edits, a secret scanner catches what slips through, branch protection means the agent can never push to main directly.
  • Solo or team, the cheapest high-value move is a 10-line permissions.deny block plus gitleaks as a pre-commit hook.

Who this is for

Anyone giving Cursor, Claude Code, or Codex write access to a real repository — solo devs, small teams, and especially anyone whose repo touches secrets, infrastructure, or production data. If your only guardrail today is “I asked the agent nicely,” start here.

Why “ask nicely” fails

Permission rules in Claude Code are enforced by the harness, not the model. Anthropic’s own docs say it plainly: instructions in your prompt or CLAUDE.md “shape what Claude tries to do, but they don’t change what Claude Code allows.” That single sentence is the whole problem with prompt-only guardrails:

  • Agents reset between sessions; verbal rules don’t persist.
  • Long context degrades — rules near the top get echoed back, rules buried in the middle get dropped.
  • Cursor, Claude Code, and Codex each honor different conventions, so one written rule doesn’t port across tools.
  • Even with rules present, an agent will rationalize an edge case (“I had to touch this to fix the bug”).
  • A rule with no enforcement mechanism behind it is a hope, not a guardrail.

The four enforcement layers

Think in layers, cheapest first. Each one catches what the layer before it missed.

LayerWhat it doesWhere it livesBypassable by the model?
Ignore fileRemoves files from the agent’s context entirely.cursorignore, .codexignore, project configNo — the agent never sees them
Deny rule / allowlistBlocks the edit/command at the tool boundaryClaude Code settings.json, Cursor allowlistNo — enforced by the harness
SandboxOS-level filesystem/network limit on shellClaude Code sandbox, Codex workspace-writeNo — even if a prompt injection slips through
Branch protection + push protectionStops the change from landing in mainGitHub server-sideNo — runs server-side, not locally

The first two stop the agent from making a bad change; the last two stop a bad change from landing. You want both halves.

Claude Code: deny rules in settings.json

Claude Code evaluates permission rules in the order deny → ask → allow; the first match wins, and a deny at any settings level can never be overridden by an allow at a lower level. Put a deny block in .claude/settings.json at the repo root (commit it so the whole team gets it):

{
  "permissions": {
    "deny": [
      "Read(.env)",
      "Read(.env.*)",
      "Read(./src/lib/secrets/**)",
      "Edit(.env)",
      "Edit(.env.*)",
      "Edit(/infra/**)",
      "Edit(/migrations/**)",
      "Edit(/prisma/schema.prisma)",
      "Bash(git push --force *)",
      "Bash(npm publish *)",
      "Bash(rm -rf *)"
    ]
  }
}

Two details that bite people:

  • Read and Edit patterns follow .gitignore semantics, not glob-from-root. A bare Read(.env) is equivalent to Read(**/.env) and matches .env at any depth. A leading / anchors to the project root (Edit(/infra/**)), and a leading // is the only way to write a true filesystem-absolute path.
  • A Read/Edit deny rule also covers Bash file commands Claude Code recognizes — cat, head, tail, sed — but not an arbitrary script (a Python or Node program that opens the file itself). For that, enable the OS-level sandbox (/en/sandboxing in the docs), which merges your deny rules into the real filesystem boundary.

Avoid the trap of running with --dangerously-skip-permissions (a.k.a. bypassPermissions mode) on a real repo. It skips prompts for most paths; explicit deny rules still apply, but you’ve thrown away every interactive checkpoint. Reserve it for a container or VM. Admins can hard-disable it with permissions.disableBypassPermissionsMode: "disable" in managed settings.

For an absolute block on a handful of commands while otherwise running fast, allowlist Bash and add a PreToolUse hook that exits with code 2 to reject the specific commands — a code-2 exit stops the call before permission rules are even evaluated.

Cursor: the denylist is gone — use an allowlist

If you set up Cursor before 2026 with an Auto-Run denylist, rip it out. Security researchers (Backslash) found at least four ways a compromised agent could slip commands past it, and Cursor deprecated the denylist in release 1.3. The modern default on Cursor 3.6 and above is Auto-review: it runs allowlisted commands, sandboxes what it can, and routes anything unrecognized through an LLM classifier that decides allow-or-block based on safety and how well the call matches your stated intent.

Practical setup:

  • Add sensitive paths to .cursorignore at the repo root (same syntax as .gitignore). Files there are excluded from indexing and agent access — out of context, can’t be edited.
  • Prefer an allowlist of safe commands (lint, test, typecheck) over a denylist of dangerous ones. A denylist tries to enumerate every bad command; an allowlist names the few good ones and blocks the rest by default.
  • Keep terminal-command approval on for anything not allowlisted. Don’t flip the whole repo into unattended Run Mode on day one.

Codex: pick the sandbox mode, not just the prompt

OpenAI’s Codex CLI enforces safety through a sandbox mode plus an approval policy, set in config.toml or with /permissions mid-session:

ModeWhat Codex can doUse when
read-onlyBrowse files; no edits or commands without an approved planReviewing, planning, exploring an unfamiliar repo
workspace-write (default)Read, edit inside the workspace, run routine local commandsNormal local work
danger-full-accessNo filesystem or network boundaryNever on a repo with secrets; containers only

The low-friction default is workspace-write with approval on-request: Codex works inside the sandbox and asks before stepping outside it. “Full access” (sandbox_mode = "danger-full-access" + approval_policy = "never") removes every boundary at once — treat it the way you’d treat sudo. Use .codexignore to keep sensitive paths out of context.

The written-rules layer (still worth keeping)

Enforced layers are the fence; written rules are the signpost that keeps the agent from walking toward the fence in the first place. Put a concrete block at the top of CLAUDE.md / AGENTS.md — specific paths only, never “be careful with config”:

## Files the agent must NOT edit
- .env, .env.*, .env.local, .env.production
- infra/**, terraform/**, k8s/**
- migrations/**
- src/lib/secrets/**
- prisma/schema.prisma  (read OK, edit requires human PR)
- package-lock.json     (only `npm install` may modify)
- VERSION, CHANGELOG.md (humans only)

## Operations forbidden
- git push --force, git push --force-with-lease
- git rebase on any branch already pushed to origin
- npm publish, pnpm publish, yarn publish
- any rm -rf in the project root

Mirror these paths into your deny rules and ignore files so the same list is both requested and enforced. Vague guidance becomes vibes; concrete paths generalize.

Secret scanning: the last line of defense

If a secret reaches a commit, neither rules nor reviews matter — you scan. As of June 2026 the standard stack is two layers:

  1. gitleaks as a pre-commit hook, so a secret blocks the commit locally. Install with the pre-commit framework — add a .pre-commit-config.yaml pointing at github.com/gitleaks/gitleaks and run pre-commit install — or run gitleaks protect --install directly. Either way, every commit is scanned before it’s written.
  2. GitHub push protection, which blocks pushes containing recognized secret patterns server-side, so it can’t be bypassed by skipping the local hook. Enable it under Settings → Code security → Secret scanning → Push protection. It’s free on all public repos.

Pre-commit catches it before it’s committed; push protection catches it before it reaches GitHub. Install both and forget them.

Branch protection: the agent never pushes to main

Use a GitHub ruleset (or classic branch protection) on main to require a PR, require passing status checks, disallow force-push, and require at least one human review. This is the layer that makes the agent structurally unable to push to your protected branch — even if it wanted to, even if every other layer failed. Mark infra/, migrations/, and security-sensitive paths in CODEOWNERS so a human reviewer is required on any PR that touches them.

ignore files + deny rules + sandbox + secret-scan hook (enforcement) → run agent on a clean tree → review the diff → human commits or approves the PR → branch protection gates the push. Run agents on a clean tree (no uncommitted local edits) so everything in git status after the run is the agent’s work, not yours mixed in. Each layer is cheap; together they catch what the previous layer missed.

FAQ

  • Do I really need branch protection if I’m solo? Yes. It also stops you from force-pushing in a 2am panic, and it’s the only layer that runs server-side where no local bypass can touch it.
  • Will a CLAUDE.md rule actually stop an edit? No. Anthropic’s docs are explicit that CLAUDE.md shapes what Claude tries, not what Claude Code allows. Pair every written rule with a matching deny rule or ignore-file entry.
  • What if I want the agent to edit a normally-protected file? Temporarily remove the deny rule, do the work, restore the rule, and commit the two as separate steps. Don’t soften the rule “just for this PR.”
  • My Cursor denylist used to work — why change? It was deprecated in Cursor 1.3 after researchers found multiple bypasses. On 3.6+ use Auto-review and an allowlist instead; a denylist is a losing game of enumerating every dangerous command.
  • What about agents that can run shell commands? Use a sandbox or container and an allowlist. Codex’s workspace-write mode and Claude Code’s OS sandbox both confine the shell. Never let an agent run sudo, danger-full-access, or production deploy commands.
  • The agent edited a protected file anyway — now what? Your secret-scan hook or push protection should have caught any secret; if a non-secret change landed, git revert the commit and add the path to both your deny rules and ignore file so it can’t recur.

Common mistakes

  • Trusting verbal “don’t touch X” — it decays within a single session; write it as a deny rule.
  • Keeping a Cursor Auto-Run denylist post-1.3 — switch to an allowlist on 3.6+.
  • Running with --dangerously-skip-permissions on a repo with secrets — reserve it for throwaway containers.
  • Running agents on a dirty tree — your edits blur with the agent’s and review becomes guesswork.
  • Letting the agent push directly — branch protection on main is non-negotiable.
  • Skipping the secret-scan hook — “I’ll catch it in review” eventually fails on the one commit you skim.

Tags: #AI coding #Tutorial