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:
- Project
.env– Located in the current working directory whereompis executed (process.cwd() + "/.env") - Agent
.env– Located in the tool's own data directory (getAgentDir() + "/.env") - Global π
.env– Located in the shared configuration directory (getConfigRootDir() + "/.env") - User-home
.env– Located in the user's home directory (os.homedir() + "/.env") - Existing process environment – Variables already present in
Bun.envbefore 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
.envfiles hold the highest priority, overriding agent, global, and home configurations due to the[projectEnv, agentEnv, piEnv, homeEnv]merge order inpackages/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 anyOMP_variables (lines 78-83). - Process environment variables serve as the lowest priority layer, only populating
Bun.envif not defined in any.envfile (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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →