feat(config): add user-scope .env file support#112
Conversation
donk8r
commented
Jul 19, 2026
- Load .env from shared config directory before project-local .env
- Establish precedence: system < user-scope < project-local
- Document new loading order in environment and provider guides
- Remove Groq provider from documentation table
- Load .env from shared config directory before project-local .env - Establish precedence: system < user-scope < project-local - Document new loading order in environment and provider guides - Remove Groq provider from documentation table
Octomind — developer:brief (octohub:glm)📦 Brief: Add user-scope
|
| # | Change | Risk | Confidence |
|---|---|---|---|
| 1 | .env loading now reads a user-scope file from <config_dir>/.env before the project-local .env, establishing a three-tier precedence |
🟡 | ●●● |
Card 1/1: User-scope `.env` file loaded before project-local `.env` · 🟡 · ●●●
INTENT
Allow users to set API keys once in a shared config-directory .env that applies across all projects, with project-local .env still winning on conflicts. Commit message: "feat(config): add user-scope .env file support."
WHAT CHANGED
load_dotenv_override()now loads<config_dir>/.env(user-scope) before./.env(project-local), both overriding system environment variables. Later loads win, preserving the precedence: system < user-scope < project-local.get_config_dir()is called to resolve the shared config directory; the.envfile is only loaded if it exists on disk.dotenv_loadedflag is set totrueif either file loads successfully — source tracking (get_source) remains binary (System vs DotEnv) and cannot distinguish which.envfile a variable came from.- Documentation updated in two guides to describe the new three-tier load order; Groq provider removed from the provider compatibility table.
IMPACT RADIUS
- Called from
main.rsat startup (line 89–92) — the single entry point for all CLI, ACP, and WebSocket modes. Every launch path now attempts the user-scope load. get_source()inenv_source.rsis used byconfig --showand API-key source detection. It still works correctly — both.envfiles are categorized asDotEnv, and the pre-dotenv snapshot comparison logic is unchanged.
RISK
🟡 MEDIUM. A parse error in the user-scope .env aborts loading before the project-local .env is attempted. The ? operator on dotenvy::from_filename_override(&user_env) propagates the error immediately, skipping the project-local load entirely. In main.rs the error is caught and printed as a warning, but the project-local .env — which may contain the keys the user actually needs — is silently never loaded. A malformed shared .env would thus break all project-local key overrides with only a warning.
// User-scope error here → project-local .env never reached
dotenvy::from_filename_override(&user_env)?;QUESTIONS
- Should a user-scope
.envparse failure fall through to the project-local.env(e.g., log + continue) rather than aborting the entire load chain? The current behavior means a bad shared file silently disables project-local overrides.
DIVERGENCE
🧩 Incomplete change — get_source() returns EnvSource::DotEnv for variables from either .env file, but the new documentation describes two distinct sources. The source tracker cannot tell the user which .env a key came from, which may cause confusion when debugging precedence conflicts.
📎 Source
src/config/env_source.rs#L60-L89—load_dotenv_override: two-stage .env loading with early-return on user-scope errorsrc/main.rs#L89-L97— startup call site; error printed as warning, project-local load skipped on user-scope failuresrc/config/env_source.rs#L92-L124—get_source: binary System/DotEnv classification, no per-file distinctionsrc/directories.rs#L61-L70—get_config_dir: resolves shared config directory, creates it if missing
📂 Files changed (3 files, ~34 lines)
src/config/env_source.rs— core logic: two-stage.envloading with user-scope precedencedoc/reference/04-environment-variables.md— reference doc: new load order and precedence rulesdoc/usage/04-providers.md— usage doc: user-scope.envexplanation, Groq removed from provider table
{tokens:201207,cost:0.28912679999999996}
- Implement RFC 8628 device flow for CLI authentication - Add polling mechanism to retrieve API keys after browser confirmation - Implement secure storage of API keys in user config .env file - Add support for custom API and panel URLs via environment variables - Implement login command for Octomind Cloud - Add URL repointing logic for local callback handling - Add environment variable upsert utility for API keys - Integrate login command into main CLI dispatcher
Octomind — developer:brief (octohub:glm)📦 Brief: New
|
| # | Change | Risk | Confidence |
|---|---|---|---|
| 1 | CLI login subcommand: starts device-auth, polls for token, upserts OCTOHUB_API_KEY into <config_dir>/.env with 0o600 perms |
🟢 | ●●● |
Card 1/1: Device-authorization login flow stores API key in user-scope .env · 🟢 · ●●●
INTENT
Enable octomind login as a first-class CLI subcommand so users can authenticate against Octomind Cloud without manually copying API keys — the same device-code pattern gh auth login uses (RFC 8628).
WHAT CHANGED
- New
Loginsubcommand registered in the CLI (Commands::Login) and dispatched inrun_with_cleanup; takes--no-browserflag for headless/SSH environments. execute()callsPOST /api/v1/auth/clito start a device flow, displays a user code + verification URL, opens the browser (or prints the URL), then pollsPOST /api/v1/auth/cli/tokenat the server-supplied interval until the key arrives, the code expires, or a 15-minute deadline passes.- On success, the API key is upserted into
<config_dir>/.envasOCTOHUB_API_KEY, replacing any prior value (includingexport-prefixed lines) while preserving all other variables. File permissions are set to 0o600 on Unix. - The key is also set in the current process's environment via
std::env::set_varso subsequent commands in the same shell invocation work immediately. - API responses use a
(Option<String>, Option<Value>)envelope — the first element is an error code string, the second is the data payload. Non-2xx responses carry the same envelope, so HTTP status is never inspected directly. OCTOMIND_API_URLandOCTOMIND_PANEL_URLenv vars allow overriding the API and browser-confirmation endpoints for local development.
IMPACT RADIUS
- The stored key is consumed by the existing startup path:
load_dotenv_override()insrc/config/env_source.rsloads<config_dir>/.envfirst, then cwd.envoverrides it. Every octomind process already reads this file, so no consumer changes were needed. - The
block_open/block_row/block_close_ok/key_widthdisplay functions are reused fromsrc/session/chat/tool_display.rs— same rendering path as/helpand other CLI commands.
RISK
🟢 LOW. The std::fs::read_to_string(&path).unwrap_or_default() in write_key silently returns an empty string on any read error — including permission-denied on an existing file — which would then be overwritten by the write. This conflicts with the project's own "no unwrap_or_else(|_| default) that swallows real errors" guideline. In practice the risk is minimal: if the user can't read the file they almost certainly can't write it either, so the write would fail with a visible error. The polling loop correctly checks the deadline before each sleep, and the interval is clamped to 1–30s.
DIVERGENCE
🧩 Incomplete change — OCTOHUB_API_KEY is written and set in the environment, but no code in the codebase reads it yet (grep finds only the login module itself). The key presumably flows through octolib or the provider layer at runtime via environment-variable consumption, but this can't be verified within this repo. The login command is functional as a credential-acquisition step regardless.
📎 Source
src/commands/login.rs#L103-L178—execute(): device-auth start, browser open, polling loop, key storagesrc/commands/login.rs#L183-L212—write_key+upsert: .env file write with 0o600, key replacement logicsrc/commands/login.rs#L73-L93—call(): kisscore envelope deserialization, no HTTP status checksrc/config/env_source.rs#L60-L76—load_dotenv_override(): confirms<config_dir>/.envis loaded at startupsrc/main.rs#L166-L170—Logindispatch inrun_with_cleanup
📂 Files changed (3 files, ~263 lines)
src/commands/login.rs— New file: full device-auth login implementation, .env upsert, unit testssrc/commands/mod.rs— Module declaration and re-export ofLoginArgssrc/main.rs—Loginvariant added toCommandsenum and dispatch match
{tokens:148641,cost:0.2216574}
- Add account client for control-plane API authentication - Implement session persistence and automatic JWT refresh logic - Add /usage command to CLI and chat to track spend and quotas - Integrate visual progress bars for budget, storage, and network usage - Add --force flag to login for re-authentication - Display account identity, plan, and sign-in state in config and login - Centralize session management and environment variable handling - Add error handling for unsigned-in states and API failures
- Add machine_id to persist a unique identifier per device - Send device_id during login to prevent cross-machine key collisions - Display server-assigned key name in login output - Clean up formatting in agent command list
Octomind — developer:brief (octohub:glm)I have all the context I need. Let me compile the brief. 📦 Brief: Account authentication, session persistence, and usage tracking added to the CLIOverall risk: 🟡 MEDIUM · Cards: 3
Card 1/3: Account client — panel session persistence, JWT auto-refresh, and machine-specific device IDs · 🟡 MEDIUM · ●●●INTENT WHAT CHANGED
IMPACT RADIUS
RISK The DIVERGENCE 📎 Source
Card 2/3: /usage command — account spend and quota display with visual bars · 🟢 LOW · ●●●INTENT WHAT CHANGED
IMPACT RADIUS
RISK 📎 Source
Card 3/3: Config display shows Octomind Cloud sign-in state · 🟢 LOW · ●●●INTENT WHAT CHANGED
IMPACT RADIUS
RISK 📎 Source
📂 Files changed (11 files, ~573 lines)
{tokens:263591,cost:0.3902644} |
Octomind — developer:brief (octohub:glm)📦 Brief: Remove unused
|
| # | Change | Risk | Confidence |
|---|---|---|---|
| 1 | Dead-code removal of account::clear_session() — no callers anywhere in the crate |
🟢 | ●●● |
Card 1/1: Dead-code removal of unused session-clearing function · 🟢 · ●●●
INTENT
Remove clear_session() from src/account.rs — it was defined but never called anywhere in the codebase.
WHAT CHANGED
account::clear_session()deleted. It deleted the session file from disk if it existed.- No callers exist: grep across all
.rsfiles finds zero references toaccount::clear_session. Theclear_sessionsymbols insession/context.rsandsession/dedup.rsare unrelated functions in different modules.
IMPACT RADIUS
Isolated change — no downstream impact detected. The function was private to the account module's public surface but had no consumers.
RISK
🟢 LOW. No significant risk. Pure dead-code removal with no callers to break.
📎 Source
src/account.rs#L104-L114—save_sessionremains;clear_sessionwas removed from between it andclient()
📂 Files changed (1 file, ~8 lines)
src/account.rs— account/session persistence module; unusedclear_sessionremoved
{tokens:83393,cost:0.1201402}