Why your MCP server cannot see env vars from .bashrc
Claude Code spawns MCP servers as plain child processes. They never source .bashrc. Set MCP env vars in environment.d or the MCP env config, not your shell rc files.
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.