[BUG] 'env' vars from .claude/settings.json not passed to the plugins/mcps.

Status Open
Maintainer reply ✓ Yes — claude[bot]
Activity 18 comments · opened Nov 19, 2025
💡 Likely answer: A maintainer (claude[bot], contributor) responded on this thread — see the highlighted reply below.

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

I have developed a set of Claude code plugins for the company I work for. Part of using the plugins involves adding a settings.json file in the .claude folder within the local repository.
This is how it looks (of course, with secrets and information masked).

{
  "env": {
    "SET_OF_ENV_VARS": "ENV_VARS_VALUE",
  },
  "enabledPlugins": {
    "plugin_name@plugin_owner": true,
    "plugin_name@plugin_owner": true,
    "plugin_name@splugin_owner": true
  },
  "extraKnownMarketplaces": {
    "claude-code-plugins": {
      "source": {
        "source": "github",
        "repo": "NAME_OF_PLUGIN_REPO",
      }
    }
  }
}

The issue is that from a few versions onwards (I am not sure which one), you no longer pass the 'env' variables properly. As a result, when using claude --debug, it reports that some ENV vars are missing, even though I have them and it was working perfectly before. Consequently, many MCPs no longer function correctly, or due to their retry mechanisms, it results in duplicated sessions for that MCP (e.g., context7, Serena, etc).

As a note: if I put the env vars inside .env file and I use the MCPs locally directly (not through the plugin or passing .env vars through .claude/settings.json) - it seems to be working.

Let me know if you need more info.
Thanks

What Should Happen?

ENV vars should be passed

Error Messages/Logs

2025-11-18T07:02:43.389Z [DEBUG] Found 5 plugins (4 enabled, 1 disabled)
2025-11-18T07:02:43.389Z [DEBUG] Plugin not available for MCP: plugin_name@plugin_owner - error type: hook-load-failed
2025-11-18T07:02:43.390Z [WARN] Missing environment variables in plugin MCP config: OPENAI_API_KEY, VECTOR_STORE_HOST, VECTOR_STORE_USERNAME, VECTOR_STORE_PASSWORD
2025-11-18T07:02:43.390Z [WARN] Missing environment variables in plugin MCP config: SERENA_PROJECT_PATH

Steps to Reproduce

  1. Create a plugin that requires environment variables to be passed, for MCPs or other purposes.
  2. Set it up in a local repository with .claude/settings.json, enabling the plugin and providing the necessary .env variables.
  3. Attempt to use it and verify that everything functions correctly.

Claude Model

None

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

2.0.46 (Claude Code)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

VS Code integrated terminal

Additional Information

_No response_

View original on GitHub ↗

18 Comments

b0dea · 10 months ago

Any update on this one?

matscederholm · 10 months ago

OTLP Telemetry Not Working Due to Env Var Bug

Version: Claude Code 2.0.65
Platform: macOS
Impact: Critical - All OTLP telemetry stopped working

Issue

Environment variables from ~/.claude/settings.json are not being loaded by Claude Code, which breaks OTLP telemetry completely. The env section configuration is correct and validated:

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_METRICS_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "http://10.90.55.3:4317",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
    "OTEL_METRIC_EXPORT_INTERVAL": "10000",
    "OTEL_RESOURCE_ATTRIBUTES": "user.email=user@example.com"
  }
}

Verification

Running ps eww -p <claude-pid> confirms NO OTEL environment variables are set for the Claude Code process, despite being correctly configured in settings.json.

Running lsof -nP -i4TCP | grep claude shows Claude Code has NO active connections to the configured OTLP endpoint (port 4317), confirming the env vars aren't being loaded.

Impact

  • Datadog OTLP ingestion receives zero metrics
  • Affects entire team that upgraded to 2.0.65
  • Monitoring dashboards completely broken
  • Worked in previous versions, confirmed regression

This significantly impacts teams using OTLP for monitoring Claude Code usage and costs.

b0dea · 9 months ago

Has this been fixed?

eduardolat · 9 months ago

+1

nagromLobo · 9 months ago

+1

tylerstraub · 9 months ago

+1

mattmcde78 · 8 months ago

