Skip to content

fix(home): a repository's .env cannot choose Medulla's home or account - #11

Merged
senamakel merged 6 commits into
mainfrom
dotenv-home-hardening
Oct 9, 2026
Merged

senamakel merged 6 commits into
mainfrom
dotenv-home-hardening

Conversation

@senamakel

@senamakel senamakel commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Why

A .env in the current directory is loaded at startup, and it belongs to whatever repository Medulla is opened in, so it is untrusted input. Until now it could set the variables that decide where Medulla's home is and which account in it is used. A hostile repository could ship a .env, plus a ./.medulla/ directory for MEDULLA_DEV, that points Medulla at a planted session, config, or credential store. The command then runs as an account the repository chose. #9 and #10 kept telemetry from naming such an account; this closes the underlying hole for every command.

What

  • home::dotenv::HOME_SELECTORS lists the variables a cwd .env may no longer set:
    • MEDULLA_HOME sets the root directly.
    • MEDULLA_DEV makes the root ./.medulla, inside the checkout.
    • HOME / USERPROFILE decide where the default ~/.medulla is.
    • MEDULLA_USER picks the account within the root.
    • MEDULLA_CONFIG_PATH names a config file that can carry an inline token.
  • load_dotenv_from_cwd and its pure counterpart apply_dotenv refuse those keys and return the refused names. main prints one stderr line naming them, for example medulla: ignored MEDULLA_USER from ./.env — … set it in your shell instead, so a .env MEDULLA_DEV=1 that stops working says why. The line goes to stderr because stdout can be the medulla mcp protocol stream.
  • Every other .env key behaves as before, and none of these variables changes meaning when it comes from the invoking environment.

Behaviour change for developers

The gitbook described .env as "the usual way to set MEDULLA_DEV=1 for local development". That no longer works: set it in the shell (export MEDULLA_DEV=1) or with a shell-level tool such as direnv. configuration.md and environment-variables.md are updated to say so.

Interaction with #10

#10's logout_never_reports_an_account_a_workspace_env_file_selected assumed a .env MEDULLA_USER could re-select the session, so it expected no signed_out at all. With this change that premise is gone. The merge of main replaces it with a_workspace_env_file_cannot_select_the_account, which asserts the stronger outcome: logout succeeds, warns, signs out and reports the invoker's own account (user-42), never reports the planted one, and leaves the planted session untouched. The telemetry trust checks from #9/#10 stay as a second line of defence.

Tests

  • Unit test a_dotenv_cannot_choose_the_home_or_the_account: all six keys are refused and reported in order, ordinary keys still apply, and the root resolves exactly as with no .env.
  • e2e test a_workspace_env_file_cannot_select_the_account runs the real binary. With the refusal disabled it fails.
  • cargo fmt --all -- --check and cargo clippy --locked --all-targets -- -D warnings are clean. cargo test --locked passed in two consecutive full runs (70/70 test binaries) on the branch with main merged in.

Summary by CodeRabbit

  • Bug Fixes
    • Workspace .env files can no longer change which Medulla home or account is selected. Medulla warns when these settings are ignored and continues using values from the environment that launched it.
    • Other .env settings continue to apply as before when they are not already set.
  • Documentation
    • Updated the configuration and environment-variable guides with details on which settings must be provided by the shell.

senamakel and others added 6 commits October 9, 2026 13:44
A `.env` in the working directory is untrusted input, so it may no longer
set the variables that choose Medulla's home or the account inside it;
those could otherwise point the process at a planted session, config, or
credential store. `apply_dotenv` and `load_dotenv_from_cwd` now return the
refused keys so `main` can report them, and the variables still work when
set from the invoking shell.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The TUI now prints a warning to stderr when a repository's `.env` tries to set
variables that select Medulla's home or account, since those must come from the
invoking shell. The comment describing the load order was also updated to
reflect that home selectors are never applied.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add an end-to-end test asserting that a repository's .env cannot choose the
account Medulla uses: MEDULLA_USER from the workspace file is refused with a
warning, logout acts on and reports the invoker's own account, and the planted
session is left untouched.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The configuration and environment-variable docs now explain that a
repository's `.env` cannot set `MEDULLA_HOME`, `MEDULLA_DEV`,
`MEDULLA_USER`, `MEDULLA_CONFIG_PATH`, `HOME`, or `USERPROFILE`, and that
Medulla warns when it ignores them. This closes a hole where a checked-out
repository could redirect Medulla at a session or credentials it planted,
so the docs point developers at shell-level settings such as direnv instead.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Document that MEDULLA_HOME, MEDULLA_DEV, MEDULLA_USER, MEDULLA_CONFIG_PATH, HOME, and USERPROFILE are only read from the environment Medulla is started with, so a .env in the current directory cannot set them. The per-row note on MEDULLA_DEV is replaced by this shared explanation.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Brings in #10. Its logout test assumed a cwd .env could re-select the stored
session (and so expected no signed_out); with .env home selectors refused here,
that premise no longer holds, so it is superseded by
a_workspace_env_file_cannot_select_the_account, which asserts the invoker's own
account is signed out and reported and the planted one never is.

