Agentic Coding

Using Coding Agents on Legacy and Undocumented Code

Learn how agentic coding tools can safely modernize legacy and undocumented codebases using characterization tests and codebase archaeology techniques.

By Laxaar Engineering Team Aug 24, 2026 11 min read
Using Coding Agents on Legacy and Undocumented Code

Legacy code doesn't scare engineers because it's old. It scares them because it works, nobody knows exactly why, and the cost of being wrong is borne by users at 2 a.m. on a Saturday. When teams start experimenting with agentic coding on greenfield code, the results are often good. When they point the same agent at a ten-year-old monolith with no tests and tribal knowledge baked into variable names, things go sideways fast.

The problem isn't the agent. It's the workflow. An agent given "refactor this module" and no other context will make confident, plausible, wrong changes. It'll rename variables that map to database column names, inline a function that's aliased somewhere else in the codebase, or delete dead-looking code that's actually invoked through a reflection call nobody documented. The agent doesn't know what it doesn't know.

The fix is to change what you hand the agent. Give it characterization tests that capture current behavior, a codebase archaeology pass that surfaces hidden dependencies, and bounded change scopes that keep each edit verifiable. That's the workflow. The rest of this post is how to build it.

What you'll learn

What makes legacy code dangerous for agents

Agentic coding tools are very good at reasoning about code they can read. The danger in legacy systems is the code they can't read: the business rules living in comments no one updated in 2017, the configuration files pulled from a server nobody documents in the repo, the side effects of a function call that aren't visible in the signature.

Three failure patterns show up repeatedly when agents work on legacy code without preparation:

Confident deletion. The agent identifies dead code correctly by static analysis but doesn't know that a build script or runtime reflection call invokes it. It removes the code, the tests (if any) still pass, and the failure surfaces in production under a specific user flow.

Semantic breakage with syntactic validity. The agent refactors a function to be cleaner and more idiomatic. The function still runs. But the original code had an intentional off-by-one that matched a third-party API's quirky 0-indexed response format. The new, "correct" code is wrong in context.

Context window truncation. Legacy modules are often large. An agent given a 3,000-line file may reason over the first 1,500 lines well and produce changes that are locally coherent but inconsistent with the parts of the file it didn't prioritize.

None of these are model limitations you can prompt your way out of. They're workflow gaps. The agent needs a safety net built before it touches the code.

Characterization tests as agent guardrails

A characterization test is a test you write to document what code actually does, not what it should do. Michael Feathers coined the term in Working Effectively with Legacy Code, and it's the single most effective technique for making legacy code agent-safe.

The process: run the code, observe the outputs, write tests that assert those exact outputs. You're not asserting correctness. You may not know if the behavior is correct. You're asserting that the behavior doesn't change under your modifications.

# Before touching anything: write characterization tests
import pytest
from legacy_module import calculate_discount

def test_characterize_discount_standard_customer():
    # Run it, observe the result, write it down.
    # We don't know if 12.5 is "right" — but it's what it does today.
    result = calculate_discount(customer_type="standard", order_value=100)
    assert result == 12.5

def test_characterize_discount_zero_order():
    # Edge case: what happens at zero?
    result = calculate_discount(customer_type="standard", order_value=0)
    assert result == 0

def test_characterize_discount_vip_threshold():
    # Document the threshold behavior, whatever it is
    result = calculate_discount(customer_type="vip", order_value=500)
    assert result == 87.5  # observed, not derived

Now you have a safety net. Hand the agent the module and the characterization tests together. Instruct it explicitly: "All characterization tests must pass after your changes. Do not modify the test file."

This is a concrete, checkable constraint. The agent can run the tests after each edit. You can verify correctness mechanically rather than relying on code review to catch semantic drift.

At Laxaar, we treat characterization test coverage as the prerequisite for any agent-assisted legacy work. We set a target of covering every public function and every edge case we can identify before the first agent prompt touches the source. That sounds slow. It's faster than debugging a production incident caused by an agent change that passed linting but broke a business rule nobody tested.

Codebase archaeology before you touch anything

Characterization tests protect against behavioral regression. They don't tell the agent what's connected to what across the broader codebase. Codebase archaeology does.

