Overfitting sounds like a sin: somebody trained on the test set. In practice it happens to careful people doing reasonable things. Two stories you will recognize.
The trace analyzer
You are building an LLM that reads agent traces and flags the ones where the agent went wrong. You collect 40 traces, label them by hand, and run your prompt. It misses a third of the errors. So you do the obvious thing: you open the misses, read them, understand why the model did not flag them, and add instructions to the prompt. "A tool call that returns an empty result and is never retried counts as an error." Run again: better. Open the new misses, patch again. After five rounds the prompt catches 95% of the errors in your 40 traces.
Every step was good engineering. But each instruction you wrote was shaped by a trace you had in front of you. Some instructions capture a real failure pattern; others only describe that one trace. From the dev score you cannot tell which.
The few-shot classifier
You are classifying support tickets with a few-shot prompt. Policy: every time the classifier gets a ticket wrong, add that ticket, with its correct label, to the examples. It is cheap, it feels like learning, and the dev accuracy goes up every single time, by construction: the model is now being shown the answer. Dev accuracy reaches 100%. Production accuracy moved a little, or not at all.
Both stories are the same mechanism. In classical overfitting the model has too much capacity and memorizes the training data. Here the model is frozen; the developer is the fitting agent, and the dev set is the training data. The prompt is where the memorization lives.