fix(cli): load .env when a command runs, not at import (#1064) - #1139
Conversation
codeframe/cli/app.py called load_env_files() at module import, so
`from codeframe.cli.app import app` — most of the CLI test suite — wrote the
repository's .env into os.environ for the rest of the process. The issue's own
reproduction now prints False:
$ env -u ANTHROPIC_API_KEY .venv/bin/python -c "
import os; from codeframe.cli.app import app
print('after cli app import:', bool(os.environ.get('ANTHROPIC_API_KEY')))"
after cli app import: False
The load moved into the existing Typer root callback, which runs before any
command, so `cf` behaviour and the #904 precedence rules are unchanged — they
apply a moment later. --help and --version are eager and now short-circuit
before it, which is why the "Ignoring security-sensitive key(s)" banner no
longer appears on `cf --help`; neither needs credentials.
validators.py, the other caller, already calls it inside functions (AC4).
Second bug, found by not accepting the new skips at face value:
The fix made six tests start skipping, and the reason turned out to be a
substring guard in tests/lifecycle/conftest.py:
if "lifecycle" in str(item.fspath):
That matches test_api_key_lifecycle_919.py,
test_proof_requirement_lifecycle_923.py and test_lifecycle_gates_948.py — none
of which make an LLM call. So 51 tests have been silently skipped in CI for
having "lifecycle" in their filename. It went unnoticed *because* of #1064: the
import-time load meant the key was always set locally, so the guard only ever
fired in CI, where nobody reads the skip list.
Now keyed on the `lifecycle` marker those tests already carry. Verified both
directions: the 51 run without a key, and the real lifecycle tests still skip
without one and still collect with one.
Tests run in child processes, as the AC asks — this module has already imported
the CLI transitively, so an in-process assertion would prove nothing. Confirmed
non-tautological: restoring the import-time call fails four of six.
Full suite: 6407 passed, 49 skipped — same skip set as before the change, +6
passing.
|
Claude finished @frankbria's task in 7m 0s —— View job Precision bug review — complete
Method note: verified each hunk against the surrounding code —
What I checked and cleared (no defect):
1 finding (1 major), posted as an inline review comment. |
ReviewThe core fix is sound. The One concern:
|
CI caught this and the reviewer diagnosed it identically: my test asserted "False->True" by depending on the repository's untracked .env to supply the key. So it measured the checkout rather than the behaviour, and passed locally for precisely the reason #1064 exists — the very thing this PR fixes. The tests now build their own .env in a tmp dir and run the child there with PYTHONPATH pointing at the repo, so the file load_env_files finds is one the test created. Verified both with the repo's .env present and with it moved aside, which is CI's condition. The assertions got stronger as a side effect: the load test checks the value is "from-dot-env" rather than merely truthy, and the precedence test asserts the operator's value wins AND that the .env value is absent. Full suite: 6407 passed, 49 skipped.
|
Claude finished @frankbria's task in 5m 39s —— View job Precision bug review — completeSynchronize review (new commit
1 finding (1 minor), posted as an inline review comment. Method note — what I verified and cleared (no defect):
The one residual: the load test still measures the contributor's |
ReviewI re-verified the fix on the current diff ( Confirmed sound:
Nothing new to add of substance. One very minor observation, not blocking: the Nice diagnosis chain overall — finding the marker bug by refusing to accept "these six tests started skipping" as expected fallout from the primary fix is a good instinct, and the PR body's evidence table (before/after skip counts) makes it easy to verify. |
Third round on this test. load_env_files reads ~/.env FIRST and both loads use override=False, so on a contributor machine whose real ~/.env carries ANTHROPIC_API_KEY the home value wins and the tmp .env cannot override it — the test fails there and nowhere else. I had isolated cwd and left HOME alone. _child_in now points HOME (and USERPROFILE, for Path.home() on Windows) at an empty directory beside the fixture. Verified by constructing that machine rather than reasoning about it: a fake $HOME containing ANTHROPIC_API_KEY=sk-from-home-env reproduces the predicted failure without the isolation and passes with it. Green across all four combinations of home-.env and repo-.env presence. Full suite: 6407 passed, 49 skipped.
|
Claude finished @frankbria's task in 3m 6s —— View job Precision bug review — completeSynchronize review (new commit
✅ GLM review: no defects found. Method note — what I verified and cleared (no defect):
No inline findings to post — the diff is clean on this synchronize. |
ReviewI re-verified the final state of this PR (commits Confirmed sound, independently:
Minor, non-blocking observation: the module-scope comment left where Solid diagnosis-and-fix cycle overall: refusing to accept "six tests started skipping" as expected fallout from the primary fix, and instead tracing it to a second, independent bug, is exactly the right instinct. The evidence table in the PR description (before/after skip counts, both with and without the API key) makes the fix easy to verify without re-running everything. 🤖 Generated with Claude Code |
Closes #1064.
The fix
load_env_files()moved from module scope into the existing Typer rootcallback. The issue's own reproduction:
A real command still loads it (
False->True), and the #904 precedence rules areunchanged — they just apply a moment later.
validators.py, the other caller,already calls it inside functions (AC4).
One behaviour change worth naming:
--helpand--versionare eager optionsthat short-circuit before the callback body, so they no longer load the env. That
is why the
Ignoring 1 security-sensitive key(s)banner has disappeared fromcf --help. Neither needs credentials, so I consider this an improvement ratherthan a regression — flagging it because it is user-visible.
The second bug, found by not trusting the new skip count
The fix made six tests start skipping. Rather than accept that as "correct now",
I checked why — and found this in
tests/lifecycle/conftest.py:A substring match on the filename. It catches:
tests/auth/test_api_key_lifecycle_919.pytests/core/test_proof_requirement_lifecycle_923.pytests/test_lifecycle_gates_948.pyNone of which make an LLM call. So 51 tests have been silently skipped in
CI for having "lifecycle" in their name.
It went unnoticed because of #1064: the import-time env load meant the key was
always set on a dev machine, so the guard only ever fired in CI — where nobody
reads the skip list. Fixing the env bug is what made it visible.
Now keyed on the
lifecyclemarker those tests already carry(
pytestmark = [pytest.mark.lifecycle, pytest.mark.slow]), which is also whatpytest.ini's-m "not lifecycle"uses. Verified both directions:ANTHROPIC_API_KEYtests/lifecycle/(real LLM)Evidence
Full suite: 6407 passed, 49 skipped — byte-identical skip set to the
pre-change baseline, and +6 passing. So the env fix costs no coverage and the
guard fix restores some.
Acceptance criteria
load_env_files()runs on command execution, not module importcfstill picks up.env; [P0.10] Stop a repository .env from overriding the operator's environment #904 precedence tests unchangedvalidators.pychecked for the same pattern (already correct; pinned by a test)Six tests, run in child processes as the AC requires — this module has already
imported the CLI transitively, so an in-process assertion would prove nothing.
Confirmed non-tautological: restoring the import-time call fails four of six.
Note
This is the third substring-standing-in-for-a-classifier bug found this session,
after #1113 (
typer.Exithandling) and #1116 ("started" in event_type). Mightbe worth a broader sweep for the pattern; I have not filed one, since three
instances is suggestive rather than conclusive.