Skip to content

feat(integrate): add --run-init-hook flag to run init_hook when reopening an editor - #2874

Merged
mikeland73 merged 7 commits into
mainfrom
claude/focused-goldberg-6qy8fs
Sep 15, 2026
Merged

mikeland73 merged 7 commits into
mainfrom
claude/focused-goldberg-6qy8fs

Conversation

@mikeland73

@mikeland73 mikeland73 commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Addresses #2703. The VSCode extension side is in the stacked PR #2975.

devbox integrate vscode powers the VSCode/Cursor "Reopen in Devbox" action. It computes the Devbox environment and relaunches the editor with it — but it uses Devbox.EnvVars, which deliberately excludes the init hook:

// internal/devbox/devbox.go
func (d *Devbox) EnvVars(ctx context.Context) ([]string, error) {
	// this only returns env variables for the shell environment excluding hooks
	...
}

So any environment variables exported by a project's init_hook are missing from the reopened editor, even though they are present in a normal devbox shell. Editors like Cursor that open their own terminals — and don't get the devbox shell injection — are left without those variables.

Fix

  • Add Devbox.EnvVarsWithInitHook, which sources the init hook in a subshell and captures the resulting environment.
  • Add an opt-in --run-init-hook flag to devbox integrate vscode (default off). When set, the command uses EnvVarsWithInitHook instead of EnvVars. Default behavior is unchanged, since init hooks can be slow or have side effects.

Implementation details:

  • The init hook is sourced with its stdout redirected to stderr (. "$1" 1>&2) so the hook's own output can never corrupt the captured environment (the integrate command speaks an IPC protocol to the editor over a separate fd, so this is important).
  • The hooks path is passed as a positional parameter rather than interpolated into the script, so special characters in the path can't affect shell parsing.
  • The environment is dumped NUL-separated via awk's ENVIRON (portable to macOS, unlike env -0) so values containing newlines survive intact.
  • If the init hook errors, it falls back to the hook-less environment rather than failing, so the integration keeps working.
  • The hooks file is written via the existing shellgen.WriteScriptsToFiles, and sourced the same way EnvExports/direnv already source it — so this matches existing behavior of running hooks outside an interactive shell.

How was it tested?

  • go build ./... and go vet pass.
  • Unit tests in internal/devbox/inithookenv_test.go:
    • TestParseNulEnv — parsing of NUL-separated output, including a value with an embedded newline and an empty value.
    • TestCaptureEnvWithInitHook — a hook that sets a new var, overrides an existing one, and prints to stdout; asserts the new/overridden vars are captured and the hook's stdout does not leak into the env.
    • TestCaptureEnvWithInitHook_NoHooksFile — returns the base env unchanged when there is no hooks file.

Note: the integrate vscode command itself requires a Node parent process (go2node), so it isn't exercised by a testscript here; the new logic is covered by the unit tests above and CI runs the full suite.

cc @tm-michael (issue reporter) — thanks for the clear repro.

🤖 Generated with Claude Code

`devbox integrate vscode` (the VSCode/Cursor "Reopen in Devbox" action)
launched the editor with the computed Devbox environment but never ran the
project's init hook. It used Devbox.EnvVars, which deliberately excludes
hooks. As a result, environment variables exported by init_hook (and any
other side effects) were missing from the reopened environment, even though
they are present in a normal `devbox shell`.

Add Devbox.EnvVarsWithInitHook, which sources the init hook in a subshell and
captures the resulting environment (NUL-separated so multiline values survive,
with the hook's stdout redirected to stderr so it can't corrupt the dump). If
the hook errors, it falls back to the hook-less environment so the integration
keeps working. The integrate command now uses this method.

Fixes #2703.
Copilot AI review requested due to automatic review settings June 18, 2026 14:14
- Use fileutil.Exists instead of os.Stat to detect a missing hooks file,
  avoiding the nilerr lint error from returning nil after a non-nil error.
- Use t.Context() instead of context.Background() in tests (usetesting).

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.

Pull request overview