Co-authored-by: Medulla <medulla@tinyhumans.ai>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T11:00:47.111572Z 5a18529 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b43b1caf-51e8-4f29-bee6-ce0aa434f3ff

📥 Commits

Reviewing files that changed from the base of the PR and between 65e16f3 and 5a18529.


📒 Files selected for processing (6)
  • gitbooks/developers/configuration.md
  • gitbooks/developers/environment-variables.md
  • src/sdk/src/home/dotenv.rs
  • src/sdk/src/home/tests.rs
  • src/tui/src/main.rs
  • src/tui/tests/e2e_cli.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.



📝 Walkthrough

Walkthrough

The dotenv loader now refuses six home and account selector variables from cwd .env files and returns their names. The TUI reports refused names to stderr. Tests verify that logout acts on the invoking account.

Changes

Dotenv Home Selector Handling

Layer / File(s) Summary
Define and apply dotenv selector policy
src/sdk/src/home/dotenv.rs, src/sdk/src/home/tests.rs, gitbooks/developers/configuration.md, gitbooks/developers/environment-variables.md
The loader refuses MEDULLA_HOME, MEDULLA_DEV, MEDULLA_USER, MEDULLA_CONFIG_PATH, HOME, and USERPROFILE from cwd .env files. It returns refused names and retains the existing behavior for other variables. Tests check refused and applied values, and the guides describe the restriction.
Report refused selectors at startup
src/tui/src/main.rs, src/tui/tests/e2e_cli.rs
The TUI prints refused variable names to stderr. The end-to-end test checks the warning and verifies that logout removes the invoking account’s session while leaving the planted account’s session intact.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~15 minutes

Change: Bug fix


Merge Risk

Merge Risk: ⚪ Minimal · up to 5a185

The supplied evidence identifies no actionable issue preventing merge. Run the normal checks before merging.

Security Architecture Review

Security architecture risk: ⚪ Minimal · up to 5a185

The change reduces repository control over stored accounts and credential locations while preserving shell-selected settings. The inspected changes introduce no material security regression.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The inspected attack surface is a repository-controlled cwd .env influencing a CLI process running with the operator’s filesystem access. Blocking canonical selectors removes that file’s direct ability to choose an alternate Medulla root, account directory, or explicit configuration file through those variables.

Trust Boundaries and Controls

  • observed — Home/account selector authority moves from repository dotenv input to the invoking environment. The updated logout fixture asserts that a refused MEDULLA_USER leaves the planted session untouched, removes the invoking account’s session, and reports only that account. These are source assertions; tests were not executed during this review.

Resilience and Maintainability Implications

  • observed — Logout still treats an absent session as an idempotent success and propagates session-removal failures before reporting sign-out. Reporting is timeout-bounded after deletion, account selection is retained, and remaining external credentials produce an error. Legacy deletion remains best-effort rather than an atomic logout transaction; this ordering and limitation are unchanged from the base.

Hardening Proposals

  • proposed — Separately define which repository-controlled token, project-config, and legacy-directory inputs are intentionally trusted. Any broader restriction would be a follow-up policy change, not a finding introduced by this PR.



🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: repository-local .env files can no longer select Medulla's home or account.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 4 files. (2 skipped: 2…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.


✨ Finishing Touches 💡 1
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR


🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

