Skip to content

CLI silently strips repo-local .env vars; logs say (0) from .env instead of stripped N keys #1302

@Wirasm

Description

@Wirasm

Summary

  • What broke: Environment variables defined in a repo-local .env file are silently unavailable to workflows run via the Archon CLI. The dotenv log output reports injecting env (0) from .env which reads like "the file was empty," but the vars actually were parsed and then deliberately deleted from process.env by stripCwdEnv(). Additionally, there is no repo-scoped archon-owned location (e.g. <repo>/.archon/.env) to put workflow-time env vars — the only working path is ~/.archon/.env, which is neither discoverable from the logs nor documented.
  • When it started (if known): Since packages/paths/src/strip-cwd-env.ts was introduced.
  • Severity: minor (UX / developer experience — not a data loss risk in isolation; compounds with bug(cli/setup): archon setup silently overwrites repo .env and loses secrets #1303 for users who discovered <repo>/.env as the "obvious" place to put archon env vars and then ran archon setup)

Steps to Reproduce

  1. In any Archon clone, put a new env var in the repo root .env:
    # Archon/.env
    SLACK_WEBHOOK=https://hooks.slack.com/services/XXX/YYY/ZZZ
    
  2. Run any CLI command, e.g. bun run cli workflow list --json.
  3. Observe the dotenv preamble output:
    [dotenv@17.3.1] injecting env (0) from .env
    [dotenv@17.3.1] injecting env (0) from .env.local
    [dotenv@17.3.1] injecting env (0) from .env.development
    [dotenv@17.3.1] injecting env (0) from .env.production
    [dotenv@17.3.1] injecting env (3) from ../../../../../.archon/.env
    
  4. Inside a workflow node (or any code reading process.env.SLACK_WEBHOOK), the var is not present.

Expected vs Actual

  • Expected:
    1. Logs clearly state that CWD env keys were deliberately stripped (with the concrete path and files named) — not "injecting (0)".
    2. There is an archon-scoped repo-local location (<repo>/.archon/.env) that the operator can use for per-project workflow secrets and that DOES load.
  • Actual: the misleading injecting env (0) from .env makes operators believe their env file was parsed but empty. The only loadable location is ~/.archon/.env (user-wide), which is not discoverable from the logs or docs. Users who put secrets in <repo>/.env lose them silently to stripCwdEnv.

User Flow

operator                    Archon CLI                          workflow node
────────                    ──────────                          ─────────────
edits .env ─────────────▶  bun auto-loads .env into process.env
adds SLACK_WEBHOOK=…
                            strip-cwd-env-boot [first import]:
                              parses .env to discover keys
                              [X] Reflect.deleteProperty(process.env, 'SLACK_WEBHOOK')
                              logs: "injecting env (0) from .env"
                                    (0 because processEnv:{} is used)

runs workflow ──────────▶  cli.ts loads ~/.archon/.env
                            workflow node reads process.env.SLACK_WEBHOOK
                                                                → undefined
                                                                → "Slack post skipped"
operator confused ◀────────────────────────────── no error, no actionable log line

Environment

  • Platform: CLI (also affects any entry point that imports @archon/paths/strip-cwd-env-boot)
  • Database: N/A
  • Running in worktree? No
  • OS: macOS 25.3.0, but reproduces anywhere stripCwdEnv runs

Logs

Abbreviated CLI startup (run from repo root):

[dotenv@17.3.1] injecting env (0) from .env            ← SLACK_WEBHOOK was parsed + deleted here
[dotenv@17.3.1] injecting env (0) from .env.local
[dotenv@17.3.1] injecting env (0) from .env.development
[dotenv@17.3.1] injecting env (0) from .env.production
[dotenv@17.3.1] injecting env (3) from ../../../../../.archon/.env   ← only loaded source

Relevant code: packages/paths/src/strip-cwd-env.ts:41-66.

The strip pass calls config({ path: filepath, processEnv: {} }) — parsing the file into a throwaway object. dotenv's "injecting (N)" log reflects processEnv writes (always 0 because it's a throwaway), NOT keys parsed from the file. The subsequent Reflect.deleteProperty(process.env, key) loop is what actually makes keys disappear, but it emits nothing.

