slopus/happy · warning
[EXPAND ENV] Session may fail to authenticate. Set these in
Error message
[EXPAND ENV] Session may fail to authenticate. Set these in daemon environment before launching:
What it means
This is a warning (not an exception) emitted by expandEnvironmentVariables in packages/happy-cli/src/utils/expandEnvVars.ts:89. The function expands ${VAR} references in profile environment variables against the daemon's environment (process.env by default). When a referenced variable is not defined in the source environment and no ${VAR:-default} fallback was provided, the raw ${VAR} placeholder is left in the value, and this warning tells the developer the spawned session will likely fail to authenticate because the resulting env var contains an unexpanded placeholder instead of a real credential.
Source
Thrown at packages/happy-cli/src/utils/expandEnvVars.ts:89
return resolvedValue;
} else if (defaultValue !== undefined) {
// Variable not found but default value provided - use default
logger.debug(`[EXPAND ENV] Using default value for ${varName}: ${defaultValue}`);
return defaultValue;
} else {
// Variable not found and no default - keep placeholder and warn
undefinedVars.push(varName);
return match;
}
});
expanded[key] = expandedValue;
}
// Log warning if any variables couldn't be resolved
if (undefinedVars.length > 0) {
logger.warn(`[EXPAND ENV] Undefined variables referenced in profile environment: ${undefinedVars.join(', ')}`);
logger.warn(`[EXPAND ENV] Session may fail to authenticate. Set these in daemon environment before launching:`);
undefinedVars.forEach(varName => {
logger.warn(`[EXPAND ENV] ${varName}=<your-value>`);
});
}
return expanded;
}
View on GitHub (pinned to b824cd0a46)
Solutions
- Export the missing variable in the shell that starts the daemon (e.g. `export Z_AI_AUTH_TOKEN=sk-...`) and restart the daemon with `./bin/happy.mjs daemon stop && ./bin/happy.mjs daemon start` so it inherits the value in process.env.
- Add a fallback default in the profile using bash-style expansion: `${Z_AI_AUTH_TOKEN:-<fallback>}` so the placeholder always resolves.
- Fix typos in the profile's ${VAR} references so they match the actual env var names in the daemon environment.
- Verify which variables failed by reading the '[EXPAND ENV] Undefined variables referenced in profile environment:' log line in the daemon logs under ~/.happy-dev/logs/.
- If the variable is intentionally optional, restructure the profile so the dependent key (e.g. ANTHROPIC_AUTH_TOKEN) is only set when the source variable exists.
Example fix
// before (profile env referencing unset var)
const profileEnv = { ANTHROPIC_AUTH_TOKEN: "${Z_AI_AUTH_TOKEN}" };
// daemon launched without: export Z_AI_AUTH_TOKEN=sk-real-key
// after (either export before daemon start, or provide a default)
const profileEnv = { ANTHROPIC_AUTH_TOKEN: "${Z_AI_AUTH_TOKEN:-sk-fallback-key}" };
// or in the daemon-launching shell:
// export Z_AI_AUTH_TOKEN=sk-real-key && happy daemon start Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight check: detect unresolvable ${VAR} refs before calling expandEnvironmentVariables
function findUndefinedEnvVars(envVars: Record<string, string>, sourceEnv: NodeJS.ProcessEnv = process.env): string[] {
const missing: string[] = [];
for (const value of Object.values(envVars)) {
for (const match of value.matchAll(/\$\{([^}]+)\}/g)) {
const expr = match[1];
const hasDefault = expr.includes(':-');
const varName = hasDefault ? expr.slice(0, expr.indexOf(':-')) : expr;
if (!hasDefault && sourceEnv[varName] === undefined) {
missing.push(varName);
}
}
}
return [...new Set(missing)];
}
const missing = findUndefinedEnvVars(profileEnv);
if (missing.length > 0) {
throw new Error(`Refusing to spawn session; missing env vars: ${missing.join(', ')}. Export them before starting the daemon.`);
}
const expanded = expandEnvironmentVariables(profileEnv); Type guard
// Narrow values to those with no unexpanded placeholder left after expansion
function isFullyExpanded(value: string): boolean {
return !/\$\{[^}]+\}/.test(value);
}
function allEnvVarsExpanded(env: Record<string, string>): boolean {
return Object.entries(env).every(([k, v]) => isFullyExpanded(v) || console.warn(`env var ${k} still contains unexpanded placeholder: ${v}`) === undefined);
} Try / catch
// expandEnvironmentVariables does not throw; it warns and returns placeholders.
// Guard the result instead — verify no placeholders leaked before spawning:
try {
const expanded = expandEnvironmentVariables(profileEnv, process.env);
const unresolved = Object.entries(expanded).filter(([, v]) => /\$\{[^}]+\}/.test(v));
if (unresolved.length > 0) {
throw new Error(`Unresolved env placeholders: ${unresolved.map(([k]) => k).join(', ')}`);
}
await spawnSession(expanded);
} catch (err) {
logger.error(`Session not launched: ${err instanceof Error ? err.message : String(err)}`);
// abort instead of launching a session that cannot authenticate
} Prevention
- Export required secrets in the same shell (or systemd unit / launchd plist) that starts the daemon, since the daemon captures process.env at launch time.
- Prefer ${VAR:-default} syntax in profiles for any variable that has a sensible fallback.
- Check daemon logs (~/.happy-dev/logs/) for '[EXPAND ENV] WARNING' lines after every profile change.
- Avoid variable name typos by defining profile env vars in one constants file and referencing them consistently.
- After changing ~/.bashrc or shell profiles, fully stop and restart the daemon (`happy daemon stop && happy daemon start`) — running daemons do not pick up new env.
- For non-tmux mode, remember shell expansion never happens, so every ${VAR} must be resolvable by the daemon's environment or carry a default.
When it happens
Trigger: Calling expandEnvironmentVariables (directly or via spawnSession/result) with a profile env value containing ${VAR} where VAR is absent from sourceEnv (usually the daemon's process.env) and no ':-' default is specified. E.g. profile has ANTHROPIC_AUTH_TOKEN: "${Z_AI_AUTH_TOKEN}" but the daemon was launched without Z_AI_AUTH_TOKEN exported.
Common situations: 1) Daemon started via `happy daemon start` in a shell that didn't export the API token (exported only in ~/.bashrc of an interactive shell, or set after daemon launch). 2) Typos in variable names inside profile config (e.g. ${Z_AI_TOKEN} vs ${Z_AI_AUTH_TOKEN}). 3) Moving profiles between machines where the secret env var was never set. 4) Non-tmux mode where Node spawn does not expand ${VAR} and the manual expansion is the only chance to resolve it. 5) Variable set to empty string (related but separate warning at line 68).
Understand the failure class
Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Session environment is invalid - environment variables not f
- Daemon-spawned sessions cannot use local/interactive mode. U
- [EXPAND ENV] WARNING: ${varName} is set but EMPTY in daemon
- No machine ID found in settings
- Unknown agent: ${id}. Available agents: ${available}
AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31).
Data as JSON: /api/errors/96c56c80d337c404.
Report an issue: GitHub.