Skip to content

Environment Variables

herdctl supports environment variable interpolation in configuration files, allowing you to inject secrets and environment-specific values without hardcoding them.

Environment variables can be referenced in any string value within your configuration using the ${VAR_NAME} syntax.

Use ${VAR_NAME} to reference a required environment variable:

# agent config (fleet-level equivalent: defaults.mcp_servers)
mcp_servers:
github:
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_TOKEN: ${GITHUB_TOKEN}

If the variable is not defined, herdctl will throw an error at configuration load time:

UndefinedVariableError: Undefined environment variable 'GITHUB_TOKEN' at 'mcp_servers.github.env.GITHUB_TOKEN' (no default provided)

Use ${VAR_NAME:-default} to provide a fallback value:

working_directory:
root: ${HERDCTL_WORKSPACE_ROOT:-/home/user/herdctl-workspace}
defaults:
model: ${CLAUDE_MODEL:-claude-sonnet-4-20250514}

If HERDCTL_WORKSPACE_ROOT is not set, the value /home/user/herdctl-workspace will be used instead.


Environment variable interpolation works on any string value in your configuration, at any nesting depth:

# Top-level strings
version: 1
fleet:
name: ${FLEET_NAME:-production}
description: Fleet for ${ENVIRONMENT:-development} environment
# Nested in objects
working_directory:
root: ${WORKSPACE_ROOT:-/home/user/herdctl}
# Inside arrays
agents:
- path: ./agents/${AGENT_PROFILE:-default}.yaml
- path: ${CUSTOM_AGENT_PATH}
# Deeply nested
defaults:
allowed_tools:
- "Bash(${PACKAGE_MANAGER:-npm} *)"
# MCP server configuration (fleet defaults; also valid at agent top level)
mcp_servers:
github:
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_TOKEN: ${GITHUB_TOKEN}
API_URL: ${GITHUB_API_URL:-https://api.github.com}

Interpolation only works in string fields. This is a hard rule, not just a convenience: herdctl validates the configuration against its schema before interpolation runs. A ${VAR} reference in a field the schema types as a number or boolean is still the literal string "${VAR}" at validation time, so validation fails before interpolation ever gets a chance to substitute the value.

# These FAIL schema validation - number/boolean fields cannot be interpolated
defaults:
max_turns: ${MAX_TURNS:-50} # Error: expected number, got string
docker:
enabled: ${DOCKER_ENABLED:-true} # Error: expected boolean, got string
# Set numeric/boolean values literally instead
defaults:
max_turns: 50
docker:
enabled: true

Non-string values written as literals (numbers, booleans, null) are preserved as-is and never interpolated:

working_directory:
root: /home/user/herdctl-workspace
clone_depth: 1 # Number - no interpolation
auto_clone: true # Boolean - no interpolation

You can use multiple variables in a single string value:

fleet:
description: "${TEAM_NAME} fleet in ${ENVIRONMENT}"
working_directory:
root: ${HOME}/${PROJECT_NAME:-herdctl}/workspace

When a required variable (without a default) is undefined, configuration loading fails immediately with a descriptive error:

UndefinedVariableError: Undefined environment variable 'API_KEY' at 'mcp.servers[0].env.API_KEY' (no default provided)

The error message includes:

  • The variable name that’s missing
  • The full path in the configuration where it was referenced
  • A reminder that no default was provided

Environment variables are resolved when the configuration is loaded, not when it’s parsed. This means:

  1. YAML syntax is validated first
  2. Schema validation runs second
  3. Environment variable interpolation happens third

If interpolation fails, you’ll see the UndefinedVariableError after YAML and schema validation pass.

Because schema validation runs before interpolation, ${VAR} references are only valid in string fields — see String Fields Only above.


Keep sensitive values out of version control:

.gitignore
.env
.env.local
.env.*.local

Always use interpolation for sensitive values:

# Good: Secrets come from environment (herdctl.yaml)
defaults:
mcp_servers:
github:
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_TOKEN: ${GITHUB_TOKEN}
custom-api:
command: node
args: ["./mcp-servers/custom.js"]
env:
API_KEY: ${API_KEY}
webhooks:
secret_env: WEBHOOK_SECRET # Reference by name, not value
# Bad: Secrets hardcoded in config (agent config)
mcp_servers:
github:
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_TOKEN: ghp_xxxxxxxxxxxx # Never do this!

Export variables in your shell or use a secrets manager:

Terminal window
# In your shell profile (~/.bashrc, ~/.zshrc)
export ANTHROPIC_API_KEY="sk-ant-..."
export GITHUB_TOKEN="ghp_..."
# Or set them when running herdctl
ANTHROPIC_API_KEY="sk-ant-..." herdctl run coder

For production deployments, use your platform’s secrets management:

  • CI/CD: Use GitHub Actions secrets, GitLab CI variables, etc.
  • Kubernetes: Use Kubernetes Secrets or external secrets operators
  • Cloud: Use AWS Secrets Manager, GCP Secret Manager, Azure Key Vault
  • Docker: Use Docker secrets or environment files

# MCP servers - agent config top level, or fleet-level under defaults.mcp_servers
mcp_servers:
github:
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_TOKEN: ${GITHUB_TOKEN}
herdctl.yaml
# Docker environment variables (fleet-level only)
defaults:
docker:
env:
GITHUB_TOKEN: ${GITHUB_TOKEN}
API_KEY: ${API_KEY}
# Chat integrations are per-agent (each agent has its own bot)
# See agent config for chat settings
mcp_servers:
custom-api:
command: node
args: ["./mcp-servers/custom-api.js"]
env:
API_BASE_URL: ${API_BASE_URL:-https://api.example.com}
API_VERSION: ${API_VERSION:-v1}
working_directory:
root: ${HERDCTL_WORKSPACE:-/home/user/herdctl-workspace}
agents:
- path: ${AGENTS_DIR:-./agents}/coder.yaml
- path: ${AGENTS_DIR:-./agents}/reviewer.yaml
fleet:
name: ${FLEET_NAME:-development}
description: ${FLEET_DESCRIPTION:-Local development fleet}
defaults:
model: ${CLAUDE_MODEL:-claude-sonnet-4-20250514}
# Numeric fields like instances.max_concurrent cannot be interpolated
# (validation runs before interpolation) - set them literally:
instances:
max_concurrent: 2
agents:
- path: ./agents/${AGENT_PROFILE:-standard}.yaml

Set AGENT_PROFILE=advanced to load ./agents/advanced.yaml instead of the default.


While herdctl doesn’t require specific environment variables, these are commonly used:

VariableDescriptionExample
ANTHROPIC_API_KEYClaude API key for agent sessionssk-ant-...
GITHUB_TOKENGitHub API token for MCP serverghp_...
LINEAR_API_KEYLinear API key for issue trackinglin_api_...
<AGENT>_DISCORD_TOKENPer-agent Discord bot token (e.g., SUPPORT_DISCORD_TOKEN)
SLACK_BOT_TOKENSlack Bot User OAuth Token (xoxb-) — shared across agents
SLACK_APP_TOKENSlack App-Level Token (xapp-) for Socket Mode
SLACK_CHANNEL_IDSlack channel ID for agent routingC0123456789

Here’s a full configuration demonstrating environment variable usage:

version: 1
fleet:
name: ${FLEET_NAME:-production}
description: ${FLEET_DESCRIPTION:-Agent fleet for ${TEAM_NAME:-engineering}}
defaults:
model: ${CLAUDE_MODEL:-claude-sonnet-4-20250514}
# max_turns is a number field - it cannot use ${VAR} interpolation
max_turns: 50
permission_mode: acceptEdits
allowed_tools:
- "Bash(${PACKAGE_MANAGER:-npm} *)"
- "Bash(git *)"
- "Bash(node *)"
mcp_servers:
github:
command: npx
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_TOKEN: ${GITHUB_TOKEN}
filesystem:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "${ALLOWED_PATHS:-/tmp}"]
working_directory:
root: ${HERDCTL_WORKSPACE:-/home/user/herdctl-workspace}
auto_clone: true
clone_depth: 1
agents:
- path: ./agents/${AGENT_PROFILE:-default}/coder.yaml
- path: ./agents/${AGENT_PROFILE:-default}/reviewer.yaml
# Note: Chat is configured per-agent, not at fleet level.
# Discord: each agent references its own bot token env var.
# Slack: agents share one bot token, with channel-to-agent routing.
webhooks:
enabled: true
# port is a number field - it cannot use ${VAR} interpolation
port: 8081
secret_env: WEBHOOK_SECRET

”Undefined environment variable” Error

Section titled “”Undefined environment variable” Error”

Problem: Configuration fails to load with UndefinedVariableError.

Solution: Either set the missing variable or provide a default:

Terminal window
# Option 1: Set the variable
export MISSING_VAR="value"
# Option 2: Add a default in config
# Change: ${MISSING_VAR}
# To: ${MISSING_VAR:-default_value}

Problem: The ${VAR} syntax appears in the final config instead of the value.

Possible causes:

  1. Variable name contains invalid characters (must match [A-Za-z_][A-Za-z0-9_]*)
  2. Malformed syntax (missing }, extra spaces)
  3. Value is not a string type in YAML
# Invalid variable names
${123_VAR} # Can't start with number
${VAR-NAME} # Hyphens not allowed (use underscores)
${VAR NAME} # Spaces not allowed
# Valid variable names
${VAR_NAME}
${_PRIVATE_VAR}
${myVar123}

Validate your configuration before running:

Terminal window
# Check that all required variables are set
herdctl config validate
# See the resolved configuration
herdctl config show