From 052db9b507710bb14b90c3537ee86742affe88a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 14:06:33 +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 literal newlines in environment variable values as `\`. Inside a double-quoted shell string that sequence is a line continuation, which the shell deletes — joining the value's adjacent lines. For a multi-command PROMPT_COMMAND (e.g. the bash-preexec value set by some terminals), this collapsed a line ending in `2>&1` into the next command `__bp_interactive_mode`, producing `2>&1__bp_interactive_mode` and the `bash: 1__bp_interactive_mode: ambiguous redirect` error on every prompt. A bare newline inside double quotes is already preserved verbatim, so simply drop the backslash escape for `\n`. Add a regression test. Fixes #2814 Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XMi8bm4fhxGAdxSCt5YJY3 --- internal/devbox/envvars.go | 9 ++++++++- internal/devbox/envvars_test.go | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/internal/devbox/envvars.go b/internal/devbox/envvars.go index 307546f30e0..637b58af5a9 100644 --- a/internal/devbox/envvars.go +++ b/internal/devbox/envvars.go @@ -93,7 +93,14 @@ 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 literal newline is NOT escaped. Inside double quotes + // a bare newline is preserved verbatim, whereas a backslash + // immediately followed by a newline is a line continuation that + // the shell deletes. Escaping it would join a multi-line value's + // adjacent lines and corrupt values like a multi-command + // PROMPT_COMMAND (see jetify-com/devbox#2814). + case '$', '`', '"', '\\': strb.WriteRune('\\') } strb.WriteRune(r) diff --git a/internal/devbox/envvars_test.go b/internal/devbox/envvars_test.go index d387feb8c19..1910fe1d5c3 100644 --- a/internal/devbox/envvars_test.go +++ b/internal/devbox/envvars_test.go @@ -47,6 +47,27 @@ func TestExportifySkipsInvalidNames(t *testing.T) { } } +// TestExportifyPreservesNewlines ensures that multi-line values (e.g. a +// PROMPT_COMMAND made of several commands separated by newlines) are emitted so +// that the shell preserves the newlines. A backslash before a newline would be a +// line continuation that the shell deletes, joining adjacent lines and corrupting +// the value (see jetify-com/devbox#2814). +func TestExportifyPreservesNewlines(t *testing.T) { + value := "__bp_precmd_invoke_cmd\ndbus-send ... >/dev/null 2>&1\n__bp_interactive_mode" + got := exportify(io.Discard, map[string]string{"PROMPT_COMMAND": value}) + + // The emitted value must contain a bare newline, not an escaped one. If the + // newline were escaped, "2>&1\" and "__bp_interactive_mode" would collapse + // into "2>&1__bp_interactive_mode" when the shell sourced the export. + if strings.Contains(got, "\\\n") { + t.Errorf("newline should not be backslash-escaped, got:\n%q", got) + } + want := "export PROMPT_COMMAND=\"" + value + "\";" + if !strings.Contains(got, want) { + t.Errorf("expected exported value to preserve newlines.\ngot:\n%q\nwant to contain:\n%q", got, want) + } +} + func TestExportifyNushellSkipsInvalidNames(t *testing.T) { got := exportifyNushell(io.Discard, map[string]string{ "GOOD": "value", From ada2d839f7f97a5e8ffd16ba46951670231e1129 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 14:09:58 +0000 Subject: [PATCH 2/2] fix(shellenv): rename loop var to satisfy varnamelen lint The added comment expanded the loop's scope enough for golangci-lint's varnamelen to flag the single-letter `r`. Rename it to `char`. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XMi8bm4fhxGAdxSCt5YJY3 --- 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 637b58af5a9..c3534f257dd 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 // @@ -103,7 +103,7 @@ func exportify(w io.Writer, vars map[string]string) string { case '$', '`', '"', '\\': strb.WriteRune('\\') } - strb.WriteRune(r) + strb.WriteRune(char) } strb.WriteString("\";\n") }