fix(ux): suppress duplicate install message and set UTF-8 locale (#2950)

1. Suppress Claude Code curl installer stdout — the remote installer
   prints its own "Installation complete!" which duplicated the local
   "Claude Code agent installed successfully" message.

2. Export LANG=C.UTF-8 in both the interactive SSH session command and
   the .spawnrc env config. Fresh cloud VMs often default to the C
   locale which cannot render Unicode properly, causing garbled ANSI
   output in agent TUIs (e.g. "⏵⏵bypasspermissionson" instead of
   properly spaced text).

Fixes #2946

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
A 2026-03-24 01:59:11 -07:00 committed by GitHub
parent 0f3cb8b2eb
commit f93c799db8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 8 additions and 6 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.25.25",
"version": "0.25.26",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -1213,7 +1213,7 @@ export async function interactiveSession(cmd: string): Promise<number> {
throw new Error("Invalid command: must be non-empty and must not contain null bytes");
}
const term = sanitizeTermValue(process.env.TERM || "xterm-256color");
const fullCmd = `export TERM='${term}' PATH="$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${shellQuote(cmd)}`;
const fullCmd = `export TERM='${term}' LANG='C.UTF-8' PATH="$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${shellQuote(cmd)}`;
const keyOpts = getSshKeyOpts(await ensureSshKeys());
const exitCode = spawnInteractive([
"ssh",

View file

@ -1471,7 +1471,7 @@ export async function interactiveSession(cmd: string, ip?: string): Promise<numb
}
const serverIp = ip || _state.serverIp;
const term = sanitizeTermValue(process.env.TERM || "xterm-256color");
const fullCmd = `export TERM='${term}' PATH="$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${shellQuote(cmd)}`;
const fullCmd = `export TERM='${term}' LANG='C.UTF-8' PATH="$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${shellQuote(cmd)}`;
const keyOpts = getSshKeyOpts(await ensureSshKeys());
const exitCode = spawnInteractive([

View file

@ -1107,7 +1107,7 @@ export async function interactiveSession(cmd: string): Promise<number> {
const username = resolveUsername();
const term = sanitizeTermValue(process.env.TERM || "xterm-256color");
// Use shellQuote for consistent single-quote escaping (prevents shell expansion of $variables in cmd)
const fullCmd = `export TERM='${term}' PATH="$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${shellQuote(cmd)}`;
const fullCmd = `export TERM='${term}' LANG='C.UTF-8' PATH="$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${shellQuote(cmd)}`;
const keyOpts = getSshKeyOpts(await ensureSshKeys());
const exitCode = spawnInteractive([

View file

@ -929,7 +929,7 @@ export async function interactiveSession(cmd: string, ip?: string): Promise<numb
}
const serverIp = ip || _state.serverIp;
const term = sanitizeTermValue(process.env.TERM || "xterm-256color");
const fullCmd = `export TERM='${term}' PATH="$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${shellQuote(cmd)}`;
const fullCmd = `export TERM='${term}' LANG='C.UTF-8' PATH="$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH" && exec bash -l -c ${shellQuote(cmd)}`;
const keyOpts = getSshKeyOpts(await ensureSshKeys());

View file

@ -109,7 +109,7 @@ async function installClaudeCode(runner: CloudRunner): Promise<void> {
`if [ -f ~/.bash_profile ] && grep -q 'spawn:env\\|Claude Code PATH\\|spawn:path' ~/.bash_profile 2>/dev/null; then rm -f ~/.bash_profile; fi`,
`if command -v claude >/dev/null 2>&1; then ${finalize}; exit 0; fi`,
`echo "==> Installing Claude Code (method 1/2: curl installer)..."`,
"curl --proto '=https' -fsSL https://claude.ai/install.sh | bash || true",
"curl --proto '=https' -fsSL https://claude.ai/install.sh | bash >/dev/null 2>&1 || true",
`export PATH="${claudePath}:$PATH"`,
`if command -v claude >/dev/null 2>&1; then ${finalize}; exit 0; fi`,
"if ! command -v node >/dev/null 2>&1; then export N_PREFIX=$HOME/.n; curl --proto '=https' -fsSL https://raw.githubusercontent.com/tj/n/master/bin/n | bash -s install 22 || true; export PATH=$N_PREFIX/bin:$PATH; fi",

View file

@ -185,6 +185,8 @@ export function generateEnvConfig(pairs: string[]): string {
"",
"# [spawn:env]",
"export IS_SANDBOX='1'",
"# UTF-8 locale — required for agent TUIs that use Unicode (e.g. Claude Code)",
"export LANG='C.UTF-8'",
"# Ensure agent binaries are in PATH on reconnect",
'export PATH="$HOME/.npm-global/bin:$HOME/.bun/bin:$HOME/.local/bin:$HOME/.cargo/bin:$HOME/.claude/local/bin:/usr/local/bin:$PATH"',
];