Use AI to understand code

By

How I use coding agents to understand existing code before changing it: focused questions, tracing data and side effects, and reviews in a fresh session.

~~~

Most people open an AI chat and ask it to write something.

I do that too, but more often I do the opposite. I point the agent at code that already exists and ask questions until I understand it. Then, if something needs to change, I let it write.

The code that comes out is useful, but what stays with me after the session ends is the understanding of the codebase.

Writing is the easy part now

I ship a lot of code with coding agents, in Cursor, Claude and Codex. They are fast, and they are perfectly happy to produce a diff I have not read.

I noticed this years ago, when the generated code did not feel mine and I had to go back and read it after the fact. If you want to still maintain the project next month, that reading is not optional.

So I changed the order. I use the agent to build a mental model of the code first. Then I let it write. Then I ask it more questions about what it wrote.

Give it the repo

A pasted snippet is not enough for this.

If I ask “does this leak data?” and the model only sees one function, it answers about that function. But the leak might be three files away, and the permission check might live in a helper that nobody calls anymore.

So I open the whole project in the agent and let it read. I want it to follow the call into the query, the email, the cookie, wherever the data goes. I also keep the project facts in an AGENTS.md file so it does not have to rediscover how the site builds every time.

Ask one sharp question

“Explain this file” gives me a summary, and a summary is the file rewritten in English. I still don’t know how it behaves.

So I ask one specific thing at a time:

What happens when this webhook arrives twice?

Where does this email go after we store it?

If this function returns 200, did the user get access?

Does the error reach the person who paid, or only my logs?

After five or six answers like these I can see how the system works. The model is good at this kind of question. It jumps from a function to an HTTP call to a database query to a user-facing error without losing the thread, which is exactly what I struggle to do in my head on a first read.

Build the map in layers

I do not ask for the whole architecture at once. I go in four passes.

First, I ask about the entry point:

What starts this flow, and what input crosses the boundary?
Name the route, event, command, or component.

Then I follow the data:

Trace `orderId` from the request to storage.
List every function that transforms or validates it.

Then I follow side effects:

Which operations leave this process?
Include database writes, network calls, emails, and queue messages.

Finally, I ask about failure:

For each side effect, what happens if it succeeds and the next step fails?

Going in that order, by the end I have a path through the code rather than a pile of files.

I always ask for file paths and function names in the answer. If a claim comes with a location, I can open it and check. A claim without one is easy for the model to make up.

Ask for evidence, not confidence

Models use confident language for both facts and guesses.

I make the output separate them:

Answer in three parts:

1. Confirmed behavior, with file and function references
2. Assumptions you could not verify
3. The smallest commands or tests that would resolve those assumptions

The answer can still be wrong. But now I can see which parts the model checked and which parts it assumed.

Suppose the agent says a webhook is deduplicated. I ask where the durable key is stored. If it points at an in-memory Set, the behavior is local to one process and gone on the next restart, so the deduplication only sounds like a guarantee. If it points at a unique database constraint, I can open the migration and the insert and confirm it.

A real walkthrough

Let’s take a purchase webhook. I have one on this site. Paddle hits /purchase. We grant course access and send an email.

Here is a simplified version of the decision in the middle:

if (body.alert_name) {
  return Response.json({
    message: `Ignoring account alert: ${body.alert_name}`,
  })
}

const productId = body.p_product_id

If I drop this on an agent and ask what it does, I get a paragraph about ignoring account alerts. That’s correct, but it’s not enough.

I ask the next questions myself.

Why are there two payloads for one purchase?

What happens if we handle both?

The in-memory Set that dedups emails, does it survive across Cloudflare isolates?

If the newsletter subscribe fails, do we still write the access record?

If the access record fails, did the buyer already get the email?

That last one matters, because the buyer thinks they are in while my retrieval page might not list the course yet.

None of those answers live in one function. You find them by following the path from the request to the email, and that is where the agent helps me most.

Attack a change

When an agent proposes a change, “does this look good?” gets me a yes. I ask it to attack the change instead.

What existing behavior does this touch?

Can we fold this into a function we already have?

Does a failure show up in the UI, or does it fail silent?

Is this query doing more work than it needs?

Then I read the answers with some suspicion. Ask a model for risks and it will find risks, real or not, and it can still miss the one that matters. My job is to check each one against the code and keep the ones that hold up.

I do this in a fresh chat when I can. The session that wrote the code is biased toward defending it. A new session only sees the diff and the repo.

Plan before you generate

The same habit works before the code exists.

Before I let an agent add a feature, I ask what is already there.

Is there a helper for this?

Which pages call this function today?

If we change the return shape, who breaks?

I also ask it to lay out the choices. KV or D1. Cookie or server session. One endpoint or two. I want to be the one who picks, because if I skip this step the model picks for me and I inherit a design I never agreed to.

