Environment Variables Reference
Every environment variable Plan Forge reads, grouped by subsystem, with type, default, scope, and security note. The companion reference to Appendix T — .forge.json: settings that change per-machine or contain secrets live here; settings that travel with the project live there.
.forge/secrets.json or your shell environment: never in .forge.json. The secrets file is gitignored by default; the env-var fallback works the same way in CI runners and on developer machines. The Provider API Keys table flags every secret with a lock icon.
Orientation
Plan Forge reads roughly 40 environment variables across nine subsystems. Most have sensible defaults; the only ones you typically set yourself are provider API keys (so the orchestrator can call a model) and server ports (if 3100/3101 conflict with something else on your machine).
| Group | When you touch it |
|---|---|
| Provider API keys | Always, at least one model provider must be configured. |
| Azure OpenAI | Only when routing through Azure OpenAI instead of the model vendor's public API. |
| Server ports and network | Only if the default ports collide or you need to harden the bridge. |
| Project and runtime | Mostly internal (set by tests or by pforge itself). |
| Orchestrator timing | Tuning gate or worker timeouts on slow CI runners. |
| Feature toggles | Enabling experimental subsystems or bypassing checks. |
| Telemetry (OTel) | Sending traces to a collector. |
| Host detection (read-only) | Never, Plan Forge reads these from your IDE to pick the right adapter. |
| CLI internal | Set transiently by pforge itself; documented for transparency only. |
Every field row uses the same six columns, Variable, Type, Default, Scope, Set when, Security, so the table reads the same way no matter which subsystem you land on. Scope is one of per-user (export in your shell profile), per-machine (system env or CI variable), or per-session (transient, set on a single invocation).
Provider API keys
The orchestrator needs at least one of these to route a slice through a non-Copilot model. All are read from the environment first, then from .forge/secrets.json as a fallback (see pforge-mcp/secrets.mjs for the loader). The dashboard Config → Secrets tab is the friendliest way to set them, it writes the secrets file atomically and never echoes the value back.
| Variable | Type | Default | Scope | Set when | Security |
|---|---|---|---|---|---|
XAI_API_KEY 🔒 | string | (none) | per-user | You want to route slices through Grok models (grok-4.20, grok-4, grok-3, grok-3-mini). | Secret. Prefer .forge/secrets.json. |
OPENAI_API_KEY 🔒 | string | (none) | per-user | You want GPT models or DALL-E image generation (forge_generate_image). | Secret. Prefer .forge/secrets.json. |
ANTHROPIC_API_KEY 🔒 | string | (none) | per-user | You want Claude models directly (not via GitHub Copilot). | Secret. Prefer .forge/secrets.json. |
OPENCLAW_API_KEY 🔒 | string | (none) | per-user | openclaw.endpoint is set in .forge.json and PreAgentHandoff should authenticate. | Secret. Prefer .forge/secrets.json. |
GITHUB_TOKEN 🔒 | string | (none) | per-user | forge_meta_bug_file, forge_classifier_issue, or forge_github_metrics needs to call the GitHub API. gh auth status is the easier path when the GitHub CLI is installed. | Secret. Use a fine-scoped token; repo + issues is enough. |
OPENBRAIN_KEY 🔒 | string | (none) | per-user | OpenBrain replay needs authenticated access (rare; OpenBrain is local-first). | Secret. Prefer .forge/secrets.json. |
Azure OpenAI (alternative routing)
Set this group when your organization routes model calls through Azure OpenAI for billing, residency, or governance reasons. The keys are read by cost-service.mjs for pricing, by the orchestrator for invocation, and by forge_doctor_quorum for quota preflight (when PFORGE_FOUNDRY_QUOTA_PREFLIGHT=1).
| Variable | Type | Default | Scope | Set when | Security |
|---|---|---|---|---|---|
AZURE_OPENAI_ENDPOINT | string (URL) | (none) | per-user | Routing through Azure OpenAI. | Not secret, but reveals tenant name. Example: https://my-resource.openai.azure.com/. |
AZURE_OPENAI_API_KEY 🔒 | string | (none) | per-user | Key-based auth (not Managed Identity). | Secret. Prefer AZURE_AUTH_MODE=managed-identity when possible. |
AZURE_OPENAI_DEPLOYMENT | string | (none) | per-user | You need to override the deployment name parsed from the model spec. | Not secret. |
AZURE_OPENAI_API_VERSION | string | (none) | per-user | You need a specific Azure OpenAI API version (e.g. 2024-02-01). | Not secret. |
AZURE_OPENAI_DEPLOYMENT_TYPE | enum | "global" | per-user | Pricing is regional or data-zone rather than global. Read by cost-service.mjs. | Not secret. |
AZURE_OPENAI_ACCOUNT_NAME | string | (none) | per-user | Foundry quota preflight needs the account name (also accepts AZURE_OPENAI_RESOURCE_NAME as an alias). | Not secret. |
AZURE_SUBSCRIPTION_ID | string (GUID) | (none) | per-user | Foundry quota preflight or any Azure-RM call. | Not secret. |
AZURE_RESOURCE_GROUP | string | (none) | per-user | Foundry quota preflight needs the resource group. | Not secret. |
AZURE_AUTH_MODE | string | (unset) | per-user | Switching between key-based and identity-based auth. Common values: managed-identity, service-principal, cli. | Not secret. |
Server ports and network
Set these only if the defaults collide with something else on your machine, or when you need to harden the dashboard bridge with an auth token.
| Variable | Type | Default | Scope | Set when | Security |
|---|---|---|---|---|---|
PLAN_FORGE_HTTP_PORT | number | 3100 | per-machine | Port 3100 is taken by another service. | Not secret. |
PLAN_FORGE_WS_PORT | number | 3101 | per-machine | Port 3101 is taken (the WebSocket hub). | Not secret. |
PFORGE_DASHBOARD_PORT | number | 3100 | per-machine | The CLI needs to open the dashboard on a non-default port (read by pforge open-dashboard). | Not secret. |
PFORGE_DASHBOARD_URL | string (URL) | http://127.0.0.1:3100/dashboard | per-machine | The screenshot capture script needs to point at a remote dashboard. | Not secret. |
PFORGE_BRIDGE_SECRET 🔒 | string | (none) | per-machine | You want to require authentication on the MCP bridge endpoints (recommended on multi-user hosts). | Secret. Use 32+ random bytes. |
PFORGE_AUTH_TOKEN 🔒 | string | (none) | per-machine | You want to require a bearer token on the REST API (see MCP Server Reference — REST API). | Secret. Use 32+ random bytes. |
Project and runtime
Mostly read internally. PLAN_FORGE_PROJECT is the only one you might set yourself, and almost always only in tests.
| Variable | Type | Default | Scope | Set when | Security |
|---|---|---|---|---|---|
PLAN_FORGE_PROJECT | string (path) | process.cwd() | per-session | Pointing the orchestrator at a project directory other than the working directory (mostly tests). | Not secret. |
PFORGE_ENV | string | "dev" | per-machine | You want LiveGuard and the run journal to tag runs with an env label other than dev. | Not secret. |
PFORGE_LOG_LEVEL | enum | (unset = info) | per-session | Debugging, set to debug to surface cost-service tracing and other diagnostic logs. | Not secret. |
PFORGE_NO_UPDATE_CHECK | boolean (1/0) | (unset) | per-machine | CI environment where reaching out to GitHub for an update check is unwanted. | Not secret. |
Orchestrator timing
Tune these only when defaults are biting, usually on slow CI runners or when a long-running gate (large vitest suite, integration test, browser test) hits the wall.
| Variable | Type | Default | Scope | Set when | Security |
|---|---|---|---|---|---|
PFORGE_GATE_TIMEOUT_MS | number (ms) | (see orchestrator.mjs) | per-session | A gate's test suite takes longer than the default to run. | Not secret. |
PFORGE_WORKER_TIMEOUT_MS | number (ms) | (see orchestrator.mjs) | per-session | A worker (slice executor) needs more wall-clock than the default. | Not secret. |
PFORGE_WORKER_OUTPUT_IDLE_MS | number (ms) | (see orchestrator.mjs) | per-session | A worker is legitimately silent for long stretches (large builds) but should not be killed. | Not secret. |
PFORGE_BASH_PATH | string (path) | (auto-detected) | per-machine | Windows host with bash in a non-standard location. Plan Forge cannot find bash.exe on PATH and the auto-detection fails. | Not secret. |
Feature toggles
Opt-in switches for experimental subsystems and bypasses for hardening rails. Most users never touch these.
| Variable | Type | Default | Scope | Set when | Security |
|---|---|---|---|---|---|
PFORGE_DISABLE_TEMPERING | boolean (1/0) | (unset) | per-session | You need to bypass Tempering scans for one run (e.g. running an audit-loop slice that would scan its own scaffolding). | Not secret. Use sparingly, this disables quality scans. |
PFORGE_FOUNDRY_QUOTA_PREFLIGHT | boolean (1/0) | (unset) | per-machine | You want the orchestrator to check Foundry quota before dispatching slices to an Azure OpenAI deployment. | Not secret. |
PFORGE_GATE_LINT_STRICT | boolean (1/0) | 0 | per-session | You want gate-lint findings to be hard failures rather than warnings. | Not secret. |
PFORGE_DRAIN_ON_INIT | boolean (true/false) | true | per-machine | You do not want the MCP server to drain the Tempering queue on startup (CI runners that start and stop the server many times). | Not secret. |
PFORGE_ALLOW_MASTER_COMMIT | boolean (1/0) | (unset) | per-session | You explicitly want to allow a commit on master while a run-plan is active (PreCommit hook normally blocks this). | Not secret. Discouraged, the guard exists for a reason. |
PFORGE_NETWORK_LOG_ONLY | boolean (1/0) | 1 | per-session | network.allowed is set and you want the in-process proxy to stay in log-only mode. When 1, the proxy records contacted hostnames but does not block connections. | Not secret. Default-on while allowlist enforcement remains advisory. |
PFORGE_COST_MODEL | string | (auto-detected) | per-session | You want to pin slice pricing to a specific model (subscription mode, e.g. flat Copilot pricing, or a non-default vendor). | Not secret. |
Telemetry (OpenTelemetry)
Standard OTel variables. When OTEL_EXPORTER_OTLP_ENDPOINT is set, the MCP server auto-enables tracing and ships spans to the configured collector. See Compliance & Data Residency — Observability Export for the full collector setup.
| Variable | Type | Default | Scope | Set when | Security |
|---|---|---|---|---|---|
OTEL_ENABLED | boolean (true/1) | (unset) | per-machine | You want to force-enable OTel even without an endpoint (useful for local console exporter). | Not secret. |
OTEL_EXPORTER_OTLP_ENDPOINT | string (URL) | (unset) | per-machine | You want spans shipped to an OTLP collector. Setting this implicitly turns OTel on. | Not secret if the collector is internal; treat as secret if the URL embeds a token. |
OTEL_SERVICE_NAME | string | "plan-forge-mcp" | per-machine | You run multiple Plan Forge instances and need distinct service names in your APM. | Not secret. |
Host detection (read-only)
Plan Forge reads these to figure out which IDE or agent CLI is hosting it, so the orchestrator can pick the right routing default and the right model surface. You should never set these yourself, they are populated automatically by the host. Listed here for transparency only.
| Variable | Type | Default | Source | What it tells Plan Forge | Security |
|---|---|---|---|---|---|
NODE_ENV | enum | (unset) | Node.js convention | test short-circuits hub init and notifications side-effects; production tightens logging. | Not secret. |
VSCODE_PID | number (PID) | (set by VS Code) | VS Code | Plan Forge is running inside VS Code. | Not secret. |
VSCODE_AGENT_MODE | string | (set by VS Code) | VS Code Agent Mode | enterprise means VS Code Agents Enterprise, Plan Forge picks a different default model route. | Not secret. |
TERM_PROGRAM | string | (set by terminal) | Terminal | vscode or cursor trigger host-specific routing. | Not secret. |
CLAUDECODE | string | (set by Claude Code) | Claude Code CLI | 1 means Plan Forge is running under Claude Code. | Not secret. |
CLAUDE_CODE_ENTRYPOINT | string | (set by Claude Code) | Claude Code CLI | Alternate signal for Claude Code detection. | Not secret. |
CURSOR_TRACE_ID | string | (set by Cursor) | Cursor | Plan Forge is running under Cursor; cross-checked with TERM_PROGRAM=cursor. | Not secret. |
ZED_TERM | string | (set by Zed) | Zed editor | Plan Forge is running under Zed. | Not secret. |
CLI internal (set transiently by pforge)
These variables are set by the CLI or the orchestrator for the duration of one invocation and then unset. Do not set them in your shell profile, they are documented for transparency and for users writing extensions.
| Variable | Type | Set by | Read by | Purpose | Security |
|---|---|---|---|---|---|
PFORGE_CHILD_MODE | boolean (1/0) | MCP server when it spawns a child Node process | server.mjs | Suppresses double-binding of HTTP/WS ports in the child. | Not secret. |
PFORGE_RUN_PLAN_ACTIVE | boolean (1/0) | pforge run-plan | PreCommit hook | Tells the master-branch commit guard that the commit is part of an authorised run. | Not secret. |
PFORGE_QUORUM_TURN | boolean (1/0) | Orchestrator during quorum dispatch | PreAgentHandoff hook | Skips LiveGuard context injection during quorum fan-out (one of the documented v3.5+ PreAgentHandoff bypasses). | Not secret. |
ORG_RULES_FORMAT · ORG_RULES_OUTPUT | string | pforge org-rules | orchestrator | Tells forge_org_rules what format and output path to use. | Not secret. |
FORGE_SMOKE | boolean (1/0) | CI smoke-test job | vitest skipIf gate | Enables long-running smoke tests (default off in CI). | Not secret. |
Resolution precedence
When a setting has multiple sources, the orchestrator resolves in this order, first one that yields a non-empty value wins:
- CLI flag on the running command (e.g.
--quorum=power). - Plan front-matter directive in the plan being executed (e.g.
Model: claude-opus-4.7). - Environment variable set in the shell or by the host (most of this appendix).
.forge/secrets.jsonfor the keys listed in the Provider API Keys table..forge.jsonfor the keys listed in Appendix T.- Built-in default baked into
orchestrator.mjsorcapabilities.mjs.
One concrete example. OPENAI_API_KEY is resolved by checking process.env first, then falling back to .forge/secrets.json#OPENAI_API_KEY. The dashboard's Config → Secrets tab writes the latter; CI runners typically rely on the former. Both work; the env-var wins when both are set.
Worked example
A representative shell setup for a developer on Windows running a hybrid Azure-OpenAI / Anthropic stack with the dashboard on a non-default port:
# Provider keys (better: store in .forge/secrets.json)
$env:ANTHROPIC_API_KEY = "sk-ant-..."
$env:OPENAI_API_KEY = "sk-..."
# Azure OpenAI alternative routing
$env:AZURE_OPENAI_ENDPOINT = "https://contoso-aoai.openai.azure.com/"
$env:AZURE_OPENAI_API_VERSION = "2024-02-01"
$env:AZURE_OPENAI_DEPLOYMENT_TYPE = "global"
$env:AZURE_AUTH_MODE = "managed-identity"
# Non-default ports (3100/3101 conflicted with another local service)
$env:PLAN_FORGE_HTTP_PORT = "3110"
$env:PLAN_FORGE_WS_PORT = "3111"
$env:PFORGE_DASHBOARD_PORT = "3110"
# Tracing to a local OTel collector
$env:OTEL_EXPORTER_OTLP_ENDPOINT = "http://127.0.0.1:4318"
$env:OTEL_SERVICE_NAME = "plan-forge-mcp-dev"
# Lift gate timeout for a long-running integration suite
$env:PFORGE_GATE_TIMEOUT_MS = "600000" # 10 minutes
See also
- Appendix T —
.forge.jsonReference, the per-project config file that pairs with this appendix. - Customization — Configuration Hierarchy, how
.forge.json, env vars, and instruction files interact when settings conflict. - MCP Server Reference — REST API, the dashboard's HTTP surface, including the bearer-token auth flow that
PFORGE_AUTH_TOKENgates. - pforge-mcp/secrets.mjs, the loader for
.forge/secrets.jsonand the canonical source for the provider-key whitelist. - Troubleshooting — Common Errors, recovery hints for
ERR_NO_API_KEY,ERR_TESTBED_NOT_FOUND, and the gate-timeout family.