Skip to content

Art skill: loadEnv() misses canonical ~/.claude/.env when LIFEOS_DIR points at a subdirectory (silent, no API keys load) #1515

Description

@xmasyx

Summary

In the Art skill, loadEnv() in LifeOS/install/skills/Art/Tools/Generate.ts looks for .env at ${LIFEOS_DIR}/.env. When LIFEOS_DIR is set to the LIFEOS subdirectory ($HOME/.claude/LIFEOS) — which is how many installs configure it — it resolves to $HOME/.claude/LIFEOS/.env, a file that does not exist. The canonical .env lives one level up at $HOME/.claude/.env.

The catch block silently swallows the resulting ENOENT, so no keys are loaded and generation fails with:

❌ Error: Missing environment variable: GOOGLE_API_KEY

…even though the key is present in the real .env. The fallback to $HOME/.claude/.env only fires when LIFEOS_DIR is unset, not when it points at a subdirectory that has no .env.

Location

LifeOS/install/skills/Art/Tools/Generate.ts, loadEnv() (around lines 40–42):

const paiDir = process.env.LIFEOS_DIR || resolve(process.env.HOME!, '.claude');
const envPath = resolve(paiDir, '.env');

Repro

  1. export LIFEOS_DIR="$HOME/.claude/LIFEOS" (a subdirectory, as configured on many installs).
  2. Put GOOGLE_API_KEY=... in $HOME/.claude/.env (the canonical location).
  3. Run the Art generator with a Google model, e.g. --model nano-banana-pro.

Result: Missing environment variable: GOOGLE_API_KEY, because loadEnv() read $HOME/.claude/LIFEOS/.env (absent) and silently gave up.

Root cause

loadEnv() assumes the .env sits inside LIFEOS_DIR, but the canonical .env is at $HOME/.claude/.env — the parent of the LIFEOS subdirectory. A single hard-coded path plus a silent catch means a present, valid key is invisible to the tool.

Suggested fix

Try multiple candidate paths and load the first that exists (canonical $HOME/.claude/.env included), instead of a single LIFEOS_DIR-relative path:

async function loadEnv(): Promise<void> {
  const home = process.env.HOME!;
  // The canonical .env is at ~/.claude/.env; LIFEOS_DIR may point at the
  // ~/.claude/LIFEOS subdir, so try both. Load the first candidate that exists.
  const candidates = Array.from(new Set(
    [
      process.env.LIFEOS_DIR ? resolve(process.env.LIFEOS_DIR, '.env') : null,
      resolve(home, '.claude', '.env'),
    ].filter((p): p is string => Boolean(p)),
  ));

  for (const envPath of candidates) {
    let envContent: string;
    try {
      envContent = await readFile(envPath, 'utf-8');
    } catch {
      continue; // not here — try the next candidate
    }
    for (const line of envContent.split('\n')) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('#')) continue;
      const eqIndex = trimmed.indexOf('=');
      if (eqIndex === -1) continue;
      const key = trimmed.slice(0, eqIndex).trim();
      let value = trimmed.slice(eqIndex + 1).trim();
      if (
        (value.startsWith('"') && value.endsWith('"')) ||
        (value.startsWith("'") && value.endsWith("'"))
      ) {
        value = value.slice(1, -1);
      }
      if (!process.env[key]) process.env[key] = value; // shell still wins
    }
  }
  // …existing alias handling unchanged…
}

This keeps the LIFEOS_DIR-relative path working, adds the canonical $HOME/.claude/.env as a fallback, and preserves the "shell env overrides file" semantics.

Note

The same LIFEOS_DIR-relative .env assumption likely recurs in sibling tools that ship their own loadEnv() (e.g. GenerateMidjourneyImage.ts); worth auditing them together. A shared helper would prevent drift.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions