← All posts
·8 min read

Claude Code Environment Variables

Claude CodeEnvironment VariablesSecretsSecurityConfiguration
Claude Code Environment Variables

Why environment variables matter in Claude Code

Environment variables are how you pass secrets, API keys, and configuration to Claude Code without hardcoding them into your project files. Get this wrong and your database credentials end up in a git commit. Get it right and Claude Code becomes a secure, configurable system that adapts across environments without touching a single line of code.

Claude Code interacts with environment variables at multiple levels: shell-level vars it inherits from your terminal, .env files it can read from your project, MCP server secrets defined in configuration, and the env blocks inside settings.json. Each level serves a different purpose, and understanding when to use which is the difference between a secure setup and a ticking time bomb.

If you're brand new to Claude Code, start with our beginner's guide before diving into environment configuration.

Shell environment variables

Claude Code runs inside your terminal session. That means it inherits every environment variable your shell has access to. If you export DATABASE_URL in your .zshrc or .bashrc, Claude Code can see it.

This is the simplest approach and works well for variables that are truly global to your machine:

# In your shell profile (~/.zshrc or ~/.bashrc)
export ANTHROPIC_API_KEY="sk-ant-..."
export CLAUDE_CODE_USE_BEDROCK=1

Claude Code itself uses several built-in environment variables:

  • ANTHROPIC_API_KEY: Your API key for direct Anthropic access
  • CLAUDE_CODE_USE_BEDROCK: Set to 1 to route through AWS Bedrock
  • CLAUDE_CODE_USE_VERTEX: Set to 1 to route through Google Vertex AI
  • CLAUDE_MODEL: Override the default model (e.g., claude-sonnet-4-20250514)
  • CLAUDE_CODE_MAX_TURNS: Limit turns in headless/non-interactive mode
  • DISABLE_PROMPT_CACHING: Set to 1 to disable prompt caching

Shell variables are appropriate for machine-level configuration. They are not appropriate for project-specific secrets, because they leak across every project on your machine.

Project-level .env files

Most projects store secrets in .env files at the project root. Claude Code can read these files when running Bash commands, any tool or script Claude invokes will have access to the variables defined there.

A typical .env file:

# .env - project secrets (NEVER commit this)
DATABASE_URL=postgres://user:pass@host:5432/mydb
STRIPE_SECRET_KEY=sk_live_...
REDIS_URL=redis://localhost:6379
API_SECRET=your-secret-here

The critical rule: never let Claude Code write to or modify your .env files. Claude should read environment variables at runtime, not manage the files themselves. A PreToolUse hook that blocks writes to .env files is essential:

#!/bin/bash
# Block all writes to .env files
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ "$FILE_PATH" == *.env* ]]; then
  echo "BLOCKED: Cannot write to env file: $FILE_PATH" >&2
  exit 1
fi
exit 0

This is non-negotiable. Even if you ask Claude to "update the database URL in .env," the hook should block it. Secrets management should never be delegated to an AI agent.

MCP server secrets and environment configuration

MCP servers are where environment variables get interesting. Each MCP server often needs its own credentials, API keys, tokens, connection strings. These are configured in your .mcp.json file using the env property:

{
  "mcpServers": {
    "my-database": {
      "command": "npx",
      "args": ["-y", "@my/database-mcp"],
      "env": {
        "DB_HOST": "localhost",
        "DB_PORT": "5432",
        "DB_PASSWORD": "secret-password"
      }
    },
    "my-api": {
      "command": "node",
      "args": ["./mcp-servers/api-server.js"],
      "env": {
        "API_KEY": "key-123",
        "API_SECRET": "secret-456"
      }
    }
  }
}

The env block passes variables directly to the MCP server process. These variables are scoped, they only exist in the context of that specific server, not in Claude Code's main session. This is good security practice because a database MCP server never sees your Stripe API key.

There is a catch. The .mcp.json file itself contains plaintext secrets. You have two options for managing this:

Option 1: Keep .mcp.json in .gitignore. Simple, effective. The downside is that every team member needs to configure their own MCP servers manually.

Option 2: Use environment variable references. Some MCP servers support reading from the shell environment rather than hardcoded values:

{
  "mcpServers": {
    "my-database": {
      "command": "npx",
      "args": ["-y", "@my/database-mcp"],
      "env": {
        "DB_PASSWORD": "${DB_PASSWORD}"
      }
    }
  }
}

This keeps the actual secret in your shell environment while .mcp.json only contains the reference. You can safely commit .mcp.json because it doesn't contain the actual credentials.

The settings.json env block

Claude Code's settings.json (located at .claude/settings.json) supports an env block that sets environment variables for all Bash commands Claude executes:

{
  "env": {
    "NODE_ENV": "development",
    "LOG_LEVEL": "debug",
    "PROJECT_ROOT": "/path/to/project"
  },
  "hooks": {
    "PreToolUse": []
  }
}

Variables defined here are injected into every Bash command Claude runs. This is useful for non-secret configuration, things like NODE_ENV, feature flags, or path overrides that are project-specific but not sensitive.

Do not put secrets in settings.json. This file is typically committed to version control (it contains your hooks configuration and other project settings). Anything in this file is visible to everyone with repository access.

The hierarchy of variable precedence:

  1. Bash command inline (FOO=bar command), highest priority
  2. settings.json env block: project-level defaults
  3. Shell environment (inherited from terminal), machine-level
  4. .env files (loaded by your application/scripts), application-level

If the same variable is defined at multiple levels, the higher-priority source wins.

Security best practices for Claude Code secrets

Managing secrets in an AI-assisted workflow requires extra caution. Claude Code is powerful, and that power extends to reading files, running commands, and accessing network resources. Here are the hard rules:

1. Never commit secrets to version control

This applies whether you're using Claude Code or not, but it bears repeating. Your .gitignore should include:

.env
.env.local
.env.production
*.pem
**/credentials.json
**/secrets.yml

2. Use hooks to block secret file modifications

As shown above, a PreToolUse hook on Write and Edit should block any attempt to modify files matching .env*, credentials*, secret*, *.pem, or *id_rsa*. This is a non-negotiable security boundary.

3. Scope MCP secrets narrowly

Each MCP server should only receive the credentials it needs. Don't pass your entire environment to every server. The env block in .mcp.json is scoped per server, use that scoping intentionally.

4. Rotate tokens regularly

MCP tokens, API keys, and service account credentials should have expiration dates. Track them. Claude Code's memory system is a good place to record token expiration dates so you get reminded during daily standups.

5. Use read-only tokens where possible

If an MCP server only needs to read data, give it a read-only token. If Claude Code only needs to query your database, use a read-only database user. Principle of least privilege applies doubly when an AI agent is involved.

6. Audit environment access

Use Stop hooks to log which environment variables Claude Code accesses during a session. This gives you visibility into what secrets are being used and when:

#!/bin/bash
# Log env var access patterns
INPUT=$(cat)
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "[$TIMESTAMP] Session turn completed" >> .claude/logs/env-audit.log

7. Separate development and production credentials

Never use production credentials in your development environment. If Claude Code has access to your production database token, a single misguided query could cause real damage. Use separate credentials for dev, staging, and production, and only give Claude Code the dev ones.

Common patterns for multi-environment setups

Teams often need different configurations for development, staging, and production. Here's a clean pattern using Claude Code's settings hierarchy:

Per-developer settings (.claude/settings.local.json, gitignored):

{
  "env": {
    "API_BASE_URL": "http://localhost:3000",
    "DEBUG": "true"
  }
}

Shared project settings (.claude/settings.json, committed):

{
  "env": {
    "NODE_ENV": "development",
    "LOG_FORMAT": "pretty"
  }
}

The local settings file overrides the shared one, letting each developer customize their environment without affecting the team.

Debugging environment variable issues

When Claude Code isn't picking up an environment variable, check these in order:

  1. Is the variable exported? In your shell, MY_VAR=value sets a local variable. export MY_VAR=value makes it available to child processes (including Claude Code).

  2. Did you restart Claude Code? Environment variables are read at session start. If you added a new export to .zshrc, you need to restart your terminal and Claude Code.

  3. Is settings.json valid? A malformed JSON file silently breaks all configuration, including env blocks. Validate with python3 -c "import json; json.load(open('.claude/settings.json'))".

  4. Is the MCP server receiving the variable? Add a debug log to your MCP server that prints its environment at startup. Check that the expected variables are present.

  5. Is there a naming conflict? If the same variable is defined at multiple levels, the precedence rules apply. A settings.json env block overrides a shell export for Bash commands.

Frequently asked questions

Can Claude Code read my .env file directly?

Claude Code can read any file in your project directory, including .env files. It can see the contents if you ask it to read the file or if a Bash command references it. This is why blocking write access via hooks is important, you can't prevent Claude from reading files (it needs to read code), but you can prevent it from modifying sensitive ones.

How do I pass secrets to MCP servers without hardcoding them?

Use environment variable references in your .mcp.json file (e.g., "${MY_SECRET}"), or keep .mcp.json in your .gitignore and configure secrets directly. The first approach lets you commit the MCP configuration while keeping secrets in your shell environment.

What happens if settings.json has an invalid env block?

If settings.json contains malformed JSON, all configuration breaks, not just the env block. Hooks stop firing, env vars don't load, and Claude Code falls back to default behavior. Always validate your JSON after editing settings.json. Your setup guide should include a validation step in your workflow.


Want a secure Claude Code environment out of the box? Claudify ships with pre-configured security hooks, MCP secret templates, and environment management patterns, so you never have to worry about accidental secret exposure. Production-tested configurations, ready to deploy.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir