DEV Community

Cover image for 8 Paths Your Coding Agent Should Never Edit, and the Deny Rule for Each
Gabriel Anhaia
Gabriel Anhaia

Posted on AI-assisted

8 Paths Your Coding Agent Should Never Edit, and the Deny Rule for Each


The diff for a one-test fix touches four files: the test, package-lock.json, a migration that ran in production months ago, and the CI workflow, which the agent "tidied". Each change looks reasonable in isolation. Each one can cost you a day to unwind.

Reading every diff catches some of this; a rule that refuses the edit up front catches more. Below are eight paths, the failure an agent edit causes in each, and how to block it in Claude Code, Codex, Antigravity CLI and Gemini CLI, plus a server-side backstop for agents that have no deny syntax at all.

"Never edit" means never with the agent's own file tools. Lockfiles and generated code still change, through the package manager or the generator. That route stays open on purpose.

The eight paths

One warning first. Claude Code's docs say Read and Edit deny rules cover its built-in file tools, file commands it recognises in Bash such as cat, sed and tee, and redirection targets. They don't cover "arbitrary subprocesses", like a Python or Node script that opens files itself. Treat these rules as a filter on the obvious path. The OS-level wall is the sandbox.

1. Lockfiles

npm ci "will exit with an error" when the lockfile and package.json disagree, according to the npm docs, so a hand-patched lockfile fails your next CI run. The worse case passes CI: a lockfile entry can resolve a package to a different tarball, and Liran Tal showed how easily that hides in a diff, asking "who really reviews thousands of lines of character changes?" (Snyk). His lockfile-lint checks the sources in CI.

Rule: Edit(/package-lock.json), plus pnpm-lock.yaml, yarn.lock, uv.lock or composer.lock for whatever you use. The agent can still run npm install <pkg> and let npm write the file.

2. Migrations that already ran

Flyway stores a CRC32 checksum for each migration it runs, and validation fails when "differences in migration names, types or checksums are found." Edit an applied file and the next migrate stops, because validateOnMigrate defaults to true (Flyway docs). Rails records each migration it runs by its version number, so after you edit an applied file it "thinks it has already run the migration and so will do nothing" (Rails guides). A fresh database gets the edited version, production keeps the old one, and nothing reports the difference.

