Skip to content

Environment

Each service process gets a merged environment. Later sources override earlier ones.

Default source order (ENV_SOURCE_ORDER / environment.sources):

process, defaults, vars, and runtime always run for host services. Container services deliberately omit process so the caller's whole shell is not stored in inspectable container metadata. If you set environment.sources, the listed optional sources (profile, dotenv, generated, keychain, secret_manager) are added to the always-on set.

SourceWhat it loads
processThe env of whichever CLI/TUI client most recently started or restarted this service (forwarded over the RPC as client_env), falling back to the supervisor's own environment if no client has done so yet — see below
profileprofiles.<name>.environment
dotenvRepo-root then service working-dir: .env, .env.development, .env.local, .env.<profile>
generatedBuilt-in hook that always returns {}. A plugin may register environmentSources if you need generated values
keychainNamed secrets from environment.secrets / the credential store
secret_managerValues that look like projects/*/secrets/* via the Google REST API
defaultsservices.<name>.environment.defaults
varsExplicit services.<name>.environment keys
runtimeValues devctl injects at start

keychain and secret_manager throw only when that source is listed and fetch fails.

process and the daemon-replacement limitation

The daemon remembers each service's client_env only in memory, per service, never on disk. A crash/health-triggered auto-restart or an MCP-initiated start/restart reuses the last one a real client supplied; a service that has never been started/restarted by a real client this daemon's lifetime — e.g. one adopted from a prior session by recoverSession() — has none, and falls back to the daemon's own (possibly stale) environment.

This memory does not survive the daemon process itself being replaced (upgrade, crash, devctl down then a fresh start): a new daemon starts with no client history at all, so anything it restarts before a client issues a fresh start/restart runs on whatever environment that new daemon process itself inherited at spawn. If a service depends on env that changed since the daemon last started, restart it explicitly (devctl restart <service> or the TUI) rather than relying on an automatic restart to pick it up.

Runtime-generated variables

Injected when applicable:

  • SERVICE_PORT, SERVICE_HOST
  • DEVCTL_PROXY_URL
  • DEVCTL_SERVICE_NAME
  • DEVCTL_ENVIRONMENT
  • DEVCTL_USER_EMAIL — the developer's own detected Google identity (gcloud/ADC), so a service can key on who is running it without a hardcoded, team-unfriendly value. Omitted when no identity is detected.
  • DEVCTL_TOKEN_URL and DEVCTL_INTERNAL_TOKEN for host services (never a raw access token); containers omit both because container loopback cannot reach the host loopback endpoint

References such as ${services.identity.ports.http} resolve before process start, including inside profile and dotenv values. ${identity.user} resolves to the running developer's detected email — use it to map that identity onto a service's own variable in shared config, e.g. LOCAL_USER_EMAIL: ${identity.user} (empty when no identity is detected). ${env.NAME} is rejected there. IAP route auth.client_secret is the exception: ${NAME} and ${env.NAME} are expanded from the process environment when the token is minted, not at config load.

environment.required on a service fails start if those keys are still empty after the merge.

TUI / CLI flag precedence

File plugins

plugins[].path can register additional named environment sources. Unknown configured source names fail after plugins load instead of being silently skipped. See Plugins for the SDK contract, failure behavior, and examples. The built-in generated source stays empty unless a plugin registers an environment source with that name.

Released under the MIT License.