AI coding

Guardrails for Legacy Interfaces: What an Agent May Not Touch

Antonio Evans8 min read

A split diagram of a legacy monolith labeled by layers.
Key points

Key points

  1. 1Tag every path, database object and endpoint as read-only, propose or autonomous before an agent touches the repository.
  2. 2Enforce the tags below the model: filesystem permissions, scoped tokens and CI rules reject changes to protected paths.
  3. 3Give the agent a branch, a test harness and a scoped token, never write access to production.
  4. 4Small diffs plus a named human reviewer turn agent output into something you can audit and reverse.

Start from what "legacy" actually means here

Michael Feathers' definition is blunt: legacy code is code without tests, and without tests you cannot tell whether a change improved the system or broke it Working Effectively with Legacy Code. That single sentence explains why agent guardrails matter more on brownfield code than on a greenfield project. On new code, a wrong change usually fails a test you just wrote. On a system that has run untouched for a decade, a wrong change may look correct, pass the thin test suite and surface as a settlement mismatch three weeks later.

Feathers' change algorithm is still the right spine for any agent task: identify the change points, find test points where behavior can be sensed, break dependencies carefully, cover the target code with characterization tests, then add functionality and refactor only inside the covered area. The BSSw summary is explicit that refactoring must not extend outside code that is sufficiently covered by tests Better Scientific Software. For an agent, that translates into a hard rule you can encode: the allowed diff scope equals the files the agent actually covered with tests in the same pull request.

The practical consequence is a decision rule you can apply in a minute. If the agent cannot get the target code into a test harness in the current task, its scope is read-only for that task. It may read, trace and explain, but it may not propose a patch. Everything else follows from that.

Three tiers, one tag file

Before any agent runs, inventory the repository. You need three kinds of inventory and each maps to a different enforcement mechanism.

TierWhat belongs hereAgent permissionEnforcement
AutonomousNew modules, adapters, test files, docs, feature-flagged pathsMay edit, must open a PRCI allowlist plus branch protection
ProposeCore business logic with test coverage, shared utilities, API contractsMay draft a diff, must not mergeCI accepts diff, human owner approval required
Read-onlyAuth, payments, migrations, deployment manifests, generated clients, vendor codeMay read and explain onlyFilesystem permissions and CI path block

The third row is where most incidents would have been prevented. Code ownership guidance for agent pull requests recommends starting with boundaries that carry distinct failure modes and notes that authentication code, payment flows, deployment manifests, database schemas, public APIs and generated clients usually need different reviewers Factory on code ownership. Those are exactly the paths that belong in the read-only tier.

Two details decide whether this works in practice. First, GitHub evaluates CODEOWNERS with the last matching pattern, and some familiar gitignore syntax does not apply, so test pattern precedence with a throwaway pull request touching each protected area Factory on code ownership. Second, protect the ownership file itself. If the agent can edit CODEOWNERS, it can quietly remove the reviewer it needs. An automatic review request is only a notification; a merge rule is the control Factory on code ownership.

Keep task instructions separate from accountability. An AGENTS.md file can tell the agent which commands and local rules apply, while CODEOWNERS identifies who reviews the resulting change. Together the two files describe execution and accountability without giving the agent approval authority Factory on code ownership.

Enforce with permissions, not with prompts

A prompt that says "do not modify the migrations folder" is a suggestion. Filesystem permissions are a wall. The most reliable way to keep an agent out of production code is to give it a copy of the repository where the protected paths are genuinely unwritable.

One working approach is to ban the agent from the real filesystem and let it operate inside a virtualized copy. LlamaIndex describes using AgentFS, a SQLite-based virtual filesystem, so the agent can perform destructive operations without damage because everything happens on copies, then wiring custom read, write and edit tools through MCP and disallowing the agent's built-in filesystem tools entirely LlamaIndex on secure filesystem access. The article also notes a PreToolUse hook to block bypass attempts, because a hallucinating agent may try another route.

If you stay on a normal filesystem, the equivalent is a shell wrapper that applies the tags. Here is a small allowlist checker you can run locally and in CI, with the same list as the single source of truth:

#!/usr/bin/env bash
# scope-check.sh: fail if a diff touches a read-only path
set -euo pipefail
BASE="${1:-origin/main}"
DENY_FILE="agent/readonly-paths.txt"

changed=$(git diff --name-only "$BASE"...HEAD)
violations=""
while IFS= read -r path; do
  [ -z "$path" ] && continue
  if printf '%s\n' "$changed" | grep -qxF -- "$path"; then
    violations="${violations}${path}"$'\n'
  fi
done < "$DENY_FILE"

if [ -n "$violations" ]; then
  echo "Blocked: the agent touched read-only paths:"
  printf '%s' "$violations"
  exit 1
fi
echo "Scope check passed."

For broader protection, prefix matching is usually what you want. Turn each line of the deny file into a pattern with grep -E and keep the file under CODEOWNERS protection so a change to the guardrail needs a second pair of eyes. The same idea applies to databases: a scoped agent token should hold SELECT on the legacy schemas and INSERT only into staging tables, so a bad query cannot mutate the production ledger.

Scoped credentials do the rest of the work. An agent token should be issued per task, expire quickly, and be limited to one branch. A token that can push only to agent/* branches and open pull requests, and cannot merge, delete branches or write to protected refs, turns most worst-case scenarios into a rejected push.

Size the diffs, and make them reversible

An agent can produce a 4,000-line pull request in minutes. A reviewer cannot meaningfully review it in the same order of magnitude. Cap the diff.

A workable rule for legacy repositories: a single agent pull request may touch at most one module and roughly 300 changed lines, excluding tests and generated files. Anything larger should be split into a sequence of tasks, each with its own test evidence. This is not arbitrary caution. When an agent proposes a change to a legacy module with hidden shared dependencies, the local invariant that matters is often invisible in a large diff, which is why ownership reviewers chosen by proximity rather than by knowledge miss it Factory on code ownership.

There is a second reason to cap size: reversible increments are the only safe way to change code nobody fully understands. The strangler fig pattern replaces behavior gradually, starting small and shifting responsibility from the legacy system to new code until the old system can be decommissioned, with a façade routing requests between the two during the transition Martin Fowler on the strangler fig. Microsoft's version is concrete about the four phases: introduce the façade, shift requests incrementally, decommission the legacy system once dependencies are gone, then remove the façade Azure Architecture Center. Each of those phases is an agent-sized task, and the façade is the thing that lets you route around a bad change without a rollback.

Branch by abstraction is the in-process sibling. You put an abstraction over the component, make old and new implementations coexist, migrate consumers, then delete the old one, all on the mainline rather than a long-lived branch Continuous Delivery on branch by abstraction. The continuous delivery write-up includes the detail that makes this enforceable for agents: a rule of thumb that you never add to the old style, and one way to enforce it is to fail the build when a new old-style call is added, so the count can only go down Continuous Delivery on branch by abstraction. A count-based CI check is trivial and it directly bounds agent behavior.

Worked example: a 12,000-function insurance platform

Take a realistic scenario from published consulting work. Forte Group describes a legacy insurance policy management platform where they generated natural-language summaries for more than 12,000 functions and indexed them with embeddings so engineers could explore functionality without guesswork Forte Group on modernizing legacy codebases with LLMs. The same account reports detecting and consolidating 86 redundant implementations of date-range filtering in a .NET monolith, and decomposing ETL scripts of more than 3,000 lines each into validate, transform and persist modules Forte Group on modernizing legacy codebases with LLMs.

Now apply the tiers with rough numbers to see the decision rule in action. Suppose the repository has 12,000 functions across 9,000 files. Inventory results, as an illustrative split:

TierFilesTypical agent taskGate
Read-onlyabout 700Explain a function, trace callers, draft a summaryNone, read is always allowed
Proposeabout 2,300Draft a refactor of one date-filter call siteOwner approval, characterization test required
Autonomousabout 6,000New adapter, new tests, documentationCI only

A task to consolidate the 86 date-filter implementations should start as read-only across all 86 call sites, then move to propose on the shared utility and one call site at a time. Each of the 86 becomes its own small diff with a functional-equivalence test, which is exactly the evidence the case study describes producing. If the agent attempts a single patch across all 86 files, the size cap and the module rule reject it before a human spends an afternoon reading it. That is the whole point: the guardrail does the rejecting, not the reviewer's attention.

The same arithmetic applies to the 3,000-line ETL scripts. Decomposing one script into three modules is a legitimate autonomous task if the modules are new files and the original stays in the read-only tier until the cutover. The risky move, editing the monolith in place, is blocked by the tag, not by the agent's judgment.

Where a model router fits

Guardrails decide what an agent may touch. They do not decide which model reads 60,000 tokens of legacy context, which one writes the characterization tests and which one reviews the resulting diff for missed call sites. Teams that run this workflow quickly end up with several providers and a key management problem, and a legacy repository is the last place you want an unvetted key rotation to break a build.

Quanta Inference puts text, image, video, audio and embedding models behind one OpenAI-compatible API key, prepaid and priced in US dollars, which is a reasonable fit for a diff-review step that needs a cheap model for the bulk of the files and a stronger one for the protected paths: Quanta Inference. The guardrails stay yours either way. Model choice should never be the thing that decides whether a payment path changes.

Review rules that survive contact with a busy week

Rules decay. A guardrail that requires a reviewer who is on vacation becomes a guardrail that gets bypassed. Three practices keep it alive.

Route by risk, not by availability. Every protected path needs a team owner, not a named individual, because a single named person creates a bottleneck and team ownership with documented escalation survives organizational change better Factory on code ownership. Track how often agent pull requests arrive with no owner, request a team with no available reviewer, or need manual reassignment. Rising numbers mean the ownership file has gone stale.

Ask the agent to report its own scope. The task should require the agent to list matched owners, call out files with no specific owner, and avoid mixing unrelated subsystems, splitting the pull request when changes can be reviewed and deployed independently Factory on code ownership. This costs one instruction in the agent's task brief and gives the reviewer a map before reading a line of the diff.

Require test evidence in the pull request body. Not a claim that tests pass, but the names of the characterization tests added and the command that ran them. Feathers' algorithm makes coverage the precondition for refactoring Better Scientific Software, so a diff to the propose tier without new tests is incomplete by definition. Automated review can add a separate correctness pass that flags likely defects, while owners decide whether a change fits the system's constraints and operating model Factory on code ownership.

FAQ

FAQ

What is the difference between the propose tier and the autonomous tier?

Autonomous means the agent may edit and open a pull request that CI can merge once checks pass, typically for new files, tests and documentation. Propose means the agent may draft a diff but a named owner must approve it, typically for existing business logic. The split is about blast radius: a new adapter file can be deleted, while a change to a settlement calculation cannot be undone by reverting a commit that already ran in production.

Should I let the agent run read-only tasks against production data?

Reading production schemas and sampled rows is often necessary to understand a legacy system, but the token should be limited to SELECT and column masking should apply to personal data. Never grant write scope outside staging. If the agent needs to test a migration, it should run against a restored copy with a scoped credential that expires with the task.

How do I stop an agent from editing the guardrail files themselves?

Put CODEOWNERS, the deny list and the CI workflow files in the read-only tier and protect them with branch protection requiring owner approval. An ownership file that the agent can change is not a control. Test the setup by opening a draft pull request that edits one protected path and confirming that CI fails and the correct owner is requested.

My legacy code has no tests at all. Can an agent do anything useful?

Yes, but only in the read-only and autonomous tiers at first. Have the agent trace call graphs, summarize function intent and map dependencies, which shortens the investigation phase that makes legacy changes risky. Then let it add characterization tests as new files. Only after tests cover a target module does that module move from read-only to propose.

How small should an agent pull request be?

Small enough that a reviewer can hold the whole change in mind, which in practice means one module and a few hundred changed lines excluding tests and generated files. If the task genuinely spans several ownership domains, split it into stacked pull requests that can be reviewed and deployed independently. A change that cannot be split is usually a sign the abstraction layer is missing.

Sources

Sources

  1. Working Effectively with Legacy Code - Better Scientific Software (BSSw)
  2. Code ownership for coding agent pull requests - Factory.ai
  3. Secure Filesystem Access for Coding Agents
  4. Strangler Fig - Martin Fowler
  5. Strangler Fig Pattern - Azure Architecture Center | Microsoft Learn
  6. Make Large Scale Changes Incrementally with Branch By ...
  7. Modernizing Legacy Codebases with LLMs: A Practical Framework
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles