---
title: "Why your MCP server cannot see env vars from .bashrc"
date: 2026-04-25
tags: [claude, mcp, claude-code, linux, systemd, troubleshooting]
summary: 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.
aliases: [mcp-env-vars, mcp-env-vars-not-from-bashrc, claude-mcp-env]
---

You add an [MCP](https://modelcontextprotocol.io/) server to [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) 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:

```bash
# ~/.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:

1. **systemd `environment.d`** (Linux, systemd-based distros). Drop `~/.config/environment.d/mcp.conf`:

    ```
    MY_API_KEY=sk-xxxxxxxx
    ANTHROPIC_LOG=debug
    ```

   These 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, or `systemctl --user daemon-reload` and relog.

2. **The MCP server's own `env` config.** In `~/.claude/settings.json` or project `.mcp.json`:

    ```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.

3. **`~/.profile` or `~/.zprofile`**. Login shells source these, and most display managers do start the user session via a login shell. Cleaner than `.bashrc` but more fragile across distros.

4. **`launchctl setenv`** on macOS. Persists only inside the current `launchd` session unless you wire a `LaunchAgent` plist.

## 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
  bash -c '/path/to/mcp-server'
  ```

  An interactive shell sources `.bashrc`. `bash -c` doesn'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

- [[ssm-shell-apparmor-systemd-run]]. SSM child shells inherit an AppArmor profile silently.
- Cron with no `$PATH` because cron's environment is a stripped-down version of root's login env.
- Docker build steps not seeing `$DOCKER_BUILDKIT` because the daemon, not your shell, holds it. (See [[podman-vs-docker]])

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.

## References

- [Model Context Protocol — server configuration](https://modelcontextprotocol.io/docs/specification/server)
- [`environment.d(5)`](https://www.freedesktop.org/software/systemd/man/environment.d.html)
- [Claude Code MCP docs](https://docs.claude.com/en/docs/claude-code/mcp)
