Skip to content

fix(run): run scripts and init hooks with bash, not sh (#2607)#2928

Open
mikeland73 wants to merge 2 commits into
mainfrom
claude/focused-goldberg-cngh7e
Open

fix(run): run scripts and init hooks with bash, not sh (#2607)#2928
mikeland73 wants to merge 2 commits into
mainfrom
claude/focused-goldberg-cngh7e

Conversation

@mikeland73

Copy link
Copy Markdown
Collaborator

Summary

Fixes #2607.

devbox run executed generated scripts — and the init hook they source — with sh. On many systems (Debian/Ubuntu, and the CI containers used here) /bin/sh is a POSIX shell such as dash. Init hooks that use bash features, most commonly the source builtin, work inside devbox shell (which runs them under the user's interactive shell, usually bash) but fail under devbox run with:

.hooks.sh: source: not found
Error: error running script "..." in Devbox: exit status 127

So the same devbox.json behaves differently between devbox shell and devbox run, which is surprising and hard to debug.

Change

  • nix.RunScript now prefers bash, falling back to POSIX sh (and finally a well-known absolute path) when bash isn't installed. Running scripts under bash matches the interactive shell and is a strict superset of the previous behavior for POSIX scripts.
  • Updated the now-stale Scripts always use sh to run comments in internal/shellgen to reflect the new behavior.

The fallback keeps things working on the rare system without bash, so no POSIX script regresses.

How was it tested?

  • go build ./..., go vet, and gofmt are clean.
  • Added a unit test in the nix package (internal/nix/run_test.go) that runs the source builtin through RunScript and asserts it succeeds — this fails when scripts run under dash and passes under bash.
  • Added an integration testscript (testscripts/run/init_hook_source.test.txt) reproducing the exact issue: an init_hook that sources a file, with a script that reads the sourced variable.
  • Manually confirmed the root cause: dash -c 'source /dev/null'source: not found (exit 127); bash -c 'source /dev/null' → ok.

cc @ashishkurian (issue reporter)

Community Contribution License

All community contributions in this pull request are licensed to the project
maintainers under the terms of the
Apache 2 License.

By creating this pull request, I represent that I have the right to license the
contributions to the project maintainers under the Apache 2 License as stated in
the
Community Contribution License.


Generated by Claude Code

`devbox run` executed generated scripts (and the init hook they source)
with `sh`, which on many systems is a POSIX shell such as dash. Init
hooks that use bash features — most commonly the `source` builtin —
work inside `devbox shell` (which runs them under the user's interactive
shell, usually bash) but failed under `devbox run` with:

    .hooks.sh: source: not found

This inconsistency between `devbox shell` and `devbox run` is surprising.

Have nix.RunScript prefer bash, falling back to POSIX sh (and finally a
well-known absolute path) when bash isn't installed. Running scripts
under bash matches the interactive shell and is a strict superset of the
previous behavior for POSIX scripts.

Adds a unit test in the nix package and an integration testscript that
reproduces the sourced-init-hook case, and updates the now-stale
"scripts always use sh" comments.

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

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 run executing generated scripts (and sourced init hooks) under sh—often dash on Debian/Ubuntu—by preferring bash so bash-specific init hooks (notably source) behave consistently with devbox shell.

Changes:

  • Update internal/nix.RunScript to execute via bash when available (fallback to sh).
  • Refresh internal/shellgen comments/docs to reflect the new runner behavior.
  • Add unit + integration tests reproducing and preventing the source: not found regression.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
testscripts/run/init_hook_source.test.txt Integration test ensuring init_hook using source works under devbox run.
internal/shellgen/tmpl/script-wrapper.tmpl Update template comments to document bash-preferred behavior.
internal/shellgen/scripts.go Update comment explaining why set -e is safe under bash/sh.
internal/nix/run.go Switch runner selection to prefer bash and add helper for resolving the shell.
internal/nix/run_test.go Unit tests verifying RunScript uses bash (and that the runner prefers bash).

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

Comment thread internal/nix/run.go
Comment on lines +47 to +52
func scriptRunnerPath() string {
if bashPath := cmdutil.GetPathOrDefault("bash", ""); bashPath != "" {
return bashPath
}
return cmdutil.GetPathOrDefault("sh", "/bin/sh")
}
Comment thread internal/nix/run_test.go
Comment on lines +6 to +29
import (
"strings"
"testing"

"go.jetify.com/devbox/internal/cmdutil"
)

// TestRunScriptUsesBashForBashisms verifies that RunScript executes scripts
// with bash so that init hooks and scripts relying on bash features work with
// `devbox run`, matching the behavior of `devbox shell`. `source` is a bash
// builtin that POSIX sh implementations like dash don't provide, so it makes a
// good proxy for "are we running under bash?".
//
// Regression test for https://github.com/jetify-com/devbox/issues/2607
func TestRunScriptUsesBashForBashisms(t *testing.T) {
if !cmdutil.Exists("bash") {
t.Skip("bash is not installed; RunScript falls back to sh")
}

// `source` fails under dash (`source: not found`) but succeeds under bash.
err := RunScript(t.TempDir(), "source /dev/null", map[string]string{})
if err != nil {
t.Fatalf("RunScript should run bashisms under bash, got error: %v", err)
}
The CI testscript sandbox runs devbox with a minimal PATH that excludes
/bin and /usr/bin, so exec.LookPath("bash") failed and scriptRunnerPath
fell back to /bin/sh (dash), reintroducing the "source: not found"
failure this change is meant to fix (and failing the new
init_hook_source testscript).

Check well-known absolute locations (/bin/bash, /usr/bin/bash,
/usr/local/bin/bash) before falling back to sh, so bash is used even
when it isn't on PATH. Also addresses Copilot review feedback: source a
temp file instead of /dev/null in the unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEsmatXnTzGGBLG9H5ySrw

Copy link
Copy Markdown
Collaborator Author

Pushed e3dbc7a to address the CI failure and the review feedback.

The init_hook_source testscript failed for exactly the reason @copilot flagged: the testscript sandbox runs devbox with a minimal PATH (no /bin or /usr/bin), so exec.LookPath("bash") failed and scriptRunnerPath fell back to /bin/sh (dash) — reintroducing source: not found. scriptRunnerPath now checks well-known absolute locations (/bin/bash, /usr/bin/bash, /usr/local/bin/bash) before falling back to sh, so bash is used even when it isn't on PATH.

Also switched the unit test to source a temp file instead of /dev/null, per the review.


Generated by Claude Code

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.

Cannot run devbox script if another script is sourced in the init hook

3 participants