+1

jat255 · 8 months ago

I was seeing this, and it turns out it was because I had IS_DEMO set in my environment. Removing that fixed the issue

edumusikse · 8 months ago

Adding diagnostic evidence from a separate reproduction (originally filed as #22571, closing as duplicate).

Reproduction context: .mcp.json in project root with @modelcontextprotocol/server-memory:

{
  "mcpServers": {
    "memory": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-memory"],
      "env": {
        "MEMORY_FILE_PATH": "/Users/me/.claude-memory/memory.json"
      }
    }
  }
}

Confirmed via process inspection:

  • Claude Desktop's spawned server (via claude_desktop_config.json): MEMORY_FILE_PATH is set ✅
  • Claude Code's spawned server (via .mcp.json): MEMORY_FILE_PATH is not set ❌

Both use identical config — only the Claude Code spawner fails to pass env vars through.

Impact for server-memory specifically: Knowledge graph data is not persisted to the configured file path and is not shared with Claude Desktop, since the server silently falls back to an ephemeral in-memory store with no warning.

Environment: macOS Darwin 25.2.0 (arm64), Claude Code latest as of 2026-02-02.

prong-raft-squishy · 8 months ago

Workaround for anyone hitting this: Use a wrapper script that sets the env vars before exec, then point your MCP config at the wrapper instead of the bare command.

Example for Playwright MCP (needed PLAYWRIGHT_MCP_BROWSER=chromium):

#!/bin/bash
export PLAYWRIGHT_MCP_BROWSER=chromium
exec npx @playwright/mcp@latest "$@"

In ~/.claude/.mcp.json:

{
  "mcpServers": {
    "playwright": {
      "command": "/path/to/playwright-mcp-wrapper",
      "args": []
    }
  }
}

The wrapper sets the env in its own process before exec, so it doesn't depend on Claude Code passing it through. Works for any MCP server that needs env vars.

Full details: https://gist.github.com/prong-raft-squishy/9b1ff44310c90de9fc70c0232d67632f

kws1207 · 7 months ago

This bug has been open since November 2025 — over 3 months with bug, has repro, and area:mcp labels, yet no assignee or milestone.

