Environment Variable Precedence in Oh-My-Pi (omp): The Complete Resolution Order

Oh-My-Pi (omp) resolves environment variables using a strict five-level precedence hierarchy where project-level .env files override agent, global, and home directory configurations, with existing process environment variables serving as the final fallback.

The omp (Oh-My-Pi) toolchain normalizes environment variable resolution through a centralized loading system implemented in the @oh-my-pi/pi-utils package. Understanding the precedence order for resolving environment variables in omp is critical for debugging configuration conflicts across multi-directory setups. The resolution logic, found in packages/utils/src/env.ts, employs a "first-source-wins" merging strategy that prioritizes local project definitions over global settings.

The Five-Level Precedence Hierarchy

When the env module initializes, it loads and merges environment variables from four distinct .env file locations in a specific sequence. The effective precedence from highest to lowest priority is:

  1. Project .env – Located in the current working directory where omp is executed (process.cwd() + "/.env")
  2. Agent .env – Located in the tool's own data directory (getAgentDir() + "/.env")
  3. Global π .env – Located in the shared configuration directory (getConfigRootDir() + "/.env")
  4. User-home .env – Located in the user's home directory (os.homedir() + "/.env")
  5. Existing process environment – Variables already present in Bun.env before the module runs

Sources at the top of this list take precedence over those below. If a variable is defined in both the project .env and the home directory .env, the project value wins because it is merged first.

How the Resolution Logic Works

The core resolution mechanism operates in packages/utils/src/env.ts through a synchronous loading phase followed by a conditional merge phase.

First, the system loads all four .env files simultaneously (lines 88-93):

// Synchronous loading from four locations
const projectEnv = parseEnvFile(process.cwd() + "/.env");
const agentEnv = parseEnvFile(getAgentDir() + "/.env");
const piEnv = parseEnvFile(getConfigRootDir() + "/.env");
const homeEnv = parseEnvFile(os.homedir() + "/.env");

Next, the merging algorithm processes these sources in a fixed array order (lines 101-107):

// Merging loop: first source wins
for (const env of [projectEnv, agentEnv, piEnv, homeEnv]) {
  for (const [key, value] of Object.entries(env)) {
    if (!(key in Bun.env)) {
      Bun.env[key] = value;
    }
  }
}

The critical logic is the condition if (!(key in Bun.env)). This check ensures that once a variable is set by a higher-priority source, lower-priority sources cannot overwrite it. Consequently, the array order [projectEnv, agentEnv, piEnv, homeEnv] directly determines the precedence hierarchy.

Safety Filtering and Variable Aliasing

Before merging occurs, the module applies security filtering and namespace aliasing rules.

Safety filtering (lines 33-40 and 94-99) validates environment variable names and values, stripping any entries with invalid characters or dangerous patterns before they enter the Bun.env object. This prevents injection attacks through malformed .env files.

OMP_ to PI_ aliasing (lines 78-83) automatically duplicates variables for backward compatibility. After parsing any .env file, if a key starts with OMP_, the system creates an additional entry with the PI_ prefix. For example, OMP_API_TOKEN becomes accessible as both OMP_API_TOKEN and PI_API_TOKEN.

Finally, the module re-exports Bun.env as $env (lines 115-117), making the fully resolved environment available to the rest of the application.

Accessing Resolved Variables in Code

Once the precedence resolution completes, you can access variables through the @oh-my-pi/pi-utils package:

// Import triggers the loading/merging logic immediately
import { $env, $pickenv, $flag } from "@oh-my-pi/pi-utils";

// Direct access reflects the final merged environment
console.log("API token:", $env["OMP_API_TOKEN"]);   // Also accessible as $env["PI_API_TOKEN"]

// Resolve the first defined value among alternatives
const apiKey = $pickenv("OMP_API_TOKEN", "PI_API_TOKEN", "MY_CUSTOM_TOKEN");
if (!apiKey) throw new Error("No API token found in any scope");

// Check boolean flags respecting the same precedence
if ($flag("OMP_DEBUG")) {
  console.log("Debug mode enabled via highest-precedence source");
}

The $pickenv helper searches through the provided keys in order, while $flag normalizes truthy values ("1", "true", "yes") to boolean true according to the resolved precedence hierarchy.

Summary

  • Project .env files hold the highest priority, overriding agent, global, and home configurations due to the [projectEnv, agentEnv, piEnv, homeEnv] merge order in packages/utils/src/env.ts (lines 101-107).
  • First-source-wins logic prevents lower-priority sources from overwriting variables already set by higher-priority sources.
  • Safety filtering occurs before merging (lines 33-40), stripping invalid entries to prevent environment pollution.
  • Automatic aliasing creates PI_ prefixed copies of any OMP_ variables (lines 78-83).
  • Process environment variables serve as the lowest priority layer, only populating Bun.env if not defined in any .env file (lines 94-99).

Frequently Asked Questions

What happens if the same variable is defined in both project and home .env files?

The project .env value takes precedence. According to the merge loop in packages/utils/src/env.ts (lines 101-107), project environment variables are processed first, so when the home directory .env is merged later, the condition if (!(key in Bun.env)) evaluates to false, and the home directory value is ignored.

How does omp handle environment variables prefixed with OMP_ versus PI_?

After parsing any .env file, omp automatically creates aliases for compatibility. Any key starting with OMP_ is duplicated with a PI_ prefix (lines 78-83 in env.ts). This means defining OMP_API_KEY in your project .env makes it accessible via both $env["OMP_API_KEY"] and $env["PI_API_KEY"], with both references pointing to the same resolved value.

Can existing system environment variables override .env file configurations?

No. Existing process environment variables (already present in Bun.env when the module loads) are filtered through a safety check (lines 94-99) but are not overwritten by .env file contents. However, if a variable is not defined in the process environment but exists in multiple .env files, the project-level definition wins over agent, global, and home definitions.

Where does omp look for .env files by default?

Omp searches four specific locations in order of precedence: the current working directory (process.cwd()), the agent data directory (getAgentDir()), the global configuration root (getConfigRootDir()), and the user's home directory (os.homedir()). These paths are resolved in packages/utils/src/env.ts (lines 88-93) using helper functions from packages/utils/src/dirs.ts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →