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

  1. 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.
  2. Add a fallback default in the profile using bash-style expansion: `${Z_AI_AUTH_TOKEN:-<fallback>}` so the placeholder always resolves.
  3. Fix typos in the profile's ${VAR} references so they match the actual env var names in the daemon environment.
  4. 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/.
  5. 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

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.

Related errors


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/96c56c80d337c404. Report an issue: GitHub.