This PR fixes devbox integrate vscode so the VSCode/Cursor “Reopen in Devbox” flow captures environment variables and side effects from a project’s init_hook, matching what users get in a normal devbox shell.

Changes:

  • Added Devbox.EnvVarsWithInitHook to compute env vars by sourcing the generated hooks file in a subshell and capturing the resulting environment.
  • Added unit tests covering NUL-separated env parsing and init hook capture behavior (including preventing hook stdout from corrupting the captured env).
  • Updated integrate vscode to use EnvVarsWithInitHook instead of EnvVars.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
internal/devbox/inithookenv.go Implements env capture that includes init hook effects by sourcing hooks in a subshell and parsing captured env output.
internal/devbox/inithookenv_test.go Adds unit tests for NUL-env parsing and init-hook-based env capture behavior.
internal/boxcli/integrate.go Switches VSCode integration to use the new env computation that includes init hook effects.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/devbox/inithookenv.go Outdated
Address review feedback on captureEnvWithInitHook:

- Pass the hooks path as a positional parameter ($1) to `sh -c` instead of
  interpolating it into the script, so paths containing spaces, quotes, $()
  or backticks can't alter shell parsing.
- Dump the environment with awk's POSIX ENVIRON instead of `env -0`. macOS'
  default /usr/bin/env does not support `-0`, which would have made the hook
  capture silently fall back to the hook-less env on macOS. awk is portable
  across Linux and macOS.

Copy link
Copy Markdown
Collaborator Author

Heads up on CI: the one red check — test (not-main, ubuntu-latest, project-tests-off, 2.18.0) — failed only on TestScripts/add_platforms_flakeref.test, which is an unrelated, network-heavy devbox add flakeref test (it fetches ~100 MiB across 80 nixpkgs store paths and ran ~200s). It is not exercised by this PR's changes (a new isolated internal/devbox/inithookenv.go helper plus one line in internal/boxcli/integrate.go).

Evidence it's a flake, not a regression:

  • test-nix-versions (ubuntu-latest, 2.18.0) — the same Nix version, running the full testscript suite — passed, as did all other test-nix-versions jobs (2.18.0/2.19.2/2.24.7/2.30.2 × ubuntu/macos).
  • golangci-lint (ubuntu + macos), build-devbox (both), Test Flake Build, and Spell Check all passed.
  • The other test (not-main, …) jobs show cancelled — that's the matrix fail-fast cascade from this single failure, not independent failures.

I don't have permission to re-run failed jobs (API returns 403). Could a maintainer re-run that job? It should go green on retry. Happy to push a no-op commit to re-trigger CI instead if you'd prefer.


Generated by Claude Code

@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 3m 5s —— View job


Reviewing PR #2874

  • Gather context (diff, changed files, CLAUDE.md)
  • Review internal/devbox/inithookenv.go
  • Review internal/devbox/inithookenv_test.go
  • Review internal/boxcli/integrate.go change
  • Verify the macOS env -0 fix mentioned in review comments actually landed
  • Post final review

Review

I reviewed the full diff (base c9fd5b8 → tip 4d765f6, confirmed unchanged through current HEAD) covering internal/boxcli/integrate.go, internal/devbox/inithookenv.go, and internal/devbox/inithookenv_test.go. I don't see anything that should block merging.

Copilot's earlier concerns are resolved. I verified in the current code (internal/devbox/inithookenv.go:86):

cmd := exec.CommandContext(ctx, "sh", "-c", script, "sh", hooksPath)

The hooks path is passed as a positional parameter ($1), not interpolated into the script string, so paths with spaces/quotes/$()/backticks are safe. And env -0 was replaced with a portable POSIX awk 'BEGIN { for (k in ENVIRON) ... }', avoiding the macOS /usr/bin/env limitation Copilot flagged.