Impact

  • Affected workflows/commands: any CLI command reading workflow-time env vars (e.g. the repo-triage workflow relying on SLACK_WEBHOOK to post a digest to Slack).
  • Reproduction rate: Always.
  • Workaround: put the var in ~/.archon/.env (user-level) or export it via shell before running. Neither is discoverable from the logs or docs.
  • Data loss risk? No, on its own. Compounds with bug(cli/setup): archon setup silently overwrites repo .env and loses secrets #1303 — a user who put secrets in <repo>/.env and then runs archon setup loses them permanently.

Proposed fix — unified design

Scope by directory convention, not by filename. .archon/ (whether at ~/ or at <repo>/) is the archon-owned namespace. Anything else is the user's.

Path strip-cwd-env Archon loads? Why
<repo>/.env strips (keeps guard) never User's project env — may contain unrelated secrets (their own ANTHROPIC_API_KEY for their app's LLM calls, random tool secrets, etc.) that must not leak into Archon
<repo>/.archon/.env untouched yes (repo scope) Archon-owned directory — user explicitly put these vars here for archon
~/.archon/.env untouched yes (user scope) Archon-owned directory — user-wide archon config

Directory ownership is the security boundary, not the filename.

Load order

1.  ~/.archon/.env        (user-wide defaults; override: true over shell)
2.  <cwd>/.archon/.env    (repo-scoped overrides; override: true over #1)
   — strip-cwd-env runs BEFORE #1, still deletes <repo>/.env keys as today

Specific wins over general — a user can set a default SLACK_WEBHOOK in ~/.archon/.env for personal use, then override per-project with a different #gh-digest webhook in <repo>/.archon/.env.

Clearer log output

Replace the current misleading dotenv@17.3.1 preamble with Archon-owned lines that name the exact CWD path and the exact files, and only print when there's something to report.

Output when there is a repo-local env with keys that get stripped, plus a repo-scoped archon config loaded:

[archon] stripped 4 keys from /Users/rasmus/Projects/cole/Archon (.env, .env.local) to prevent target repo env from leaking into Archon processes
[archon] loaded 3 keys from ~/.archon/.env
[archon] loaded 2 keys from /Users/rasmus/Projects/cole/Archon/.archon/.env (repo scope, overrides user scope)

Output when running from a directory with no CWD env files and no repo-scoped archon config:

[archon] loaded 3 keys from ~/.archon/.env

Rules:

  • Use the CWD path directly — no owner/repo git lookup. Simpler, no boot-time git call, works anywhere Archon runs (CI, Docker, arbitrary dirs).
  • First line only prints when > 0 keys were stripped. Silent in the common case (no target-repo .env). When it does print, the parenthetical names every affected file.
  • Second line only prints when ~/.archon/.env exists and loaded > 0 keys.
  • Third line only prints when <cwd>/.archon/.env exists and loaded > 0 keys. The "(repo scope, overrides user scope)" annotation reminds operators of precedence.
  • Pass { quiet: true } to the underlying dotenv.config() calls so the existing [dotenv@17.3.1] injecting env (N) from … output is suppressed. These Archon-owned lines become the only env-loading signal an operator sees.

Call sites to update

  • packages/paths/src/strip-cwd-env.ts:41-66 — aggregate the 4 files into one log line after the delete loop (emit only when cwdKeys.size > 0); suppress per-file dotenv logging with quiet: true.
  • packages/cli/src/cli.ts:22-31 — replace silent load with a count log after config() returns (emit only when result.parsed has entries); add a second load for <cwd>/.archon/.env with override: true.
  • packages/server/src/index.ts (and any other entry point that loads ~/.archon/.env) — mirror the same two-scope load + log format.

Relation to #1303

Same underlying design decision. #1303 is the write side (where can archon setup write?); this issue is the read side (where does the CLI load from?). They must agree on the same three paths for the model to be coherent.

Landing either without the other leaves the system half-coherent. Ideally they ship together, or #1303 lands on top of #1302.

Scope

  • Package(s) likely involved: paths, cli, server (any entry point that calls stripCwdEnv / loads ~/.archon/.env)
  • Module: paths:strip-cwd-env, cli:cli.ts, server:index.ts

Metadata

Metadata

Assignees

Labels

bugSomething is broken

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions