Early on in a small MCP server I built to manage my GitHub profile and DEV.to articles, every GitHub API call started failing with a 404 that made no sense. The token was valid, the endpoint was right, the request looked fine in isolation. The bug was one line up the stack: GITHUB_USERNAME
was being read straight out of .env
, and the value sitting in that variable was enjoy_kumawat
, my DEV.to handle, with an underscore. My actual GitHub username has no underscore. GitHub's API doesn't fuzzy-match; it looked for a user that doesn't exist and told me so, correctly.
The .env
file had one key doing two jobs. Both platforms needed "the username," both usernames were short lowercase strings that looked plausible in isolation, and whichever one got written to that slot first became the one every script quietly assumed was correct, right up until a call to the other platform used it.
GITHUB_USERNAME=enjoy_kumawat # looks fine, is actually the dev.to handle
The instinctive first fix, the one I almost reached for, is to make the code smarter about it: validate the username against a regex, ping both APIs at startup and figure out which one the value belongs to, add a fallback that tries GitHub first and falls back to a hardcoded default on 404. All of that is guessing logic. It would have worked, probably, and it would have been a second bug waiting to happen the next time someone added a third platform with its own username shape, or the regex assumption turned out to be wrong for some edge case.
What actually fixed it was removing the ambiguity instead of building a system to resolve it:
GITHUB_USERNAME = "enjoykumawat" # no underscore: this repo's GitHub identity
DEV_USERNAME = "enjoy_kumawat" # with underscore: DEV.to identity
Two named constants, hardcoded in the scripts that use them, each with a comment that states which platform it's for and why it looks the way it does. No lookup, no inference, no "smart" resolution step. If a third platform ever gets added, it gets its own named constant too, and the pattern doesn't degrade as things scale, because nothing is being inferred from a shared slot in the first place.
I wrote this down as its own decision record at the time (ADR-003
in this repo's docs/project_notes/decisions.md
) specifically so I wouldn't relitigate it later: "Hardcode GitHub username, don't read from .env
." Reading the ADR back now, the interesting part isn't the fix, it's the framing: the alternative I didn't write down as a real option was "add validation so the ambiguous read fails loudly instead of silently." That's the more common advice you'll hear about this class of bug: fail fast, validate inputs, add an assertion. It's better than nothing. But it still leaves the ambiguity in the data and just adds a guard in front of it. The guard has to be maintained, has to cover every new case, and only tells you after the fact that something guessed wrong.
This distinction matters more now than it did when I first hit it, because the thing reading .env
values in this repo isn't only me anymore. An agent working on this codebase will grep for GITHUB_USERNAME
, find it, and use whatever string comes back with complete confidence. It has no independent way to know that key was semantically overloaded unless something in the repo tells it so. A human debugging a 404 has priors: "huh, that username looks off, let me check where it came from." An agent asked to "wire up the GitHub client" doesn't have that hesitation by default. It trusts the name of the variable, not the shape of the string inside it, unless you've made the variable name itself impossible to misread.
That's the actual generalizable point, and it's broader than usernames. Any place in a codebase where one storage slot is reused for two subtly different meanings is a spot where a reader, human or agent, has to guess correctly every single time, forever, with no signal that a guess is even being made. You can teach the reader to guess better (validation, fallbacks, runtime checks), or you can make the slot stop being ambiguous (separate, explicitly named constants, one per meaning). The second one doesn't scale in difficulty as more readers show up. The first one does, because every new reader needs to be taught the same disambiguation rule again.
I've started treating "does this value's name fully describe what it holds, with nothing left to infer from context" as a real review question on this project, not just a style nitpick. It costs nothing to answer while you're writing the constant. It costs a confusing 404 and a debugging session to answer it later, once something is already depending on the wrong guess.