You add an MCP server to Claude Code and it requires an environment variable. But the server doesn't show up when you run /mcp, no error surfaces, nothing useful in the logs. Nine times out of ten: an env var it needs is set in ~/.bashrc, and nothing Claude Code launches will ever see it.
# The mechanic
Claude Code spawns each MCP server as a plain child process. No interactive shell, no .bashrc sourcing. .bashrc is for interactive shells only. Non-interactive children inherit the parent's already-resolved environment, not the file.
So this:
# ~/.bashrc
export MY_API_KEY=sk-xxxxxxxx
is invisible to MCP servers, hooks, and skill scripts. The server starts, can't read the var, exits silently. Claude Code only sees "didn't initialise."
# Where to put env vars instead
In rough order of preference:
-
systemd
environment.d(Linux, systemd-based distros). Drop~/.config/environment.d/mcp.conf:MY_API_KEY=sk-xxxxxxxx ANTHROPIC_LOG=debugThese are part of the user session, loaded by
systemd --user, inherited by every desktop, IDE, terminal, and Claude Code process spawned from that session. Log out / log in to apply, orsystemctl --user daemon-reloadand relog. -
The MCP server's own
envconfig. In~/.claude/settings.jsonor project.mcp.json:{ "mcpServers": { "myserver": { "command": "/path/to/server", "env": { "MY_API_KEY": "sk-xxxxxxxx" } } } }Explicit and per-server. Downside: secrets end up in a config file. Only safe if it isn't synced or committed.
-
~/.profileor~/.zprofile. Login shells source these, and most display managers do start the user session via a login shell. Cleaner than.bashrcbut more fragile across distros. -
launchctl setenvon macOS. Persists only inside the currentlaunchdsession unless you wire aLaunchAgentplist.
# Confirming the failure mode
When an MCP server is missing from /mcp and you suspect env vars are the cause:
-
Run the binary from a non-interactive shell to reproduce:
bash -c '/path/to/mcp-server'An interactive shell sources
.bashrc.bash -cdoesn't. If it fails here, it'll fail when Claude Code spawns it. -
Add a startup log line that prints
os.environ.get("MY_API_KEY", "(missing)"). Fastest way to confirm the var isn't reaching the child.
# Child processes not seeing what you'd expect is a recurring failure mode
- Escaping the AWS SSM AppArmor profile with systemd-run. SSM child shells inherit an AppArmor profile silently.
- Cron with no
$PATHbecause cron's environment is a stripped-down version of root's login env. - Docker build steps not seeing
$DOCKER_BUILDKITbecause the daemon, not your shell, holds it. (See Living without docker: podman as a daily driver)
In every case, don't assume your shell's environment is what the child process actually sees. Set the var where the child picks it up.