Skip to content

fix(shellenv): only export env vars Devbox adds or changes - #2894

Merged
mikeland73 merged 1 commit into
mainfrom
claude/focused-goldberg-3l9m20
Sep 15, 2026
Merged

mikeland73 merged 1 commit into
mainfrom
claude/focused-goldberg-3l9m20

Conversation

@mikeland73

Copy link
Copy Markdown
Collaborator

Summary

Fixes #2826.

devbox shellenv and devbox global shellenv export the entire environment, including variables that Devbox never touches such as HOSTNAME and LANG. On distributions that mark some variables read-only (e.g. openSUSE marks PROFILEREAD read-only), eval "$(devbox global shellenv)" then fails with errors like:

(eval):55: read-only variable: PROFILEREAD

Re-exporting a variable to the value it already has is a no-op at best, and at worst breaks the user's prompt/shell on every command.

Fix

EnvExports now restricts the exported variables to those Devbox actually adds or changes relative to the current shell environment. Variables whose value already matches the ambient environment are dropped, since they are already present in the shell — so there is no reason to re-emit them (and no reason to trip over read-only ones).

  • internal/devbox/envvars.go: add onlyModifiedEnvVars(env, ambient) helper that returns only new/changed variables.
  • internal/devbox/devbox.go: in EnvExports, filter the computed env against os.Environ() when the new option is set. Skipped in pure mode, where the intent is to emit a complete, self-contained environment rather than a diff against the current shell.
  • internal/devbox/devopt/devboxopts.go: add OnlyModifiedEnv to EnvExportsOpts.
  • internal/boxcli/shellenv.go: enable OnlyModifiedEnv for the shellenv command (the reported path). The devbox shell --print-env / direnv path is intentionally left unchanged.

Variables Devbox relies on (PATH, the DEVBOX_* helpers, the shellenv hash, plugin/config env, etc.) are always new or changed, so they continue to be exported. Re-running shellenv inside an already-active environment now emits only what differs, which is idempotent.

Testing

  • Added TestOnlyModifiedEnvVars in internal/devbox/envvars_test.go covering unchanged (dropped), changed (kept), and new (kept) variables, including a read-only-style PROFILEREAD case.
  • go test ./internal/devbox/ -run 'TestOnlyModifiedEnvVars|TestExportify' passes.
  • go build ./internal/..., go vet, and gofmt are clean.

(The remaining failures in ./internal/devbox/ unit tests are pre-existing and environmental — they require the nix binary, which isn't available in CI sandbox; they are unrelated to this change.)

cc @gasuketsu (issue reporter)


🤖 Generated with Claude Code

https://claude.ai/code/session_01E6tBNHN6DFv5VabVTAosNq


Generated by Claude Code

`devbox shellenv` and `devbox global shellenv` exported the entire
environment, including unrelated variables such as HOSTNAME and LANG.
On distributions that mark some variables read-only (e.g. PROFILEREAD on
openSUSE), `eval "$(devbox global shellenv)"` then fails with errors like:

    (eval):55: read-only variable: PROFILEREAD

Restrict the exported variables to those Devbox actually adds or changes
relative to the current shell. Unchanged variables are already present in
the shell, so re-exporting them is redundant and only risks these errors.

The filtering is applied to the `devbox shellenv` command path and skipped
in pure mode (where a complete, self-contained environment is intended).

Fixes #2826

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E6tBNHN6DFv5VabVTAosNq
Copilot AI review requested due to automatic review settings June 26, 2026 14:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@mikeland73

Copy link
Copy Markdown
Collaborator Author

@claude please review this PR for correctness, and flag anything that would block merging.

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown

Claude finished @mikeland73's task in 2m 10s —— View job


Reviewing PR #2894

  • Gather context (read changed files, understand diff)
  • Understand the request (code review for correctness, flag merge blockers)
  • Review internal/devbox/envvars.go (onlyModifiedEnvVars)
  • Review internal/devbox/devbox.go (EnvExports integration)
  • Review internal/devbox/devopt/devboxopts.go (new option)
  • Review internal/boxcli/shellenv.go (wiring)
  • Review tests
  • Post final review

Review

I traced the full data flow (computeEnvensureStateIsUpToDateAndComputeEnvEnvExportsonlyModifiedEnvVarsexportify) and this looks correct.

Why it works: computeEnv (internal/devbox/devbox.go:723-724, via parseEnvAndExcludeSpecialCases) starts from os.Environ() and layers Nix/plugin/config env on top, so any variable Devbox doesn't touch (HOSTNAME, LANG, PROFILEREAD, …) passes through with an identical value. onlyModifiedEnvVars (internal/devbox/envvars.go:173-181) then diffs the computed map against a second os.Environ() read taken in EnvExports (internal/devbox/devbox.go:389) — since nothing mutates the process environment in between, this comparison is safe and correctly drops only the untouched variables, fixing the reported PROFILEREAD read-only crash from #2826.

Scoping checked and looks right:

  • Filtering is skipped when Pure is set (internal/devbox/devbox.go:388), which is correct — pure mode intentionally emits a self-contained environment, and the ambient-diff comparison wouldn't be meaningful (parseEnvAndExcludeSpecialCases strips almost everything from the base for pure mode anyway).
  • EnvExports has only two callers: shellenv.go (opts in via OnlyModifiedEnv: true) and shell.go:86 (zero-value EnvExportsOpts{}, so OnlyModifiedEnv defaults to false) — confirming the devbox shell --print-env / direnv path is untouched, as the PR description claims.
  • Both devbox shellenv and devbox global shellenv share shellEnvFunc (internal/boxcli/shellenv.go:96, wired from both internal/boxcli/root.go:79 and internal/boxcli/global.go:41), so the fix applies to both, matching the issue report.
  • The refresh alias and init-hook source line are appended after filtering (internal/devbox/devbox.go:400-407), so they're always emitted regardless of the diff — good, since those aren't "ambient" variables.
  • PATH and the Devbox marker vars (DEVBOX_PROJECT_ROOT, the shellenv hash key, etc.) are always new/changed relative to ambient, so they continue to be exported every time — the fix doesn't regress the core "activate a Devbox env" behavior.

Minor / non-blocking observations:

  • onlyModifiedEnvVars only drops unchanged keys; it has no way to unset a variable that Devbox previously exported into the shell but no longer wants set (e.g. a var from a package that was since removed). This isn't a regression — EnvExports never emitted unset statements before this PR either — but it's worth knowing that "only what differs" is directional (add/change only), not a full reconciliation. Not something this PR needs to solve.
  • onlyModifiedEnvVars/EnvExports do two separate os.Environ() reads (once inside computeEnv via parseEnvAndExcludeSpecialCases, once directly in EnvExports). In-process there's nothing between them that would mutate the environment, so this is safe, just noting it as a subtle coupling to be aware of if either call site is refactored later.
  • Test coverage in envvars_test.go (TestOnlyModifiedEnvVars) directly covers the reported PROFILEREAD-style case plus unchanged/changed/new variables — good, targeted coverage.

Verification: I wasn't able to run go build/go test myself in this sandbox (shell commands required approval that wasn't available in this session), so I relied on careful manual tracing of the diff and surrounding code instead. Nothing in the logic raised red flags, and the PR description's reported local test/build results are consistent with what I see in the code.

Verdict: I don't see anything that should block merging. The change is well-scoped, the filtering logic is sound, and the "opt-in only for shellenv, skip in pure mode" design correctly targets the reported bug without touching the shell/direnv path.

@mikeland73
mikeland73 merged commit 3dbf79b into main Sep 15, 2026
26 checks passed
@mikeland73
mikeland73 deleted the claude/focused-goldberg-3l9m20 branch September 15, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Why devbox shellenv and devbox global shellenv export all existing envvars ?

3 participants