A short written brief helps here: the facts, the constraints, and a request to list its assumptions before it starts.

Tests are another way to ask

“Write tests” is a weak prompt.

“Write a test that proves a duplicate webhook does not send a second email” is a question about behavior, written as a test.

If the test is hard to write, I don’t understand the behavior yet, and finding that out before the change is cheaper than after.

I still read the tests, because a model can write a test that proves the wrong thing.

Turn the model into a debugger

When code fails, I give the agent evidence in the order the program produced it.

I include:

  • the command I ran
  • the complete error, not one line
  • the expected behavior
  • the smallest input that reproduces it
  • any recent change that might be related

Then I ask for competing explanations:

Give me the three most likely causes.
For each cause, name one observation that would prove or disprove it.
Do not change files yet.

This works better than “fix the bug” because it keeps diagnosis separate from implementation.

If two explanations predict the same observation, the test is weak. I ask for a check that separates them.

For example, a failed API call might come from invalid input, an expired credential, or a network failure. Logging the status code separates the first two from the third. Inspecting the response body and credential expiry separates the first from the second.

The model is good at designing the experiment, but I still have to run it to get the answer.

Read the diff as a story

A diff tells me what changed, but not whether the new behavior is complete.

I ask the agent to walk the diff in execution order:

Trace one successful request through this diff.
Then trace one invalid request and one dependency failure.
Point out any branch that has no test.

This catches a common problem: the happy path changed in one file, but error mapping, cleanup, or the caller stayed on the old contract.

I also ask what did not change. If a return type changed but no callers changed, either the change is backwards compatible or we missed something. I want to know which.

Compare the explanation with runtime behavior

Static reading has limits.

Dynamic configuration, framework routing, generated code, and environment variables can change what runs. The agent may trace the obvious function while production calls another one.

I verify the map with small observations:

  • run the focused test
  • call the endpoint with a known input
  • inspect the response status and headers
  • add a temporary breakpoint
  • check which handler appears in the stack trace
  • inspect a database row before and after

If HTTP is still a bit foggy, the free HTTP course helps, because status codes and headers become evidence you can read. For general code reading, the free JavaScript course builds the vocabulary the model assumes you have.

So the loop is: ask, then look at what the program does, then compare the two.

Do not outsource the judgment

People ask the model whether permissions are enforced downstream, get a yes, and relax.

The model can trace a call. It cannot be the authority on whether your product is safe. You still open the other file, try the request yourself, and look at the header in the response.

I wrote about hardening public form endpoints for this reason. Body size limits, allowlists, rate limits are things you verify by sending a request, not things you accept because the explanation sounded right.

Same for “is there a leak?”. Ask, then grep, then look at the logs. An email address in a console.log is a leak if those logs leave your machine.

This does not teach you how to read

If you are learning to code, be careful with this habit.

You can collect answers and feel like you understand, without ever practicing the reading. The next file, with no chat open, is still opaque.

My advice is to try first. Read the function, guess what it does, then ask and compare your guess with the answer. That is how you develop a feel for when the answer is nonsense, and you need that feel.

The fundamentals matter for the same reason. My free JavaScript and Git courses exist so you can notice when the model is wrong.

Common mistakes

Asking for a file summary. Summaries compress syntax and rarely explain behavior that crosses files. Ask about one path, one value, or one failure instead.

Accepting a list of possible risks. Ask “what could go wrong?” and the model produces a list that fits any application. Ask which risks are reachable in this code, and require the route from input to consequence.

Letting the writing session review itself. It already has a story about why the code works. Start a fresh session with the repository and the diff.

Changing code before the model can explain it. If the agent cannot describe the current path, it should not rewrite it yet. Narrow the question first.

Treating generated tests as independent evidence. A test written from the same wrong assumption confirms the wrong assumption. Compare it with the requirement and make sure at least one case fails on purpose.

Pasting secrets into the context. The agent almost never needs real credentials or customer data to explain a path. Use redacted payloads and local fixtures.

How I use this every day

I run a bunch of small sites. I am usually the only person in the repo.

A typical session looks like this.

I open the project in Cursor and point at a function I have not touched in months. I ask what it does, then what it does not do, who calls it, and what happens when it fails.

If I am about to change it, I ask what else will move. Then I write a short plan, or I let the agent write one and I cut it.

Then I let it implement one slice, I review it, and I ask it to attack its own change. Whatever survives, I keep.

I don’t do any of this for a one-off script I will delete tonight. It takes time, and the time is worth it only for code I will live with.

It is also a poor fit when you already know the path cold. If I wrote the function this morning, I do not need a tour, only a second pair of eyes on the one thing I might have missed.

Code is the byproduct

I still want the feature shipped. I just do not want a repository I cannot explain.

What I take away from a session is the picture of how the system works. The files on disk are a side effect of that, and they are better files because I understood what they had to do.

Tagged: AI · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about ai: