Why I Ask AI to Investigate Before It Writes Code

Before I let an AI coding agent modify a codebase, I often ask it to investigate first. Here's how separating investigation from implementation helps me understand existing behavior, challenge assumptions, and keep changes focused.

Why I Ask AI to Investigate Before It Writes Code

One of the first things I tell an AI coding agent when working on a non-trivial task is surprisingly simple: Read-only first. Don't change any files yet.

I'm not necessarily asking it to solve the problem. Sometimes I want it to trace a flow through the codebase, understand how a feature is implemented, inspect the surrounding tests, or challenge an assumption I've made about the system. I've become increasingly deliberate about separating that investigation from implementation, not because I don't trust AI to write code, but because writing code is often the easier part of a change. Understanding what should change, what should remain untouched, and how to know whether the result is correct requires a different kind of attention.

In my repository instructions, I make those boundaries explicit. Asking for an investigation doesn't authorize modifying files. Asking for implementation doesn't automatically authorize committing, pushing, or opening a pull request. Those are separate decisions, and I want to make them consciously.

Understanding the path before changing it

A recent example came from work on authentication diagnostics in our iOS UI tests. On the surface, the task was relatively contained. We wanted better information when authentication-related tests failed. But the behavior involved several parts of the system: application startup, persisted credentials, a hosted login page, test helpers, failure attachments, and CI reporting. Looking at the sign-in helper alone wouldn't explain the complete behavior.

For example, relaunching the application resets its navigation and in-memory state, but it doesn't automatically clear credentials stored in the Keychain. A UI test can launch a fresh application process and still recover an authenticated session. That's useful when authentication isn't what we're testing. Repeating the entire interactive login flow before every UI test would introduce additional execution time and more opportunities for failures unrelated to the behavior under test. But the same optimization becomes a problem when the test is specifically supposed to verify authentication.

A test reaching the home screen doesn't necessarily prove that the hosted login flow worked. It may have reused an existing session without visiting the login page at all. This is exactly the kind of distinction I want to understand before asking an agent to change anything.

I'll typically ask it to trace the relevant execution path, including setup helpers, persisted state, launch arguments, and fallback behavior. I want references to the actual implementation, not just a plausible description of how authentication normally works. I'm also interested in what the existing tests actually prove. Their names and assertions only tell part of the story because the initial conditions matter just as much.

If the agent overlooks session reuse, misunderstands the test setup, or assumes that relaunching means starting from a completely clean state, I would rather discover that during investigation than after reviewing an implementation built around the wrong assumption. The goal isn't to produce an exhaustive architecture document. It's to establish a shared, inspectable understanding of the behavior relevant to the change.

A plausible explanation isn't the same as evidence

One thing I've found particularly important when working with AI agents is making uncertainty visible. Coding agents are good at connecting pieces of information. They can follow call chains, identify related helpers, compare implementations, and construct explanations that sound technically reasonable. But a reasonable explanation is not necessarily a verified one.

I try to keep three categories separate during an investigation: what the available code or evidence confirms, what we can infer from it, and what remains unknown. In the authentication example, we can confirm from the implementation that persisted credentials allow certain tests to reuse a session. We can also explain how that behavior might affect a specific test. What we cannot automatically conclude is that session reuse caused a particular failure, or what the hosted authentication page displayed during a CI run we didn't observe.

A static code trace cannot tell us what an external service returned at a specific moment. It can show us possible execution paths, but it cannot recreate missing runtime evidence. That distinction becomes especially important when the agent proposes a fix. If we accept a convincing explanation too quickly, we can end up changing code to address a problem we haven't actually established. The implementation might be technically sound and still solve the wrong thing.

Sometimes the most useful outcome of an investigation is realizing that we need better diagnostics before attempting a fix. That was an important consideration in the authentication work. We needed more visibility into what happened when the hosted login flow failed, but gathering additional information also introduced security and privacy considerations.

A hosted login page can contain a submitted email address and other sensitive information. Capturing everything would make debugging easier in one sense while introducing unnecessary exposure elsewhere. The diagnostic changes were deliberately narrow. At the relevant failure points, they collected static page text and the URL host, redacted the submitted email, and avoided input-field values, full URLs, and screenshots. The point wasn't to capture as much information as possible. It was to collect information that could help explain a failure without exposing data we didn't need.