Before pointing an agent at a legacy module, run a structured discovery pass. This is itself a good task for an agent, but a read-only one, with a clear deliverable.

Prompt: You are performing read-only codebase archaeology on the file src/billing/discount.py.
Do not make any changes.

Produce a structured report covering:
1. All call sites of public functions in this file (grep the entire repo)
2. All external dependencies imported or injected
3. Any configuration keys or environment variables referenced
4. Any database table names or column names referenced as strings
5. Any comments that appear to document non-obvious behavior or known bugs
6. Functions that appear to be dead code (no call sites found)

Output as structured markdown. Do not infer — only report what you can verify by reading the code.

The report this generates is the context you give the agent when you move into the actual change phase. It surfaces the hidden dependencies that would otherwise cause confident-but-wrong edits.

Pay particular attention to string-based references: database column names, API response keys, configuration identifiers. Agents are prone to renaming these because they look like refactorable identifiers. They're not. They're contracts with external systems that will break silently at runtime.

Scoping changes so the agent stays verifiable

Wide change scopes are where agent-assisted legacy work breaks down. "Modernize this entire module" is not a good prompt for legacy code. Too much surface area, too many unknowns, too little verifiability per iteration.

The right scope is the smallest unit that produces a meaningful outcome and can be verified independently. A good rule of thumb: one function, one behavioral intent, one commit. The agent makes the change, the characterization tests run, the diff is reviewed, it's committed. Then move to the next function.

Good scope:
"Refactor the `calculate_discount` function to remove the nested ternary on line 47.
The function's behavior must not change — all characterization tests in test_discount_characterization.py must pass.
Do not touch any other function in this file."

Bad scope:
"Clean up the billing module. It's messy and uses old patterns."

The good scope is verifiable. The bad scope is an invitation for the agent to make sweeping changes across shared state, rename things that are referenced elsewhere, and generally produce a diff you can't confidently approve without re-reading the entire module.

This isn't a limitation of agentic coding. It's how AI-powered software development works responsibly on high-stakes code. The agent is a capable collaborator, not a replacement for engineering judgment about what's safe to change.

A comparison of legacy migration approaches

Teams approaching legacy modernization have a few options. Here's how they compare when agent assistance is in the picture.

ApproachAgent SuitabilityRisk LevelTime to Value
Characterization tests first, then agent editsHighLowSlower start, safer progress
Agent edits with existing test suite onlyMediumMediumFast start, gaps in coverage
Big-bang rewrite with agentLowHighLong runway, high regression risk
Strangler fig with agent-assisted new servicesHighLowIncremental, parallel delivery
Agent with no tests, no archaeologyVery LowVery HighFast to break, slow to recover

The strangler fig pattern (building new functionality alongside legacy code and gradually routing traffic away from the old system) pairs well with agent-assisted development. Each new service the agent helps build is greenfield, well-tested, and isolated from the legacy risk surface. The legacy code stays untouched until it's fully replaced.

Our honest take: the "rewrite with agent" row is tempting because it sounds fast. It isn't. Rewrites without deep understanding of legacy behavior reproduce the same bugs in new code. Agents are no exception. They'll reproduce behaviors they observe, including undocumented ones, without signaling that anything unusual happened.

Prompting patterns that work on undocumented code

Beyond scope control, a few prompt patterns make agents substantially more reliable on legacy code.

Explain before changing. Ask the agent to describe what the code does in plain language before writing any edits. This surfaces misunderstandings early: if the agent's explanation is wrong, you correct it before it acts on that wrong model.

"Before making any changes, explain in plain language what the `process_order` function does,
including any non-obvious behavior you notice. Then propose the change you'd make to address
the goal, and wait for my confirmation before applying it."

Reference the archaeology report explicitly. Include the call-site and dependency report in the prompt context. The agent can reason about what's connected to what and is less likely to make changes that break hidden callers.

Require a rationale for deletions. Any time the agent proposes removing code, require it to explain why the code is safe to remove. "No call sites found in this repo" is acceptable reasoning. "It looks unused" is not. It could be invoked from a build script, a separate service, or via a configuration-driven loader.