Things I specifically checked and confirmed are correct:

  • Shell consistency: The hook is sourced via sh -c. This matches existing devbox convention — internal/shellgen/tmpl/script-wrapper.tmpl explicitly states "Scripts always use sh to run, so POSIX is OK," and devbox run already sources the same .hooks file the same way. So this doesn't introduce a new inconsistency, though it's worth noting: a hook containing bash-only syntax (e.g. [[, arrays) could behave differently here than under an interactive devbox shell (which sources hooks via the user's actual $SHELL, e.g. zsh/bash). That's a pre-existing split in the codebase, not something this PR introduces — not a blocker.
  • stdout/stderr isolation: . "$1" 1>&2 correctly redirects the hook's own stdout to stderr before exec awk ..., so hook output (e.g. echo) can't corrupt the NUL-separated env dump. Covered by TestCaptureEnvWithInitHook.
  • Base environment correctness: cmd.Env = envir.MapToPairs(baseEnv) — confirmed baseEnv (from ensureStateIsUpToDateAndComputeEnv) is a full environment (starts from os.Environ() and layers on nix/devbox vars), not a partial diff, so the subprocess has everything it needs (PATH, HOME, etc.) — equivalent to what an interactive devbox shell would see.
  • Failure handling: If sourcing the hook fails, EnvVarsWithInitHook falls back to the hook-less env rather than erroring, so integrate vscode keeps working — matches the PR's stated design.
  • parseNulEnv: correctly splits on first = only (via strings.Cut), skips empty trailing segments after the last NUL, and preserves embedded newlines in values. Test coverage (TestParseNulEnv) matches.
  • fileutil.Exists: equivalent to the original os.Stat check (returns false on any stat error).
  • Confirmed CI is currently green (cli-tests run 27926791020 passed) — the earlier flaky add_platforms_flakeref.test failure mentioned in the PR comments is not present in the latest run.

Minor, non-blocking observations:

  1. slog.Debug("failed to run init hook while computing env", ...) is the only signal when the hook fails — a user whose hook errors (e.g. due to the sh-vs-bash syntax gap above) gets silently the same missing-env-vars behavior this PR set out to fix, with no visible diagnostic (slog.Debug is essentially invisible without extra flags). Not a correctness bug, just a UX rough edge worth a follow-up if it comes up in practice.
  2. The PR description's framing that stdout redirection matters because "the integrate command speaks an IPC protocol... over a separate fd" is slightly imprecise — go2node's IPC doesn't run through this subprocess's stdout at all; the real reason redirection matters is simply that cmd.Output() here is being used to parse the env dump, and hook stdout would corrupt that parsing. Doesn't affect correctness, just a docs nit.

Nice fix — good test coverage for the tricky parts (NUL parsing, stdout isolation, missing-hooks-file fallback), and the iteration in response to the Copilot review meaningfully improved portability/robustness.
· branch

Running the init hook when reopening an editor is now behind an explicit
--run-init-hook flag (default off), since init hooks can be slow or have
side effects. The VSCode extension will expose this as a setting in a
follow-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mikeland73 mikeland73 changed the title fix(integrate): run init_hook when reopening an editor in the Devbox environment feat(integrate): add --run-init-hook flag to run init_hook when reopening an editor Sep 15, 2026
@mikeland73
mikeland73 merged commit 389ef11 into main Sep 15, 2026
28 checks passed
@mikeland73
mikeland73 deleted the claude/focused-goldberg-6qy8fs branch September 15, 2026 21:25
mikeland73 added a commit that referenced this pull request Sep 16, 2026
Stacked on #2874 (adds the `--run-init-hook` flag to `devbox integrate
vscode`). Merge that first.

## Summary

- New extension setting **`devbox.runInitHookOnReopen`** (boolean,
default `false`). When enabled, "Devbox: Reopen in Devbox shell
environment" passes `--run-init-hook` to `devbox integrate vscode`, so
the project's `init_hook` runs and any environment variables it exports
are present in the reopened editor. See #2703.
- The flag is only appended when the setting is on, so users on an older
CLI that doesn't know the flag keep working with default settings.
- Bumps the extension to **0.1.9** so `vsce publish --skip-duplicate` in
the release workflow actually publishes the change.
- README and CHANGELOG updated.

## How was it tested?

- `yarn compile` and `yarn lint` pass in `vscode-extension/`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

3 participants