fix: include .env file vars in gateway service environment on install#52492
fix: include .env file vars in gateway service environment on install#52492steipete merged 2 commits intoopenclaw:mainfrom
Conversation
When building the gateway install plan, read and parse ~/.openclaw/.env (or $OPENCLAW_STATE_DIR/.env) and merge those key-value pairs into the service environment at the lowest priority — below config env vars, auth-profile refs, and the core service environment (HOME, PATH, OPENCLAW_*). This ensures that user-defined secrets stored in .env (e.g. BRAVE_API_KEY, OPENROUTER_API_KEY, DISCORD_BOT_TOKEN) are embedded in the LaunchAgent plist (macOS), systemd unit (Linux), and Scheduled Task (Windows) at install time, rather than relying solely on the gateway process loading them via dotenv.config() at startup. Previously, on macOS the LaunchAgent plist never included .env vars, which meant: - launchctl print did not show user secrets (hard to debug) - Child processes spawned before dotenv loaded had no access - If the same key existed in both .env and the plist, the stale plist value won via dotenv override:false semantics Dangerous host env vars (NODE_OPTIONS, LD_PRELOAD, etc.) are filtered using the same security policy applied to config env vars. Fixes openclaw#37101 Relates to openclaw#22663
- Apply normalizeEnvVarKey({ portable: true }) before security
filtering, matching the established pattern in env-vars.ts.
Rejects non-portable key names (spaces, special chars) that
would produce invalid plist/systemd syntax.
- Isolate existing tests from the developer's real ~/.openclaw/.env
by providing a temp HOME directory, preventing flaky failures
when the test machine has a populated .env file.
Greptile SummaryThis PR fixes a long-standing gap where
Confidence Score: 4/5
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0b5248161f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // 3. Auth-profile env refs (credential store → env var lookups) | ||
| // 4. Service environment (HOME, PATH, OPENCLAW_* — highest) | ||
| const environment: Record<string, string | undefined> = { | ||
| ...readStateDirDotEnvVars(params.env), |
There was a problem hiding this comment.
Don't freeze state-dir .env keys into the service definition
Adding readStateDirDotEnvVars(params.env) here turns every ~/.openclaw/.env entry into an install-time snapshot. On later starts the gateway still calls loadCliDotEnv() (src/cli/run-main.ts:93), and that loader uses override: false (src/cli/dotenv.ts:12-19), so rotating or deleting a key in .env no longer updates the running service: the stale value already embedded in the plist/unit/task wins forever until the user reinstalls. Before this change, a service restart would pick up the current .env contents.
Useful? React with 👍 / 👎.
| if (isDangerousHostEnvVarName(key) || isDangerousHostEnvOverrideVarName(key)) { | ||
| continue; | ||
| } | ||
| entries[key] = value; |
There was a problem hiding this comment.
Reject multiline .env values before copying them into service env
This helper now forwards parsed .env values straight into the install plan, but quoted multiline values are valid dotenv syntax (for example PEM/private-key entries). The Linux and Windows service renderers explicitly reject CR/LF in environment values (src/daemon/systemd-unit.ts:30-34, src/daemon/cmd-set.ts:58-60), so a previously valid ~/.openclaw/.env with a multiline secret will now make openclaw gateway install fail on those platforms.
Useful? React with 👍 / 👎.
|
CI failures are unrelated to this PR:
Neither involves the files changed here. The |
|
Landed via temp rebase onto main. Thanks @Chevron7Locked! |
The isolatedHome pattern added in dd860e7 covered only 2 of 10 buildGatewayInstallPlan call sites. The remaining 8 still passed env: {} (or env without HOME), causing readStateDirDotEnvVars to fall back to os.homedir() and read the developer's real ~/.openclaw/.env — producing non-deterministic failures when that file contains keys matching test assertions. Fixes all remaining call sites to include HOME: isolatedHome, completing the isolation started in openclaw#52492.
Summary
~/.openclaw/.env(or$OPENCLAW_STATE_DIR/.env) duringgateway installand merge those key-value pairs into the service environment.envvars are merged at the lowest priority — config env vars, auth-profile refs, and core service env all override themNODE_OPTIONS,LD_PRELOAD, etc.) are filtered using the same security policy applied to config env varsProblem
On macOS, the LaunchAgent plist never included env vars from
~/.openclaw/.env. This meant:.envand the plist (from a previous install), the stale plist value won becausedotenv.config()usesoverride: falseBRAVE_API_KEY,OPENROUTER_API_KEY,DISCORD_BOT_TOKENstored in.envweren't visible tolaunchctl print(hard to debug)loadCliDotEnv()call had no access to.envvarsThe same issue exists on Linux (systemd) and Windows (Scheduled Task) — the install plan never read
.env.Fix
Added
readStateDirDotEnvVars()indaemon-install-helpers.tsthat:OPENCLAW_STATE_DIR).envusing the existingdotenvdependencyisDangerousHostEnvVarName/isDangerousHostEnvOverrideVarNameThe merge priority in
buildGatewayInstallPlanis now:Test plan
readStateDirDotEnvVars— reads key-value pairs from .envreadStateDirDotEnvVars— returns empty when .env missingreadStateDirDotEnvVars— drops dangerous vars (NODE_OPTIONS)readStateDirDotEnvVars— drops empty/whitespace valuesreadStateDirDotEnvVars— respects OPENCLAW_STATE_DIR overridebuildGatewayInstallPlan— merges .env vars into install planbuildGatewayInstallPlan— config env vars override .env varsbuildGatewayInstallPlan— service env overrides .env varsbuildGatewayInstallPlan— works when .env does not existFixes #37101
Relates to #22663