cd /news/developer-tools/a-space-before-the-in-my-env-file-ma… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-93072] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

A Space Before the `=` in My .env File Made a Credential Silently Disappear

A developer discovered a silent bug in their MCP server project 'my-git-manager' where a space before the '=' in a .env file caused credentials to be ignored. The custom load_env() functions strip the value but not the key, so 'KEY = value' sets an environment variable with a trailing space in the key, which no caller looks up. The bug is silent, allowing scripts to run with stale credentials without any warning.

read5 min views1 publishedAug 12, 2026

I have four different load_env()

functions in my MCP server project (my-git-manager

) β€” one in server.py

, one in publish_devto.py

, one in reply_comments.py

, one in scripts/list_all_published_titles.py

. All four exist for the same dumb reason: this repo has no dependency on python-dotenv

, so each script that needs GITHUB_TOKEN

or DEV_TO_API

reads .env

by hand.

I went digging for a fresh bug in this repo this week β€” I write a lot about it, and the well is getting shallow β€” and decided to actually diff all four load_env()

implementations against each other instead of reading them one at a time like I usually do. They'd never been compared side by side before. That's how I found this one.

Every one of them does roughly this:

for line in f:
    line = line.strip()
    if "=" in line and not line.startswith("#"):
        k, v = line.split("=", 1)
        os.environ.setdefault(k, v.strip().strip('"').strip("'"))

Look closely at what gets .strip()

ed there. v

β€” the value β€” gets stripped of whitespace and surrounding quotes. k

β€” the key, the actual name of the environment variable β€” gets nothing.

That's fine if your .env

file looks like this:

DEV_TO_API=abc123

It's not fine if it looks like this:

DEV_TO_API = abc123

Spaces around =

are a completely normal thing to type. Plenty of .env

examples online use them. Plenty of people reach for that style out of habit from other config formats. And line.split("=", 1)

doesn't care β€” it splits on the first =

no matter what's next to it, so k

comes out as "DEV_TO_API "

, trailing space included.

os.environ.setdefault("DEV_TO_API ", "abc123")

sets an environment variable. It's just not the one anything is looking for. Every caller in this repo does os.environ.get("DEV_TO_API")

β€” no trailing space, because that's the name everyone actually types. That lookup returns None

, or whatever was already sitting in the environment before .env

ever got read.

I reproduced this for real in the sandbox this repo runs in, which already has a legitimate GITHUB_TOKEN

injected into the environment. I wrote a scratch .env

with:

GITHUB_TOKEN = should-not-be-used

and called load_env()

on it. os.environ["GITHUB_TOKEN"]

afterward was still the original, real token β€” untouched. Meanwhile os.environ

now had a second entry, key "GITHUB_TOKEN "

, value "should-not-be-used"

, sitting there unused by anything. No exception. No warning. The .env

file's content had zero effect, and nothing told me that.

That's the part that makes this worse than a bug I fixed a few days ago in the same function family, where a missing .env

(no DEV_TO_API

set at all) used to blow up with a raw KeyError

. That older bug was loud β€” a stack trace, an obvious failure. This one is silent. If someone rotates a token, edits .env

by hand, and happens to leave a space before the =

β€” a completely unremarkable thing to do β€” the script keeps running on whatever credential was already there. In a throwaway container that's None

and you get a clean, fast failure. On a long-lived machine where an old token is still exported from a previous session, you get a script that appears to work while silently ignoring the credential you thought you just updated.

Here's the part that made me want to write this up instead of moving on. Three of the four load_env()

copies β€” publish_devto.py

, reply_comments.py

, scripts/list_all_published_titles.py

β€” already strip quotes off v

:

os.environ.setdefault(k, v.strip().strip('"').strip("'"))

Someone (an earlier version of me, going by the commit history) clearly hit the "my token has quotes around it in .env

" problem at some point and fixed the value side. But nobody ever asked the obvious follow-up question: if the value needs stripping, does the key? It's the same split, the same line, the same habit of typing KEY = value

instead of KEY=value

. The fix touched half the bug and let the fixed half provide false confidence that the whole line was handled.

server.py

's copy hadn't even gotten the value-side fix β€” it was still the original os.environ.setdefault(k, v)

, no stripping at all, which meant a quoted GITHUB_TOKEN="ghp_..."

line loaded through server.py

left literal quote characters in the token and would have produced a broken Authorization: token "ghp_..."

header against the GitHub API.

Strip both sides, everywhere:

os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))

One extra .strip()

call, applied consistently across all four files. I added a regression test to each file's --selftest

block that writes a real temp .env

with a spaced KEY = value

line, loads it, and asserts the unspaced key name is what actually got set β€” not just that didn't crash:

with tempfile.NamedTemporaryFile("w", suffix=".env", delete=False) as f:
    f.write("DEV_TO_API = spaced-value\n")
    path = f.name
try:
    os.environ.pop("DEV_TO_API", None)
    os.environ.pop("DEV_TO_API ", None)
    load_env(path)
    assert os.environ.get("DEV_TO_API") == "spaced-value"
    assert "DEV_TO_API " not in os.environ
finally:
    os.unlink(path)

That second assertion matters as much as the first. It's not enough to check that the right value showed up β€” you have to check that the wrong, space-suffixed key didn't, or a future refactor could silently reintroduce a phantom key sitting next to the real one.

Four copies of the same nine-line function, and the value-stripping fix had already propagated to three of them before I noticed the key never got the same treatment in any of the four. When you copy-paste a small parsing function across files and later patch a bug in one half of what it does, that's exactly the moment to go back and ask whether the other half needs the same patch β€” not evidence the whole thing got fixed once and is now safe everywhere.

── more in #developer-tools 4 stories Β· sorted by recency
── more on @my-git-manager 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/a-space-before-the-i…] indexed:0 read:5min 2026-08-12 Β· β€”