Even after that work, better diagnostics didn't mean we had established the root cause of every authentication failure. Those are different outcomes, and I think it's important to keep them separate.

Deciding what the agent is allowed to change

Once I understand the relevant behavior and the remaining uncertainty, I usually ask the agent for a proposed implementation plan. I'm interested in the change itself, but also in its boundaries. What needs to change? Which existing behavior should remain intact? What dependencies or shared helpers are involved? How can we validate the result, and what would that validation still leave unproven?

That last question is easy to overlook. Consider a change to UI test-result reporting. If the issue involves a failed attempt followed by a skipped retry, a focused regression test can establish whether the final result is still reported as a failure. Successfully building the iOS application wouldn't prove that behavior. Similarly, compilation and linting can tell us useful things about an authentication diagnostics change, but they cannot prove that the real hosted login flow works correctly in the required environment. I want that distinction visible before implementation starts, not discovered afterward when the agent reports that everything is done.

I also explicitly identify what shouldn't change. AI agents can sometimes identify nearby code that could be simplified or reorganized while working on a task. Those suggestions may be reasonable, but they don't automatically belong in the same change. A focused fix shouldn't quietly turn into a refactor just because the agent noticed an opportunity.

This is one reason I prefer reviewing a plan before authorizing implementation. It gives me a chance to correct the direction, narrow the scope, or identify a missing dependency while changing course is still inexpensive. Once we're aligned, I give the agent a different instruction: implement the agreed change, reuse existing helpers where appropriate, avoid unrelated cleanup, and run the relevant checks. Committing, pushing, and publishing remain separate actions.

That separation isn't about making the workflow unnecessarily formal. It's about preserving a point where I can evaluate the proposed solution before it becomes a modification to the codebase.

Reviewing the result against what we agreed

The read-only investigation doesn't make the code generated afterward automatically trustworthy. I still review the diff, and I still care about validation. What changes is that I now have a clearer basis for that review. Instead of looking only at whether the new code seems correct, I can compare the implementation against the behavior we established and the scope we agreed on.

Did the agent modify only the relevant paths? Did it preserve the existing session-reuse behavior for tests that depend on it? Did the diagnostics avoid capturing sensitive information? Did the tests exercise the conditions we actually care about? These are questions I can evaluate more effectively because we've already discussed the expected behavior and the boundaries of the change.

I also want the completion report to distinguish implementation from verification. An agent might implement the change successfully but be unable to execute a particular test because it lacks the required account, simulator configuration, backend condition, or CI environment. That's a legitimate limitation, but it needs to remain visible. I would much rather receive a report explaining what changed, which checks ran, which passed, and what couldn't be verified than a confident statement that the task is complete when the most important behavior hasn't been exercised.

The same principle applies to the investigation itself. If the agent couldn't establish something from the available evidence, implementing code shouldn't magically turn that uncertainty into a confirmed fact.

Not every change needs the same process

I don't apply this level of investigation to every task. For a typo, a straightforward local adjustment, or a change with very limited impact, a brief inspection of the surrounding code may be enough. Spending significant time reconstructing execution paths and writing implementation plans for every small change would introduce more overhead than value. But when a task involves authentication, concurrency, shared infrastructure, external services, or behavior that crosses multiple system boundaries, I want more confidence before implementation begins.

The level of investigation should reflect the risk and uncertainty of the change. What matters to me isn't the amount of documentation the agent produces. It's whether I understand the relevant behavior well enough to make a decision.

AI coding agents have made it much easier to explore unfamiliar code, follow dependencies, and implement changes across multiple files. I use those capabilities regularly, and they've changed how I approach engineering work. But they haven't removed the responsibility to decide whether a change is appropriate. If anything, the ease of generating an implementation makes that responsibility more important. When producing code becomes inexpensive, it's tempting to move directly from a plausible explanation to a proposed fix.

I prefer to keep a boundary between the two. By the time I authorize an implementation, I want to understand what we're changing, why that scope makes sense, what assumptions we're relying on, and what evidence would tell us we got it wrong.

That's what read-only first gives me. Not a guarantee that the resulting implementation will be correct, but a better foundation for deciding what to change in the first place.