Skip to content

Read .env from the working directory, as grapharc.toml already is - #85

Merged
Shashankss1205 merged 2 commits into
mainfrom
fix/issue-20
Aug 4, 2026
Merged

Read .env from the working directory, as grapharc.toml already is#85
Shashankss1205 merged 2 commits into
mainfrom
fix/issue-20

Conversation

@Shashankss1205

Copy link
Copy Markdown
Collaborator

⚠️ Behaviour change — read this first

A .env in a parent directory is no longer read. If you relied on one (a key in $HOME, or above a checkout), do one of:

  • move the .env into the directory you run from,
  • export the variable in your shell, or
  • pass env_file= naming the file explicitly.

A real environment variable still beats any file, and an explicit env_file= still reads a file anywhere on disk. Called out in the README's Status and limits list and in CHANGELOG.md under ## Unreleased.

The fix

gateway/config.find_env_file walked (here, *here.parents) — up to / — while cli/config.py refuses exactly that on principle for grapharc.toml. So the thing that spends money was discovered more eagerly than the thing that constrains a run: a run started in a scratch subdirectory silently billed against a key the operator never put in scope (a .env in $HOME on a shared box; a demo checked out under a client project), and since redact() is the only thing that ever prints a key, nothing in normal operation revealed which file paid.

It now reads the start directory (default: cwd) and no ancestor of it:

def find_env_file(start: Path | None = None) -> Path | None:
    """The `.env` in `start` itself (default: cwd), or None. Parents are not read."""
    candidate = (start or Path.cwd()).resolve() / ".env"
    return candidate if candidate.is_file() else None

The signature and the None-when-absent contract are unchanged, so get_secret and the four backend accessors (openrouter_api_key, openai_api_key, ollama_api_key, the base-URL lookups) needed no edit. Env-var precedence is untouched — get_secret still checks os.environ first.

Deliberately not done, per the issue: no new "search boundary" (stopping at a git root is still an upward search), no change to grapharc.toml discovery, no new key spellings.

Before / after

Same repro as the issue — a .env three directories above the working directory:

before                                   after
cwd:            …/deeply/nested/project  cwd:            …/deeply/nested/project
found .env at:  …/envdemo/.env           found .env at:  None
key resolved:   sk-or-parent-secret-…    key resolved:   None

Prose updated

  • grapharc/gateway/config.py module docstring — states the one-directory rule and why it binds harder here than on the config layer.
  • README.md limits list — the "inconsistency" bullet becomes ".env and grapharc.toml follow the same discovery rule", plus the migration note.
  • grapharc/slack/config.py docstring and docs/cookbook/07-slack.md — both described the upward search as the reason the bot won't use this loader; the reason is restated (the bot's workdir is somewhere other things write), the stale claim is gone.
  • docs/cookbook/02-models.md — "the nearest .env" was the walk by another name.

docs/architecture-review.md §4.7 is left alone on purpose: it is a dated audit snapshot ("Version audited: 0.1.1"), not live documentation.

Tests

Three new tests in tests/test_gateway_openrouter.py:

  • a parent-directory .env is not found while one in the start directory is;
  • an explicit env_file= in an unrelated directory still resolves;
  • a process environment variable still beats a .env in cwd.

Reverting the one loop turns the first one red (assert find_env_file(deep) is None → the parent's .env), verified; the other two stay green either way, which is what makes them a precedence check rather than a restatement of the fix.

Verification: pytest → 1,847 passed, 12 deselected (the live ones); ruff check grapharc tests clean. The README byte-comparison tests (test_readme.py, test_cookbook_*.py) are green.

Fixes #20

🤖 Generated with Claude Code

Shashankss1205 and others added 2 commits August 5, 2026 00:13
`find_env_file` walked up parent directories to `/`, while the config layer
next door refuses exactly that on principle — so the file that *spends money*
was discovered more eagerly than the one that *constrains* a run. A run started
in a scratch subdirectory picked up an `OPENROUTER_API_KEY` from any ancestor:
a `.env` in `$HOME` billed every user's experiment on a shared box to that key,
and since `redact()` is the only thing that ever prints a key, nothing said
which file paid.

The start directory (default: cwd) is now the only directory consulted. The
signature and the None-when-absent contract are unchanged, so `get_secret` and
the four backend accessors needed no edit, and neither escape hatch moved: a
real environment variable still beats any file, and `env_file=` still names a
file anywhere. No search boundary replaces the walk — stopping at a git root
would still be an upward search.

Behaviour change: a parent-directory `.env` stops being read. It is called out
in the README's limits list and the changelog, and the docstrings and cookbook
sentences that described the walk now describe the rule that replaced it.

Fixes #20

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Shashankss1205
Shashankss1205 merged commit 1b2aecd into main Aug 4, 2026
5 of 6 checks passed
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.

gateway: the .env credential loader searches parent directories upward; grapharc.toml deliberately does not

1 participant