Your coding agent is guessing which file to edit.
Every tool that applies a fix has to answer one question before it writes: which file? When the error does not name one, something has to guess, and most guesses are worse than you think.
Before any tool can apply a fix, it has to answer a question that nobody talks about: which file.
When the error names a file, this is trivial. App.tsx:12 is a file and a line
and there is nothing to decide. But a large share of real errors do not name a
file you can write to. A React warning points into node_modules. A promise
rejection surfaces with frames like at li and at App, no path, no line. A
console error from a third-party script names a bundle URL that maps to nothing
on disk.
In every one of those cases, something in your tool chain still has to pick a destination. And the thing it usually picks, in tool after tool, is whatever file you happen to have open.
I know this because I built one that did it, and then watched it append a JSX
snippet to the bottom of index.html. Twice.
Four ways a tool decides where to write
Rank them by how much they can be trusted, because the ranking is the whole design.
- An exact string match in a file the user submitted. The strongest one. If the fix arrives as "replace this exact text with that exact text", and the text is found once in the file, the destination is not in question. Nothing is inferred.
- A path parsed from the error itself. Strong, with one caveat: the path has to survive a sanity check. A path into
node_modules, a virtual module URL, or one of the synthetic sources a runtime invents (<eval>,VM12345,blob:) is a real string that is not a real file you should write to. - A path from the debugger. Underused, and better than it sounds. A debug adapter's output event carries a sourcemapped
source.pathand line, which means it can tell youApp.tsx:12while the text of the same error is still talking about a bundle. If your tool parses the error text and ignores the adapter, it is throwing away the better answer. - Ambient editor state. The active tab. The last focused document. This is not a source of truth. It is a coincidence between your attention and the error, and it is correct often enough to hide how often it is not.
The failure I want to describe is what happens when 1 through 3 all decline, and 4 is still sitting there at the end of the chain, ready to answer.
The five conditions, all reasonable, one bad outcome
This is a worked example from a debugging tool I build. The specifics belong to my code. The shape belongs to any tool that writes fixes.
I was testing a feature that captures browser errors and turns them into something you can act on. I picked a React key warning out of a list, and got back a generic diagnosis with a live Apply button.
Five conditions had to hold at once:
- The warning's source pointed into
node_modules. The guard that refuses library paths did its job. Correctly. So there was no anchor path. - The error text carried no path either. Frames read
at liandat App. Nothing to parse. - Resolution fell through to the active editor, which was
index.html, because that is what I had open. index.htmlis not a library path, so the check that disables Apply for library files saw nothing to complain about. Apply stayed enabled.- The fix carried no line hint. With no hint, the applier does a keyword search to find an insertion point. The first line of the fix was a comment. No match.
Step 5 is where it turns from wrong file into wrong file and garbage, because of a default:
insertAt starts at the end of the file. The search only ever moves it
earlier. Miss the search, and "insert" silently means "append".
Warning: A fallback that means "I could not find the target" should not be spelled the same as "the target is the end of the file". That single line of code is the difference.
That default had been in place for a long time without incident, because until this feature existed, every path into the applier came from an error that named a file. The landmine was old. The feature is what made stepping on it routine.
The other half: it was answering blind and nobody read the warning
The reason the fix was generic has the same root. No resolved file meant no editor was opened, which meant no code snippet went up with the request. The demo project had never been indexed, so no retrieval either.
The model received an error string and nothing else. To its credit, it said so in its own output: without code, it could not cite exact lines.
So the analysis layer correctly reported that it was working blind, and the client took that blind answer and offered to write it to disk.
Note: An honest upstream does not protect you. If a component can report "I had nothing to work with", every consumer downstream has to actually read that field and change behaviour. Otherwise the honesty is decoration.
Three rules for anything that writes to disk
These are what I changed, stated so they are useful outside my codebase.
Never infer a destination silently. If the target file came from ambient state rather than from the error, that fact has to travel with the request. Downstream code cannot make a good decision about a guess it does not know is a guess. In my case the request now carries a flag saying the target was inferred.
The writer validates, not just the caller. A caller-side check protects the one code path you were thinking about the day you wrote it. The function that rewrites the file is the piece that cannot be routed around, so the refusal belongs there. Mine grew two guards: one that refuses a snippet whose language cannot live in the target file (a bare JS or Python statement bound for markup or a stylesheet), and one that refuses the end-of-file append when the caller admits its target was inferred.
Unknown should disable the write, not pick a default. "I could not find where this goes" has exactly one safe behaviour: return the file unchanged and let the caller offer copy instead. Every other behaviour is a guess wearing the clothes of a decision.
A fourth, softer one, from the same incident: any list that hides items has to explain the hiding. The React warning was classified as never-toast, which I had read as "cannot interrupt you". The history list that fed the quick pick included it anyway, rendered identically to a real exception. Suppressed and unreachable are different promises, and a suppressed item shown in the same font as a real one has had its suppression quietly undone.
Sixty seconds to test whatever you use
I am not going to tell you how any other tool behaves, because I have not read their source and neither has anyone who confidently posts about it. Run this yourself, on whatever assistant, agent, or internal script you point at your repo.
Three outcomes, in descending order of how much I would trust the tool:
- It tells you it cannot locate the file and offers the fix as text. Good.
- It shows you a diff against a file, and the file is the one you had open. Now you know its resolution chain ends in ambient state. Useful to know before it matters.
- It writes without showing you a diff at all. That is the one worth finding out about on a scratch repo rather than on your work branch.
The test costs a minute and it tells you something about your tooling that no changelog will.
FAQ
Is "use the open file" a reasonable default, given it is usually right?
It is usually right, which is the problem. A heuristic that is correct 90% of the time and silently destructive the other 10% is worse than one that refuses, because you stop checking. Correctness rate is the wrong metric for a destructive operation. The right one is what happens on the miss.
Does refusing to apply make the tool less useful?
Only in the cases where it was about to be wrong. Errors that name a file still get one-click apply, which is most of them. The narrow rule is: if the target file was inferred rather than named, refuse the write that depends on the guess and offer the fix as text.
Why not require the model to always output a file path?
Because it will always output one. That is the thing models are good at. A required field gets filled whether or not the information exists, and a confidently invented path is harder to catch than a missing one. Absent is a better signal than fabricated.
Where did this actually happen?
On a demo app in /tmp, on an unreleased branch, which is why this is a blog
post rather than an apology. The applier guards are the part that shipped, and
they default to previous behaviour for every existing caller.
DebugAI reads the stack trace, pulls the files that matter, and hands you a fix you can apply in one click. It now also knows when it does not know where to put one.
Debug faster starting today.
Free VS Code extension · 10 sessions/day · no credit card