Skip to content

${VAR} in settings.json headers not resolved from .env files env var substitution runs before .env is loaded #4466

Description

@R-omk

Environment: Qwen Code 0.16.0

Context

I'm using Qwen Code with sandbox enabled (docker) and a custom MCP server (mcp-github-docker) running in a Docker Compose network. I want to pass a personal access token via ${GITHUB_PERSONAL_ACCESS_TOKEN} in the MCP server headers, with the actual token stored in ~/.qwen/.env.

Current configuration

~/.qwen/settings.json:

{
  "env": {
    "OLLAMA_API_KEY": "",
    "VLLM_API_KEY": "",
    "LMSTUDIO_API_KEY": "",
    "CUSTOM_API_KEY": "any",
    "QWEN_SANDBOX_IMAGE": "qwen-code-sandbox",
    "SANDBOX_FLAGS": "--network mcp-network"
  },
  "tools": {
    "sandbox": true
  },
  "mcpServers": {
    "mcp-github": {
      "httpUrl": "http://mcp-github-docker:8082",
      "headers": {
        "Authorization": "Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}"
      },
      "timeout": 60000,
      "trust": true
    }
  }
}

~/.qwen/.env:

GITHUB_PERSONAL_ACCESS_TOKEN=github_pat_xxx

What happens

The ${GITHUB_PERSONAL_ACCESS_TOKEN} placeholder in headers is not substituted

Workaround (with side effects)

The only way to make this work is to add the token via SANDBOX_ENV:

# ~/.qwen/.env
SANDBOX_ENV=GITHUB_PERSONAL_ACCESS_TOKEN=github_pat_xxx

This has two problems:

  1. Secret leaks to console — every time Qwen Code starts, the full SANDBOX_ENV: GITHUB_PERSONAL_ACCESS_TOKEN=github_pat_xxx is printed to stderr.
  2. Two-step workaround — I still need export GITHUB_PERSONAL_ACCESS_TOKEN=xxx for the ${VAR} substitution to work in settings, plus SANDBOX_ENV for the sandbox container to receive it.

Root cause (analysis of source code)

In packages/cli/src/config/settings.ts, the loadSettings() function executes in this order:

  1. preResolveHomeEnvOverrides() — loads only QWEN_HOME / QWEN_RUNTIME_DIR from .env
  2. Read settings.json (all scopes)
  3. resolveEnvVarsInObject() — substitutes ${VAR} from process.env
  4. Merge settings + trust check
  5. loadEnvironment() — loads the full .env file into process.env

Step 3 (variable substitution) happens before step 5 (.env loading). So any variable defined only in a .env file is not available for ${VAR} substitution.

Expected behavior

  • Variables from ~/.qwen/.env should be available for ${VAR} substitution in settings.json, including MCP server headers.
  • OR: there should be documented guidance on how to properly use .env secrets with MCP server configuration.

Related issues

  • #3877 — similar problem where project-level .env overrides ~/.qwen/.env (different root cause but same symptom: .env variables not reaching their expected consumers)

Additional notes

  • SANDBOX_ENV is not documented anywhere in the official documentation
  • The full list of environment variables forwarded to the sandbox container is not documented
  • This also affects non-sandbox mode — ${VAR} in settings.json cannot be resolved from .env files regardless of sandbox usage

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions