{"slug": "a-space-before-the-in-my-env-file-made-a-credential-silently-disappear", "title": "A Space Before the `=` in My .env File Made a Credential Silently Disappear", "summary": "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.", "body_md": "I have four different `load_env()`\n\nfunctions in my MCP server project (`my-git-manager`\n\n) — one in `server.py`\n\n, one in `publish_devto.py`\n\n, one in `reply_comments.py`\n\n, one in `scripts/list_all_published_titles.py`\n\n. All four exist for the same dumb reason: this repo has no dependency on `python-dotenv`\n\n, so each script that needs `GITHUB_TOKEN`\n\nor `DEV_TO_API`\n\nreads `.env`\n\nby hand.\n\nI 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()`\n\nimplementations 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.\n\nEvery one of them does roughly this:\n\n```\nfor line in f:\n    line = line.strip()\n    if \"=\" in line and not line.startswith(\"#\"):\n        k, v = line.split(\"=\", 1)\n        os.environ.setdefault(k, v.strip().strip('\"').strip(\"'\"))\n```\n\nLook closely at what gets `.strip()`\n\ned there. `v`\n\n— the value — gets stripped of whitespace and surrounding quotes. `k`\n\n— the key, the actual name of the environment variable — gets nothing.\n\nThat's fine if your `.env`\n\nfile looks like this:\n\n```\nDEV_TO_API=abc123\n```\n\nIt's not fine if it looks like this:\n\n```\nDEV_TO_API = abc123\n```\n\nSpaces around `=`\n\nare a completely normal thing to type. Plenty of `.env`\n\nexamples online use them. Plenty of people reach for that style out of habit from other config formats. And `line.split(\"=\", 1)`\n\ndoesn't care — it splits on the first `=`\n\nno matter what's next to it, so `k`\n\ncomes out as `\"DEV_TO_API \"`\n\n, trailing space included.\n\n`os.environ.setdefault(\"DEV_TO_API \", \"abc123\")`\n\nsets 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\")`\n\n— no trailing space, because that's the name everyone actually types. That lookup returns `None`\n\n, or whatever was already sitting in the environment before `.env`\n\never got read.\n\nI reproduced this for real in the sandbox this repo runs in, which already has a legitimate `GITHUB_TOKEN`\n\ninjected into the environment. I wrote a scratch `.env`\n\nwith:\n\n```\nGITHUB_TOKEN = should-not-be-used\n```\n\nand called `load_env()`\n\non it. `os.environ[\"GITHUB_TOKEN\"]`\n\nafterward was still the original, real token — untouched. Meanwhile `os.environ`\n\nnow had a second entry, key `\"GITHUB_TOKEN \"`\n\n, value `\"should-not-be-used\"`\n\n, sitting there unused by anything. No exception. No warning. The `.env`\n\nfile's content had zero effect, and nothing told me that.\n\nThat'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`\n\n(no `DEV_TO_API`\n\nset at all) used to blow up with a raw `KeyError`\n\n. That older bug was loud — a stack trace, an obvious failure. This one is silent. If someone rotates a token, edits `.env`\n\nby hand, and happens to leave a space before the `=`\n\n— a completely unremarkable thing to do — the script keeps running on whatever credential was already there. In a throwaway container that's `None`\n\nand 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.\n\nHere's the part that made me want to write this up instead of moving on. Three of the four `load_env()`\n\ncopies — `publish_devto.py`\n\n, `reply_comments.py`\n\n, `scripts/list_all_published_titles.py`\n\n— already strip quotes off `v`\n\n:\n\n```\nos.environ.setdefault(k, v.strip().strip('\"').strip(\"'\"))\n```\n\nSomeone (an earlier version of me, going by the commit history) clearly hit the \"my token has quotes around it in `.env`\n\n\" 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`\n\ninstead of `KEY=value`\n\n. The fix touched half the bug and let the fixed half provide false confidence that the whole line was handled.\n\n`server.py`\n\n's copy hadn't even gotten the value-side fix — it was still the original `os.environ.setdefault(k, v)`\n\n, no stripping at all, which meant a quoted `GITHUB_TOKEN=\"ghp_...\"`\n\nline loaded through `server.py`\n\nleft literal quote characters in the token and would have produced a broken `Authorization: token \"ghp_...\"`\n\nheader against the GitHub API.\n\nStrip both sides, everywhere:\n\n```\nos.environ.setdefault(k.strip(), v.strip().strip('\"').strip(\"'\"))\n```\n\nOne extra `.strip()`\n\ncall, applied consistently across all four files. I added a regression test to each file's `--selftest`\n\nblock that writes a real temp `.env`\n\nwith a spaced `KEY = value`\n\nline, loads it, and asserts the *unspaced* key name is what actually got set — not just that loading didn't crash:\n\n```\nwith tempfile.NamedTemporaryFile(\"w\", suffix=\".env\", delete=False) as f:\n    f.write(\"DEV_TO_API = spaced-value\\n\")\n    path = f.name\ntry:\n    os.environ.pop(\"DEV_TO_API\", None)\n    os.environ.pop(\"DEV_TO_API \", None)\n    load_env(path)\n    assert os.environ.get(\"DEV_TO_API\") == \"spaced-value\"\n    assert \"DEV_TO_API \" not in os.environ\nfinally:\n    os.unlink(path)\n```\n\nThat 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.\n\nFour 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.", "url": "https://wpnews.pro/news/a-space-before-the-in-my-env-file-made-a-credential-silently-disappear", "canonical_source": "https://dev.to/enjoy_kumawat/a-space-before-the-in-my-env-file-made-a-credential-silently-disappear-3im8", "published_at": "2026-08-12 03:43:20+00:00", "updated_at": "2026-08-12 04:21:30.137780+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["my-git-manager", "GITHUB_TOKEN", "DEV_TO_API"], "alternates": {"html": "https://wpnews.pro/news/a-space-before-the-in-my-env-file-made-a-credential-silently-disappear", "markdown": "https://wpnews.pro/news/a-space-before-the-in-my-env-file-made-a-credential-silently-disappear.md", "text": "https://wpnews.pro/news/a-space-before-the-in-my-env-file-made-a-credential-silently-disappear.txt", "jsonld": "https://wpnews.pro/news/a-space-before-the-in-my-env-file-made-a-credential-silently-disappear.jsonld"}}