Multiple users have confirmed the issue with detailed reproduction steps (#22571, #28090 closed as duplicates, #23216 still open separately). The community-suggested wrapper script workaround also doesn't work in the VSCode extension — the extension doesn't even pick up the updated mcp.json, continuing to spawn the server with the old config.

This is a basic contract: env block in MCP config should be passed to the spawned process. It worked before, it broke, and it hasn't been fixed.

Can someone from the team please prioritize this? cc @bcherny @ant-kurt @fvolcic

bytemines · 7 months ago

This might help partially. I built a config orchestrator that has an env template system — .env.template gets tracked in git with the variable names, .env.local stays gitignored with the actual values. When you set up on a new machine, mycelium env setup walks you through filling in the values.

Not a full fix for the underlying issue but it makes managing MCP env vars across machines less painful.

https://github.com/bytemines/mycelium

Valentyn-Kubrak · 6 months ago

Can confirm this is still an issue. In our case, env variables in plugin .mcp.json with stdio transport load inconsistently — sometimes they resolve correctly, sometimes they don't, with no changes to the config between sessions. This makes plugin-distributed MCP servers unreliable and hard to debug.

Would really appreciate a fix for this.

Valentyn-Kubrak · 6 months ago

As a temporary workaround, I wrote a script that merges plugin .mcp.json configs into the project-level .mcp.json with env vars interpolated from .claude/settings.local.json. Maybe it helps someone:


#!/usr/bin/env bash
# build-mcp.sh — Workaround for Claude Code bug #11927
# Merges plugin .mcp.json configs into project-level .mcp.json
# with env vars interpolated from .claude/settings.local.json
#
# Usage: ./scripts/build-mcp.sh
#
# What it does:
#   1. Reads env vars from .claude/settings.local.json
#   2. Finds .mcp.json from each enabled plugin (latest version in cache)
#   3. Interpolates ${VAR} patterns with actual values
#   4. Merges everything into project .mcp.json (gitignored)
#   5. Preserves any non-plugin servers already in .mcp.json

set -euo pipefail

PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SETTINGS_LOCAL="$PROJECT_DIR/.claude/settings.local.json"
SETTINGS_PROJECT="$PROJECT_DIR/.claude/settings.json"
MCP_OUT="$PROJECT_DIR/.mcp.json"
PLUGIN_CACHE="$HOME/.claude/plugins/cache"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'

if ! command -v jq &>/dev/null; then
  echo -e "${RED}Error: jq is required. Install with: brew install jq${NC}"
  exit 1
fi

if [[ ! -f "$SETTINGS_LOCAL" ]]; then
  echo -e "${RED}Error: $SETTINGS_LOCAL not found. Run /setup-tokens first.${NC}"
  exit 1
fi

if [[ ! -f "$SETTINGS_PROJECT" ]]; then
  echo -e "${RED}Error: $SETTINGS_PROJECT not found.${NC}"
  exit 1
fi

echo "Build MCP"
echo "Settings: $SETTINGS_LOCAL"
echo ""

# Load env vars as JSON object for jq-based interpolation
ENV_JSON=$(jq -c '.env // {}' "$SETTINGS_LOCAL")
ENV_COUNT=$(echo "$ENV_JSON" | jq 'length')
echo -e "  Loaded ${GREEN}${ENV_COUNT}${NC} env vars from settings.local.json"

# Get enabled plugins
ENABLED_PLUGINS=$(jq -r '.enabledPlugins // {} | to_entries[] | select(.value == true) | .key' "$SETTINGS_PROJECT")

# Start: preserve manual (non-plugin) servers from existing .mcp.json
if [[ -f "$MCP_OUT" ]]; then
  MANUAL_SERVERS=$(jq '.mcpServers // {} | with_entries(select(.value.__plugin_source == null))' "$MCP_OUT")
  MANUAL_COUNT=$(echo "$MANUAL_SERVERS" | jq 'length')
  if [[ "$MANUAL_COUNT" -gt 0 ]]; then
    echo -e "  Preserved ${MANUAL_COUNT} manual MCP server(s)"
  fi
else
  MANUAL_SERVERS='{}'
fi

MERGED="$MANUAL_SERVERS"
PLUGIN_COUNT=0
SERVER_COUNT=0

for plugin_entry in $ENABLED_PLUGINS; do
  plugin_name="${plugin_entry%%@*}"
  marketplace="${plugin_entry#*@}"

  plugin_cache_dir="$PLUGIN_CACHE/$marketplace/$plugin_name"
  [[ -d "$plugin_cache_dir" ]] || continue

  latest_version=$(ls -d "$plugin_cache_dir"/*/ 2>/dev/null | sort -V | tail -1)
  [[ -n "$latest_version" ]] || continue

  mcp_file="${latest_version}.mcp.json"
  [[ -f "$mcp_file" ]] || continue

  plugin_servers=$(jq -c '.mcpServers // {}' "$mcp_file")
  [[ "$plugin_servers" != "{}" ]] || continue

  # Interpolate ${VAR} and ${VAR:-default} using jq
  # Also add __plugin_source marker
  interpolated=$(echo "$plugin_servers" | jq --argjson env "$ENV_JSON" --arg src "$plugin_name" '
    # Walk all string values and replace ${VAR} patterns
    def interpolate:
      if type == "string" then
        # Replace ${VAR:-default} and ${VAR} patterns
        reduce ($env | to_entries[]) as $e (.;
          gsub("\\$\\{" + $e.key + ":-[^}]*\\}"; $e.value) |
          gsub("\\$\\{" + $e.key + "\\}"; $e.value)
        )
      elif type == "array" then
        map(interpolate)
      elif type == "object" then
        with_entries(.value |= interpolate)
      else
        .
      end;
    with_entries(
      .value |= (interpolate | . + {"__plugin_source": $src})
    )
  ')

  server_count=$(echo "$interpolated" | jq 'length')
  SERVER_COUNT=$((SERVER_COUNT + server_count))
  PLUGIN_COUNT=$((PLUGIN_COUNT + 1))

  # Merge
  MERGED=$(echo "$MERGED" | jq --argjson new "$interpolated" '. + $new')

  version_name=$(basename "$latest_version")
  server_names=$(echo "$interpolated" | jq -r 'keys[]' | tr '\n' ', ' | sed 's/,$//')
  echo -e "  ${GREEN}✓${NC} $plugin_name ($version_name): $server_names"
done

# Check for unresolved ${VAR} patterns
unresolved=$(echo "$MERGED" | grep -oE '\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}' | sort -u || true)
if [[ -n "$unresolved" ]]; then
  echo ""
  echo -e "  ${YELLOW}Warning: unresolved variables:${NC}"
  for var in $unresolved; do
    echo -e "    ${YELLOW}$var${NC}"
  done
fi

# Write
jq -n --argjson servers "$MERGED" '{"mcpServers": $servers}' > "$MCP_OUT"

echo ""
echo -e "${GREEN}Done!${NC} $MCP_OUT updated"
echo "  Plugins: $PLUGIN_COUNT"
echo "  MCP servers: $SERVER_COUNT"
echo ""
echo "Restart Claude Code session to pick up changes."
Blackspell01 · 5 months ago

how is it possible that ts is not fixed yet?

gtapps · 1 month ago

Still happening on 2.1.235, and on Linux CLI (this is labelled macOS, so here's another platform).

One thing worth adding to the scope: it's not just .claude/settings.json. The same block in .claude/settings.local.json is ignored too, so switching files doesn't help.

After a few probings, testing both scopes with the same made-up variable names and a full restart each time:

| declared in | exported? |
|---|---|
| ~/.claude/settings.json | yes |
| <project>/.claude/settings.json | no |
| <project>/.claude/settings.local.json | no |

So it isn't that certain variable names are filtered out. Arbitrary names work fine from the user-level file. Project scope just never gets applied.

Two things that made this hard to spot:

  • The rest of the project file is honoured. enabledPlugins, permissions and hooks all work, so the file clearly isn't being ignored, only the env key.
  • Editing the block mid-session does nothing, despite the docs saying a running session picks up changes on save.

Where it hurt in practice: the official discord plugin or any channel really reads DISCORD_STATE_DIR to find its per-project credentials folder. Set in project scope, it silently falls back to the shared default and the server exits, and all you see is CONNECTION_CLOSED. Exporting the exact same value in the shell before launching makes it connect straight away, so the plugin and the credentials were fine the whole time.

The workarounds (user-level settings, or the shell) are both global, which means every project ends up pointing at one project's credentials. That's the thing this issue is really about for anyone using per-project plugin config.

Related: #41768 is the same problem with the same variable and was closed as stale rather than fixed. It still reproduces.

claude[bot] contributor · 1 month ago

Reproduced in part on 2.1.235 (Linux), using a stdio MCP server that records its environment when it is spawned, with distinct marker variables in user ~/.claude/settings.json env, project .claude/settings.json env, the server's own env block in .mcp.json, and the shell:

  • claude mcp list: the health-check spawn received the user-level and shell variables but not the project-level env value (deterministic across runs). So a server that needs a project-scoped variable is reported as failing by claude mcp list regardless of how it behaves in a session.
  • In the same setup, servers started by an actual session (claude interactive and claude -p; both a project .mcp.json server and a plugin-provided server loaded via --plugin-dir) received both the user-level and project-level values at spawn.

That matches the user-vs-project split in the table above for the mcp list path; the in-session case may depend on how the plugin is installed or how the server reads the variable, which this test did not cover.

🤖 Generated with Claude Code

---
_Generated by Claude Code_

smichalski7 · 1 month ago

We're seeing the same underlying issue with the Atlassian plugin (mcp-atlassian), which relies on JIRA_PERSONAL_TOKEN and CONFLUENCE_PERSONAL_TOKEN env vars set in .claude/settings.json:

  • v2.1.252 — works correctly; env vars are passed through to the plugin MCP.
  • v2.1.257 — broken; the env vars no longer reach the MCP server.

This looks like the same env-var-to-plugin-MCP propagation regression described above, just surfacing through a different plugin.