Rule: ask, not deny, on Edit(/db/migrate/**). The agent still needs to add new migrations, so every write prompts you and you reject the ones that touch old files. The CI check in the backstop section below enforces it.

3. CI workflows

A workflow runs with your repository's secrets. In September 2025, a commit titled "Add Github Actions Security workflow" added a workflow that posted secrets to an attacker's server: 3,325 secrets from 817 repositories (GitGuardian). That was an attacker, not an agent. An agent "fixing CI" writes to the same file, with the same reach.

Rule: Edit(/.github/workflows/**), and Edit(/.github/actions/**) if you keep composite actions there.

4. Generated code

The next generator run overwrites any hand edit. Go marks generated files with a line matching ^// Code generated .* DO NOT EDIT\.$ (cmd/go). On GitHub, paths marked linguist-generated in .gitattributes are "hidden by default in diffs" (GitHub docs), so a hand edit there is easy to scroll past in review.

Rule: Edit(/src/gen/**) (use your real output folder). The agent edits the .proto or OpenAPI source and runs the generator. The generator is a subprocess, so the deny rule doesn't stop it, which is what you want here.

5. Vendored dependencies

go mod vendor "resets the main module's vendor directory" (cmd/go), so a patch inside vendor/ disappears the next time anyone runs it. Until then, the go command only checks that vendor/modules.txt matches the versions in go.mod (Go modules reference). The reference describes no check of the vendored files themselves, so a patched copy can build and ship without a warning. The durable fix is a fork and a replace directive.

Rule: Edit(/vendor/**), and third_party/ if you have one.

6. CODEOWNERS

CODEOWNERS decides who is asked to review what, and with branch protection, who must approve. GitHub reads it from the base branch of a pull request, so an edit doesn't change the reviewers of its own PR. Once it merges, though, it changes who reviews everything after. An agent "removing stale owners" can drop the only required reviewer for your payments folder.

Rule: Edit(/.github/CODEOWNERS), or the root or docs/ copy if that is where yours lives.

7. Released changelog entries

release-please writes CHANGELOG.md from Conventional Commits when you merge its Release PR. To fix a note, you add a BEGIN_COMMIT_OVERRIDE section to the merged PR's body (squash merges only), and release-please uses it on its next run, so it fixes notes for the coming release. An agent rewriting a released entry by hand makes the changelog disagree with the tag that shipped.

Rule: Edit(/CHANGELOG.md). If you keep a hand-written "Unreleased" section, use ask instead.

8. Secrets files

Reading .env sends its values to the model with the rest of the context. In Claude Code, a Read deny rule also blocks Edit and Write on the same path, including creating a new file there. NotebookEdit isn't covered, so add an Edit rule if you use notebooks (docs).

Rule: Read(.env), Read(.env.*) and Read(*.pem). Bare filenames match at any depth, and a later Read(!.env.example) carves your example file back out.

The whole block for Claude Code

Commit this as .claude/settings.json. In project settings, a leading / anchors at the session's working directory, and in a worktree session Edit(/src/**) matches that worktree's own src/ (docs). Rules run deny, then ask, then allow, and an allow rule can't carve an exception out of a deny.

{
  "permissions": {
    "deny": [
      "Edit(/package-lock.json)",
      "Edit(/.github/workflows/**)",
      "Edit(/src/gen/**)",
      "Edit(/vendor/**)",
      "Edit(/.github/CODEOWNERS)",
      "Edit(/CHANGELOG.md)",
      "Read(.env)",
      "Read(.env.*)",
      "Read(!.env.example)",
      "Read(*.pem)"
    ],
    "ask": [
      "Edit(/db/migrate/**)"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Write rules as Edit(...), not Write(...). The docs say Claude Code accepts a Write(path) rule but never consults it, and warns at startup.

Codex, Antigravity CLI and Gemini CLI

Codex has permission profiles in config.toml. They are marked beta. Extending :workspace already keeps .git, .agents, .codex and .aws read-only, and a path set to read stays readable but not writable (docs):

default_permissions = "guarded"

[permissions.guarded]
extends = ":workspace"

[permissions.guarded.filesystem.":workspace_roots"]
".github" = "read"
"vendor" = "read"
"src/gen" = "read"
"**/.env" = "deny"
"**/.env.*" = "deny"
Enter fullscreen mode Exit fullscreen mode

Some limits. The docs only show globs as deny rules and directories as read entries, so this profile leaves single files like lockfiles to the backstop. The .env.* glob also hides .env.example, and an unbounded ** deny may need glob_scan_max_depth. The docs also describe profiles as the boundary for sandboxed commands, enforced by the OS sandbox, without spelling out Codex's own patch edits. Try a throwaway edit before you trust it. Use either profiles or sandbox_mode, not both.

Antigravity CLI reads action(target) rules from one global file, ~/.gemini/antigravity-cli/settings.json. Deny beats ask beats allow, and paths are relative to the workspace roots (docs). The CLI docs describe no per-repo file, so these rules apply to every project you open:

{
  "permissions": {
    "deny": [
      "write_file(.github/)",
      "write_file(vendor/)",
      "write_file(src/gen/)",
      "write_file(package-lock.json)",
      "write_file(CHANGELOG.md)",
      "read_file(.env)"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Gemini CLI was replaced by Antigravity CLI for unpaid-tier and Google One users on June 18, 2026 (Gemini CLI docs). If you still run it, its policy engine matches argsPattern as a regex against the tool's arguments serialised to JSON. Workspace policy files are disabled, so this goes in ~/.gemini/policies/agent-paths.toml. The edit tools are write_file and replace, and both take file_path (tools):

[[rule]]
toolName = ["write_file", "replace"]
argsPattern = '"file_path":"[^"]*\.github/workflows/'
decision = "deny"
priority = 900
Enter fullscreen mode Exit fullscreen mode

The backstop for agents with no deny syntax

Some agents have no path rules, and every local rule can be edited by someone on the machine. The server doesn't care which tool wrote the commit.

Own the risky paths, including the CODEOWNERS file itself, then turn on "Require review from Code Owners" in branch protection. Without that setting, owners are requested but approval isn't enforced (GitHub docs):

# .github/CODEOWNERS
/.github/       @acme/platform
/db/migrate/    @acme/data
/vendor/        @acme/platform
/CHANGELOG.md   @acme/release
Enter fullscreen mode Exit fullscreen mode

Repository rulesets add two more. "Restrict file paths" blocks pushes of commits that change matching paths, which suits .env files nobody should push; push rulesets work in private and internal repositories (About rulesets). "Required reviewers" asks specific teams to approve changes to file patterns, in organisation repos. On GitHub Enterprise Server, an enforced pre-receive hook rejects any push where its script exits non-zero.

For migrations, fail the PR when an existing file changes. --diff-filter=MDR lists modified, deleted and renamed files and skips added ones:

changed=$(git diff --name-only --diff-filter=MDR \
  origin/main...HEAD -- db/migrate)
if [ -n "$changed" ]; then
  echo "Applied migrations changed:"
  echo "$changed"
  exit 1
fi
Enter fullscreen mode Exit fullscreen mode

Where a slipped edit shows up in Hermes

I build Hermes IDE, so weigh this with that in mind. It runs Claude Code, Codex, Antigravity CLI and other agents side by side, each task on its own hermes/ branch and worktree. A committed .claude/settings.json is a tracked file, so it is already in every task's worktree.

Rules miss things: a script the agent runs can still write any of these files. The Review Desk (⌘G, Ctrl+Shift+G on Windows and Linux) shows an agent's changes by turn or by file, with risk flags. Don't count on a flag for every path in this list: open the by-file view and check the eight paths yourself.

The Hermes Review Desk in By turn view, listing two turns and showing turn 1's diff of src/db.ts with the changed-files list and a drafted commit message
The Review Desk (⌘G) in By turn view for a Claude Code task in orders-api: two turns, turn 1's diff of src/db.ts, the changed-files list, and a commit message drafted from the turns.

Every agent turn is saved, so when a turn touched one of those paths, you can diff that turn or restore it, with undo. A line comment goes back to the agent that wrote the line. Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux: download it or read the source on GitHub.


If this was useful

Two of these rules, migrations and changelogs, rest on one idea: once history has shipped, you append to it and never rewrite it. That idea runs through the Event-Driven Architecture Pocket Guide, where event logs, outbox tables and sagas all depend on never editing what already happened. It's one of 8 short books in the Pocket Guides series, covering agents, RAG, prompts, observability, events, databases and system design.

Event-Driven Architecture Pocket Guide: Saga, CQRS, Outbox, and the Traps Nobody Warns You About

Top comments (0)