From 0eb07320b39511137f48f462ecb4a3454ecefb59 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 14:08:06 +0000 Subject: [PATCH 1/2] fix(shellenv): preserve newlines in exported env var values exportify escaped newlines inside double-quoted values as backslash-newline. In the shell, a backslash-newline is a line continuation that gets deleted, so any env var whose value spans multiple lines had its newlines silently stripped and its lines glued together when re-imported via `eval "$(devbox global shellenv)"`. This corrupted a bash-preexec-style PROMPT_COMMAND, gluing `... 2>&1` onto the following `__bp_interactive_mode` command and producing `bash: 1__bp_interactive_mode: ambiguous redirect` on every prompt. A bare newline inside double quotes is already preserved literally, so the fix is simply to stop escaping it. Adds a regression test. Fixes #2814 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QGpiSzrugFvBQtAXxkVT75 --- internal/devbox/envvars.go | 7 ++++++- internal/devbox/envvars_test.go | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/internal/devbox/envvars.go b/internal/devbox/envvars.go index 307546f30e0..29fe5c25221 100644 --- a/internal/devbox/envvars.go +++ b/internal/devbox/envvars.go @@ -93,7 +93,12 @@ 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: a newline is NOT escaped. Inside double quotes a bare + // newline is preserved literally, but a backslash-newline is a + // line continuation that the shell deletes, which would silently + // join multi-line values (e.g. a PROMPT_COMMAND whose commands are + // newline-separated) into one line and corrupt them. + case '$', '`', '"', '\\': strb.WriteRune('\\') } strb.WriteRune(r) diff --git a/internal/devbox/envvars_test.go b/internal/devbox/envvars_test.go index d387feb8c19..18fb2295a37 100644 --- a/internal/devbox/envvars_test.go +++ b/internal/devbox/envvars_test.go @@ -47,6 +47,29 @@ func TestExportifySkipsInvalidNames(t *testing.T) { } } +// TestExportifyPreservesNewlines ensures that env var values containing newlines +// (e.g. a PROMPT_COMMAND whose commands are newline-separated, as set up by +// bash-preexec) are emitted with literal newlines rather than backslash-newline +// line continuations. A backslash-newline is deleted by the shell, which would +// glue adjacent lines together and corrupt the value (see issue #2814, where +// `... 2>&1\n__bp_interactive_mode` became `... 2>&1__bp_interactive_mode` and +// produced a "1__bp_interactive_mode: ambiguous redirect" error). +func TestExportifyPreservesNewlines(t *testing.T) { + got := exportify(io.Discard, map[string]string{ + "PROMPT_COMMAND": "__bp_precmd_invoke_cmd\ncmd >/dev/null 2>&1\n__bp_interactive_mode", + }) + + want := "export PROMPT_COMMAND=\"__bp_precmd_invoke_cmd\ncmd >/dev/null 2>&1\n__bp_interactive_mode\";" + if got != want { + t.Errorf("exportify did not preserve newlines\ngot:\n%q\nwant:\n%q", got, want) + } + // A backslash immediately before a newline is a shell line continuation and + // must not appear, since it would delete the newline and join the lines. + if strings.Contains(got, "\\\n") { + t.Errorf("exportify emitted a backslash-newline line continuation, which corrupts multi-line values:\n%q", got) + } +} + func TestExportifyNushellSkipsInvalidNames(t *testing.T) { got := exportifyNushell(io.Discard, map[string]string{ "GOOD": "value", From 30992db9aa92edf51963daa509ac7fa6dbe48217 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 14:11:11 +0000 Subject: [PATCH 2/2] fix(shellenv): rename loop var to satisfy varnamelen linter The added explanatory comment lengthened the loop body enough that golangci-lint's varnamelen flagged the single-letter 'r'. Rename it to 'char'. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QGpiSzrugFvBQtAXxkVT75 --- 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 29fe5c25221..86e8690b6dc 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 // Note: a newline is NOT escaped. Inside double quotes a bare @@ -101,7 +101,7 @@ func exportify(w io.Writer, vars map[string]string) string { case '$', '`', '"', '\\': strb.WriteRune('\\') } - strb.WriteRune(r) + strb.WriteRune(char) } strb.WriteString("\";\n") }