Skip to content

fix(config): retry transient .env read errors and surface a persistent failure loudly - #82

Open
sosidudku1 wants to merge 2 commits into
mainfrom
fix/windows-env-eperm
Open

fix(config): retry transient .env read errors and surface a persistent failure loudly#82
sosidudku1 wants to merge 2 commits into
mainfrom
fix/windows-env-eperm

Conversation

@sosidudku1

Copy link
Copy Markdown
Collaborator

Problem

On Windows the startup read of <stateDir>/.env can fail with EPERM while the file exists (#59). The loader printed a single stderr line and continued; that line scrolls away before the TUI takes over the alt screen and is easy to miss, so stored API keys silently vanished and setup asked for them again.

What changed

  • load-dotenv: the read of an existing .env is retried up to 3 attempts with 50/150 ms backoff on EPERM/EACCES/EBUSY/EAGAIN, the codes transient antivirus and sync locks surface as on Windows (on POSIX an EACCES is usually a permanent denial and costs one ~200 ms loop before the warning). ENOENT stays a silent no-op, all other codes fail fast.
  • load-dotenv: a failure that survives the retries lands in the new DotenvLoadResult.error (errno code plus attempt count) and prints an actionable warning naming the path and platform-specific fixes: read-only attribute, antivirus exclusion, icacls /reset on Windows; owner and chmod on POSIX. Variable names only, never values. Malformed lines are now reported by line number and length without echoing content, since a torn line from an external non-atomic writer can be a fragment of a secret value.
  • load-config / config-schema: the load outcome travels as config.dotenv so frontends can surface it.
  • tui-command: when config.dotenv.error is set, the TUI repeats the warning as a warn-variant system chat message, because the stderr line is easy to miss under the alt screen.
  • windows-acl (extracted from dotenv-writer): after tightening the .env ACL with icacls /inheritance:r /grant:r, verify the file is still readable by this very process and roll back with icacls /reset when it is not. A grant that resolves to the wrong principal would leave a file nobody can read; that is one plausible cause of the symptom in Windows: EPERM when reading <stateDir>/.env, secrets silently not loaded #59, and the probe closes it off either way.

Deliberate boundaries

  • The sidecar does not forward the warning into the NDJSON stream; embedders currently see it on stderr only. Wiring it into the protocol is separate work.
  • The write path (setDotenvKey) has no retry loop; this PR covers the startup read only.
  • The retries cost at most ~200 ms of startup, and only when a .env exists and stays unreadable; the common paths (no file, readable file) are unchanged.
  • ACLs that are already broken are not auto-repaired at startup; the user-facing path for that is the exact command printed in the warning.

Tests

Retry-then-success loads keys; persistent EPERM warns with path and guidance without throwing; non-retryable codes fail fast; a missing file stays silent; the warning and parse diagnostics carry no values or file content; loadConfig propagates the outcome into config.dotenv (happy path and the unreadable-file case); ACL tighten/probe/rollback paths are covered with injected deps. The loader contract is pinned in AGENTS.md.

Closes #59

sosidudku1 and others added 2 commits August 7, 2026 23:00
On Windows the startup read of <stateDir>/.env can fail with EPERM
while the file exists (issue #59). The loader used to print one stderr
line and continue, which the alt-screen TUI hides entirely, so stored
API keys silently vanished and setup asked for them again.

Changes:
- load-dotenv: retry the read up to 3 times with a short backoff
  (50/150 ms) on EPERM/EACCES/EBUSY/EAGAIN, the codes transient
  antivirus/sync locks surface as. ENOENT stays a silent no-op and
  other codes fail fast.
- load-dotenv: report a surviving failure in the new
  DotenvLoadResult.error field (errno code, Node message, attempt
  count) and print an actionable warning naming the path and
  platform-specific fixes (read-only attribute, antivirus exclusion,
  icacls /reset on Windows; owner/chmod on POSIX). Variable names
  only, never values.
- load-config/config-schema: carry the load outcome as config.dotenv
  so frontends can surface it.
- tui-command: when config.dotenv.error is set, emit a warn-variant
  system chat message with the same guidance, because stderr is
  invisible under the alt screen.
- windows-acl (extracted from dotenv-writer): after tightening the
  .env ACL with icacls, verify the file is still readable by this
  process and roll back with icacls /reset when it is not. An
  account-name grant that resolves to the wrong principal after
  /inheritance:r would otherwise leave a file the agent itself can
  no longer read.

Tests: retry-then-success loads keys, persistent EPERM warns with
path and guidance without throwing, non-retryable codes fail fast,
missing file stays silent, warning text carries no values, ACL
tighten/verify/rollback paths covered with injected deps.

Closes #59

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-ups on the unreadable .env change:

- Drop DotenvReadFailure.message. For errno failures Node's message is
  a fixed function of code, syscall, and path, so it duplicated what
  the warning already prints from the code and DotenvLoadResult.path;
  the field was carried through config but never rendered anywhere.
  The failure now carries the errno code and the attempt count only.
- Stop echoing the raw .env line in the missing '=' diagnostic and
  report the 1-based line number and length instead. A line without
  '=' can be a fragment of a secret value left behind by an external
  non-atomic writer, so the diagnostic must locate it, not repeat it.
- Reword the retry-code comment honestly: EACCES on POSIX is almost
  always a permanent denial; it is retried for the sake of the Windows
  antivirus and sync-lock family, and on POSIX that costs one ~200ms
  backoff loop before the loud warning.
- Soften the windows-acl comment: a wrong-principal grant is one
  plausible cause of the symptom reported in #59, not a confirmed
  root cause; the probe read closes it off either way.
- Correct the tui-command comment: the stderr line scrolls away
  before the alt screen takes over and is easy to miss; it is not
  hidden entirely.
- Cover config.dotenv propagation in load-config.test.ts: loaded names
  on the happy path and the error case, using a directory named .env
  as the portable stand-in for an existing-but-unreadable file.
- Pin the loader contract in AGENTS.md: retried codes, attempts and
  backoff, silent ENOENT, the config.dotenv outcome, the TUI warning,
  and the icacls probe-read rollback, with the pinning tests named.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows: EPERM when reading <stateDir>/.env, secrets silently not loaded

1 participant