I’m a rabbit guarding the den,
No .env can choose it again.
Shell-set selectors may enter,
While refused names go to stderr.
I nibble a carrot, then hop away.

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a18529eb3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +75 to +76
fn is_home_selector(key: &str) -> bool {
HOME_SELECTORS.contains(&key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Match protected environment names case-insensitively

On Windows, environment-variable names are case-insensitive, but this comparison is case-sensitive. A repository can therefore use a spelling such as Home or Medulla_Home; it passes this check and is installed into the process environment, where Windows lookups and inherited child environments treat it as HOME or MEDULLA_HOME. This bypasses the new trust boundary for Windows users, so normalize the key (as the existing environment-secret scrubber does) before comparing it with HOME_SELECTORS.

Useful? React with 👍 / 👎.

@tinysweeper

tinysweeper Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Tiny Sweeper reviewed this change across 6 lane(s) and found 1 active actionable finding(s). Detailed lane evidence and any incomplete work are listed below.

State: Changes requested
Priority: high
Reviewed head: 5a18529eb32c
Updated: 1791544049 (Unix time)

Review snapshot

Change surface Files Review signal Count
Production 2 Active findings 1
Tests 2 Noted findings 0
Documentation 2 Resolved findings 0
Configuration 0 Pending checks/questions 1

Completeness: Complete
Test assessment: No supported feature-to-test mapping was available; this does not mean tests are absent or passed.

What changed

The review could not produce a supported behavioral summary; inspect the cited changed surface and lane details below.

Features

None identified with supported citations.

Tests

No supported feature-to-test mapping was produced. Test execution is not inferred.

Findings

  • high · critique · Reject home selectors regardless of environment-key casing — On Windows, environment variable names are case-insensitive, but this check is exact-case. A repository `.env` containing `medulla_home`, `medulla_dev`, `medulla_user`, `medulla_co (src/sdk/src/home/dotenv\.rs:75)

Pending checks: E2E (docker + tmux + ${{ matrix.harness }})

Before merge

  • Address Reject home selectors regardless of environment-key casing (src/sdk/src/home/dotenv\.rs).
  • Wait for E2E (docker + tmux + ${{ matrix.harness }}).

How this fits together

flowchart LR
  n0["apply_dotenv_never_overrides_existing<br/>changed"]:::changed
  n1["main<br/>changed"]:::changed
  n2["env"]:::impacted
  n3["trim"]:::impacted
  n4["fs"]:::impacted
  n5["write_active_user_id"]:::impacted
  n6["...esss_login_does_not_re_home_a_running_one"]:::impacted
  n7["iter"]:::impacted
  n0 -->|calls| n2
  n0 -->|tests| n2
  n1 -->|calls| n7
  n2 -->|calls| n7
  n3 -->|calls| n7
  n6 -->|calls| n2
  n6 -->|tests| n2
  n6 -->|uses| n4
  n6 -->|calls| n5
  n6 -->|tests| n5
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading
Agent review details

critique

  • Conclusion: Failure
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 6 files; 1 finding. _Code retrieval was unavailable (model: ladder embeddings returned 400 Bad Request: {"error":{"message":"unknown ladder vectors; known ladders are flash (also chat-v1, flash-v1), instant (also no-think, instant-v1), reasoning (also deepseek), max-reasoning (also max-reasoning-v1), deepseek-flash (also reasoning-v1, agentic-v1), deep (also luna), scribe, uncensored, vectors-oai3 (also embeddings-oai3-v1), vision (also vision-v1, multimodal-v1), image (also images-v1, image-v1), vi), so this review saw the diff alone._ _Memory was unavailable (model: cortex: v1/recall: error sending request for url (http://cortexdb:3141/v1/recall\)\), so this review ran without it._
  • Evidence: src/sdk/src/home/dotenv\.rs — Reject home selectors regardless of environment-key casing

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 4 files; 0 findings. 2 files were not security-reviewed: gitbooks/developers/configuration.md (prose or tabular data), gitbooks/developers/environment-variables.md (prose or tabular data). _Code retrieval was unavailable (model: ladder embeddings returned 400 Bad Request: {"error":{"message":"unknown ladder vectors; known ladders are flash (also chat-v1, flash-v1), instant (also no-think, instant-v1), reasoning (also deepseek), max-reasoning (also max-reasoning-v1), deepseek-flash (also reasoning-v1, agentic-v1), deep (also luna), scribe, uncensored, vectors-oai3 (also embeddings-oai3-v1), vision (also vision-v1, multimodal-v1), image (also images-v1, image-v1), vi), so this review saw the diff alone._ _Memory was unavailable (model: cortex: v1/recall: error sending request for url (http://cortexdb:3141/v1/recall\)\), so this review ran without it._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The change refuses home-selecting variables from a cwd `.env` in both the pure and process-env paths, documents the rule, and pins it with a unit test over `apply_dotenv` (explicit refused list, plus a check that the home resolves unchanged) and an end-to-end test asserting the warning on stderr and that logout acts on the invoker's account. The tests would fail if the refusal or warning regressed. Looks sound to merge. _Code retrieval was unavailable (model: ladder embeddings returned 400 Bad Request: {"error":{"message":"unknown ladder vectors; known ladders are flash (also chat-v1, flash-v1), instant (also no-think, instant-v1), reasoning (also deepseek), max-reasoning (also max-reasoning-v1), deepseek-flash (also reasoning-v1, agentic-v1), deep (also luna), scribe, uncensored, vectors-oai3 (also embeddings-oai3-v1), vision (also vision-v1, multimodal-v1), image (also images-v1, image-v1), vi), so this review saw the diff alone._ _Memory was unavailable (model: cortex: v1/recall: error sending request for url (http://cortexdb:3141/v1/recall\)\), so this review ran without it._

commits

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: Nothing sensitive found in what this pull request commits.

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The change refuses home/account-selecting variables from a repository's `.env` exactly as described, with a matching warning on stderr, updated docs, and tests covering both the pure function and the real binary. The description accurately reflects the diff; no problems found. _Code retrieval was unavailable (model: ladder embeddings returned 400 Bad Request: {"error":{"message":"unknown ladder vectors; known ladders are flash (also chat-v1, flash-v1), instant (also no-think, instant-v1), reasoning (also deepseek), max-reasoning (also max-reasoning-v1), deepseek-flash (also reasoning-v1, agentic-v1), deep (also luna), scribe, uncensored, vectors-oai3 (also embeddings-oai3-v1), vision (also vision-v1, multimodal-v1), image (also images-v1, image-v1), vi), so this review saw the diff alone._ _Memory was unavailable (model: cortex: v1/recall: error sending request for url (http://cortexdb:3141/v1/recall\)\), so this review ran without it._

e2e

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: The behavioural change — refusing home/account-selector variables from a repository `.env` and warning about them — is covered end to end: `src/tui/tests/e2e_cli.rs::a_workspace_env_file_cannot_select_the_account` runs the real `medulla` binary with a planted `.env` containing `MEDULLA_USER`, asserts the warning on stderr, that the invoker's account was signed out and reported to a live analytics server while the planted session survived. It was strengthened, not weakened: the old test accepted 'no signed_out event' as the outcome; the new one asserts the new behaviour and also asserts exit success, which the old one did not. Docs changes have no runtime surface. The runtime test drives only `MEDULLA_USER`; the other five refused keys are covered by the sdk unit test, which is appropriate for an internal list. No missing end-to-end coverage found, and the changed e2e test is not weakened. Waiting on 1 end-to-end job: `E2E (docker + tmux + ${{ matrix.harness }})`. 1 end-to-end job passed on this head.
  • Unresolved questions/checks: E2E (docker + tmux + ${{ matrix.harness }})
Evidence and run details
  • Models: gpt-5.6-luna, glm-5.3-flash
  • Spend: $0.005562
  • Tokens: 154808 input · 8220 output · 17627 cached · 0 embedding
Head State Pass summary
5a18529eb32c changes requested 1 active finding(s), 0 resolved finding(s) (at 1791544049)

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: 1 lane(s) blocking, worst finding is high.

Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.

             $0.0056 · 154,808 in / 8,220 out · 17,627 cached (11%) · gpt-5.6-luna, glm-5.3-flash
critique:    $0.0020 · 77,664 in  / 4,861 out · 10,819 cached (14%) · gpt-5.6-luna, glm-5.3-flash
security:    $0.0033 · 45,519 in  / 1,649 out · 3,736 cached (8%)   · gpt-5.6-luna
tests:       $0.0001 · 7,196 in   / 249 out   · 1,536 cached (21%)  · glm-5.3-flash
description: $0.0001 · 7,412 in   / 190 out   · 1,408 cached (19%)  · glm-5.3-flash
e2e:         $0.0001 · 10,513 in  / 207 out   · 0 cached (0%)       · glm-5.3-flash

];

/// Whether `key` is one a `.env` may not set.
fn is_home_selector(key: &str) -> bool {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority high critique confident

Reject home selectors regardless of environment-key casing

On Windows, environment variable names are case-insensitive, but this check is exact-case. A repository .env containing medulla_home, medulla_dev, medulla_user, medulla_config_path, home, or userprofile can therefore be inserted into the process environment and later read through the uppercase selector, allowing the repository to redirect Medulla's home or account despite this protection. Compare keys case-insensitively (or use platform-specific normalization) before applying them.

[RULE] case-insensitive-environment-key ·

@senamakel
senamakel merged commit 5c9e502 into main Oct 9, 2026
15 of 16 checks passed
@senamakel
senamakel deleted the dotenv-home-hardening branch October 9, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant