From dbcb8d8375055290c926b6de23b8f6443ad5c8a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 14:08:02 +0000 Subject: [PATCH 1/2] fix(shellenv): preserve newlines in exported env values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit exportify escaped newlines in env values as a backslash-newline. Inside a double-quoted bash string that is a line continuation, which the shell removes, silently joining the two lines together. Any multi-line value — most notably a PROMPT_COMMAND captured from bash-preexec — was corrupted: adjacent lines fused into strings like `... 2>&1__bp_interactive_mode`, producing a "bash: ...: ambiguous redirect" error at every prompt. A literal newline inside double quotes is already preserved as-is, so stop escaping it. Add a regression test covering the multi-line PROMPT_COMMAND case from the issue. Fixes #2814 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014nhuYqHdGxzteXxH78b4sE --- internal/devbox/envvars.go | 11 ++++++++++- internal/devbox/envvars_test.go | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/internal/devbox/envvars.go b/internal/devbox/envvars.go index 307546f30e0..651df5bce75 100644 --- a/internal/devbox/envvars.go +++ b/internal/devbox/envvars.go @@ -93,7 +93,16 @@ func exportify(w io.Writer, vars map[string]string) string { switch r { // Special characters inside double quotes: // https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03 - case '$', '`', '"', '\\', '\n': + // + // Note: newline is intentionally NOT escaped. A literal newline + // inside double quotes is preserved as-is, but a backslash + // followed by a newline is a line continuation that the shell + // *removes*, silently joining the two lines. Escaping it would + // corrupt any multi-line value (e.g. a PROMPT_COMMAND that spans + // several lines), producing broken shell such as + // `... 2>&1__bp_interactive_mode` and errors like + // "ambiguous redirect". See issue #2814. + case '$', '`', '"', '\\': strb.WriteRune('\\') } strb.WriteRune(r) diff --git a/internal/devbox/envvars_test.go b/internal/devbox/envvars_test.go index d387feb8c19..1182dbee2b2 100644 --- a/internal/devbox/envvars_test.go +++ b/internal/devbox/envvars_test.go @@ -47,6 +47,28 @@ func TestExportifySkipsInvalidNames(t *testing.T) { } } +// TestExportifyPreservesNewlines ensures multi-line env values (e.g. a +// PROMPT_COMMAND that spans several lines) survive round-tripping through the +// shell. A newline must be emitted literally, not as a backslash-newline: inside +// double quotes the latter is a line continuation that the shell removes, which +// silently joins adjacent lines and corrupts the value. See issue #2814, where a +// multi-line PROMPT_COMMAND collapsed into `... 2>&1__bp_interactive_mode` and +// produced a "bash: ...: ambiguous redirect" error at every prompt. +func TestExportifyPreservesNewlines(t *testing.T) { + value := "line1 >/dev/null 2>&1\n__bp_interactive_mode" + got := exportify(io.Discard, map[string]string{"PROMPT_COMMAND": value}) + + // The value must be emitted with a literal newline, never a + // backslash-newline (which bash would strip as a line continuation). + if strings.Contains(got, "2>&1\\\n") { + t.Errorf("newline was escaped as a line continuation, which corrupts the value:\n%s", got) + } + want := "export PROMPT_COMMAND=\"line1 >/dev/null 2>&1\n__bp_interactive_mode\";" + if got != want { + t.Errorf("exportify(...) = %q, want %q", got, want) + } +} + func TestExportifyNushellSkipsInvalidNames(t *testing.T) { got := exportifyNushell(io.Discard, map[string]string{ "GOOD": "value", From 9ee9e5007b5631daaa1dbcdd7710f9f8968aa1b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 14:10:50 +0000 Subject: [PATCH 2/2] fix(shellenv): rename loop var to satisfy varnamelen lint The added explanatory comment widened the scope over which the single-letter loop variable is used, tripping golangci-lint's varnamelen. Rename it to a descriptive name. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_014nhuYqHdGxzteXxH78b4sE --- internal/devbox/envvars.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/devbox/envvars.go b/internal/devbox/envvars.go index 651df5bce75..d92497945b6 100644 --- a/internal/devbox/envvars.go +++ b/internal/devbox/envvars.go @@ -89,8 +89,8 @@ func exportify(w io.Writer, vars map[string]string) string { strb.WriteString("export ") strb.WriteString(key) strb.WriteString(`="`) - for _, r := range vars[key] { - switch r { + for _, char := range vars[key] { + switch char { // Special characters inside double quotes: // https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03 // @@ -105,7 +105,7 @@ func exportify(w io.Writer, vars map[string]string) string { case '$', '`', '"', '\\': strb.WriteRune('\\') } - strb.WriteRune(r) + strb.WriteRune(char) } strb.WriteString("\";\n") }