Set a confidence threshold. Add an explicit instruction: "If you're less than 90% confident a change is safe given the information available, say so and describe what additional information would increase your confidence." Agents default to producing an answer. This instruction gives them permission to flag uncertainty instead.

These patterns are part of the coding agent best practices we've developed working on client legacy systems at Laxaar, and they transfer across different agent tools.

When to stop and call it a rewrite

Characterization tests and codebase archaeology can make most legacy code agent-safe to modify incrementally. But some codebases cross a threshold where incremental modification isn't the right answer.

Signs you're there:

  • The characterization tests are so numerous and so specific that any meaningful change breaks dozens of them, and you can't tell which failures represent real regressions versus overly-specific assertions about accidental behavior.
  • The codebase archaeology reveals circular dependencies so deep that changing one module requires coordinated changes in eight others.
  • The business logic is distributed across database triggers, stored procedures, application code, and undocumented batch jobs with no single source of truth.
  • The test harness itself has been undocumented for long enough that running the characterization tests reliably requires tribal knowledge you don't have.

At this point, the strangler fig pattern with agent-assisted new service development is usually faster than trying to agent-modify your way through accumulated complexity. The legacy code keeps running; new functionality is built cleanly beside it.

This is a judgment call. We've seen teams spend months on incremental agent-modification of codebases that were never going to converge. Getting the call right before that clock starts is where the Laxaar team's experience with both legacy systems and AI-assisted development pays off.

If your team is at that crossover point, our custom software development and AI development services can help you plan the right path forward.

Frequently Asked Questions

Can coding agents understand undocumented code well enough to change it safely?

Agents can read and reason about code without documentation, but their confidence outpaces their accuracy when business logic isn't explicit in the code itself. The characterization-test-first workflow compensates for this: the agent doesn't need to understand the intent behind the code if it has a safety net that catches behavioral changes. Pair that with the codebase archaeology report and most agents can work on legacy code reliably within the scoped-change workflow.

How many characterization tests do we need before the agent can start?

Cover every public function and every edge case you can identify through static analysis and manual inspection. You're not aiming for 100% branch coverage of the entire module. You're aiming for coverage of the behaviors that matter and the edge cases where legacy code is most likely to have undocumented quirks. A focused set of 20-30 well-chosen characterization tests is usually more protective than 200 tests that only exercise the happy path.

What if the legacy code has no tests at all and we can't run it locally?

This is the hardest case, and it calls for a read-only archaeology pass before anything else. Use the agent to build a dependency map, identify all call sites, and produce a plain-language description of each module's responsibilities. Then prioritize getting the code running in a test harness (even a minimal one) before any agent edits touch it. Making changes to code you can't run and verify is risky regardless of whether a human or an agent makes them.

Do we need a different agent tool for legacy work versus greenfield?

Not necessarily. The same tools you'd use for AI-powered software development on new code work for legacy code, but with different workflow configurations. The key differences are tighter scope control, mandatory characterization test runs after each change, and explicit prompting for explanation-before-action. Some teams prefer tools with strong codebase indexing (which helps agents find cross-file references) over tools optimized for single-file generation speed.

How do we handle legacy code that references external systems we can't mock?

Start with a read-only archaeology pass to document every external call: API endpoints, database connections, message queue interactions, file system paths. For characterization testing, use record-and-replay patterns where possible: capture real responses from the external systems, store them as fixtures, and run tests against the fixtures. This lets you characterize behavior that depends on external state without needing live access during every test run.


Working through a legacy modernization and not sure how to set up the agent workflow safely? Talk to the Laxaar team. We've guided teams through exactly this problem, and we can help you build the characterization test coverage and archaeology tooling before the first agent-assisted change goes anywhere near production code.

Working on something like this?

Get a fixed scope, timeline, and price within one business day — no obligation.

Agentic CodingLegacy CodeAI-Powered Development
Grow your business with us

Take your business to the next level.

Tell us what you're building. We'll come back inside one business day with a fixed scope, timeline, and team — or an honest “this isn't a fit”.

ENGINEERING PHILOSOPHY

Code is useless if it's not comprehensible to those who maintain it. We write code the next person can actually understand.