Skip to content

Fix: Windows terminal encoding set to UTF-8#46992

Open
slc03 wants to merge 3 commits intoopenclaw:mainfrom
slc03:feature-windows-encoding
Open

Fix: Windows terminal encoding set to UTF-8#46992
slc03 wants to merge 3 commits intoopenclaw:mainfrom
slc03:feature-windows-encoding

Conversation

@slc03
Copy link
Copy Markdown

@slc03 slc03 commented Mar 15, 2026

Title

Fix: Windows terminal encoding set to UTF-8

Summary

  • Problem: On Windows, commands executed via the OpenClaw exec tool could return garbled text because PowerShell/console encoding could fall back to a non-UTF-8 code page (for example GBK), while local terminal usage was UTF-8.
  • Why it matters: Users see inconsistent command output between local PowerShell and tool-executed commands, especially for non-ASCII text.
  • What changed: For Windows PowerShell execution, OpenClaw now initializes UTF-8 input/output encoding before running the requested command; this is applied in the exec runtime path (including PTY flow). Added tests for the command-wrapping behavior.
  • What did NOT change (scope boundary): No changes to tool permissions, approval policy logic, sandbox policy, command allowlist behavior, or non-Windows shell behavior.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #TBD
  • Related #TBD

User-visible / Behavior Changes

  • On Windows, exec tool command output is now consistently UTF-8 in PowerShell-based execution paths.
  • No new user-facing flags or config required.

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (Yes)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation:
    • Risk: Command execution now prepends a PowerShell UTF-8 initialization segment.
    • Mitigation: Applied only on Windows PowerShell shells; original command semantics are preserved; non-PowerShell and non-Windows paths are unchanged; tests were added for wrapper behavior.

Repro + Verification

Environment

  • OS: Windows
  • Runtime/container: Local Node runtime
  • Model/provider: N/A (tool runtime change)
  • Integration/channel (if any): N/A
  • Relevant config (redacted): Default exec setup

Steps

  1. Run an exec command that outputs non-ASCII text in OpenClaw on Windows.
  2. Compare output with the same command run directly in local PowerShell (UTF-8).
  3. Run focused tests for shell wrapper + exec PTY behavior.

Expected

  • OpenClaw exec output matches local PowerShell UTF-8 output without mojibake.

Actual

  • After fix: output is UTF-8-consistent and readable.
  • Before fix: output could be garbled under non-UTF-8 code page behavior.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios:
    • Windows exec path with PowerShell command wrapping enabled.
    • Existing focused tests pass after change.
  • Edge cases checked:
    • Non-Windows behavior remains unchanged.
    • Non-PowerShell shell names are not wrapped.
  • What you did not verify:
    • Full cross-channel end-to-end messaging flows unrelated to exec.
    • Performance benchmarking (not expected to be impacted materially).

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps:

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly:
    • Revert the Windows PowerShell UTF-8 command wrapping logic in the exec runtime path.
  • Files/config to restore:
    • Restore previous exec runtime + shell utility behavior from prior commit.
  • Known bad symptoms reviewers should watch for:
    • Missing output, command parsing errors in PowerShell, or changed behavior in chained commands.

Risks and Mitigations

  • Risk: Unexpected interaction with rare PowerShell profiles/environments.
    • Mitigation: Wrapper is explicit, minimal, and tested; applies only to Windows PowerShell execution path.
  • Risk: Behavioral differences for commands that depend on console code page side effects.
    • Mitigation: Scope is limited to UTF-8 normalization; rollback path is straightforward.

Implementation note: This change was implemented with GitHub Copilot (GPT-5.3-Codex).

@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: S labels Mar 15, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 15, 2026

Greptile Summary

This PR fixes inconsistent terminal encoding on Windows by prepending a short PowerShell UTF-8 initialization block ([Console]::OutputEncoding, $OutputEncoding, and chcp 65001) before every exec-tool command that runs through a PowerShell shell on win32. The change is narrowly scoped: non-Windows platforms and sandbox/Docker paths are untouched.

Key observations:

  • The wrapping function is correctly guarded by both a win32 platform check and a PowerShell shell-name check, so no unintended platforms or shells are affected.
  • runtimeCommand is consistently applied to both the regular child-process path and the PTY fallback path in bash-tools.exec-runtime.ts.
  • The semicolon-joined preamble works correctly for typical PowerShell commands, but there is a subtle invariant: user commands ending with a # comment are safe only because the user command is the last element — this ordering dependency should be made structurally explicit (e.g. using \n as the separator before the user command).
  • The new tests only exercise the wrapping code path on Windows runners; on Linux/macOS CI the first test trivially asserts the no-op path, leaving the UTF-8 initialization logic without cross-platform coverage.

Confidence Score: 4/5

  • Safe to merge with minor test-coverage and defensive-coding caveats noted.
  • The implementation is well-scoped, the logic is straightforward, and non-Windows/sandbox paths are untouched. The two style issues (trailing-comment ordering invariant, non-Windows CI coverage gap) do not affect correctness in current usage but are worth addressing before the pattern spreads.
  • src/agents/shell-utils.ts (comment-safe ordering invariant) and src/agents/shell-utils.test.ts (Windows-only coverage of wrapping logic).
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/shell-utils.test.ts
Line: 217-231

Comment:
**Test only covers wrapping logic on Windows**

On non-Windows CI, this test passes `"pwsh"` as the shell, but `isWindowsPowerShellShell` returns `false` immediately when `process.platform !== "win32"`. The function returns the command unchanged, and the test just asserts `wrapped === "Write-Output 'ok'"`. The actual UTF-8 initialization path (all five lines of setup code) is never exercised outside a Windows runner.

Consider splitting this into a platform-agnostic test that verifies the *output shape* of the wrapper directly (bypassing the `process.platform` guard), rather than relying on the test runner being on Windows to exercise that branch. For example, extracting a pure helper that builds the wrapped string and unit-testing it independently would let non-Windows CI catch regressions in the wrapper content.

As it stands, the test named *"wraps command with UTF-8 initialization on Windows PowerShell"* only truly tests that description on Windows; on all other platforms it silently tests the no-op path.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/shell-utils.ts
Line: 90-97

Comment:
**Trailing `#`-comment in user command could silently drop setup lines**

PowerShell treats `#` as a line comment — everything from `#` to the next newline is ignored. When all setup statements and the user command are joined with `"; "` into a single `-Command` string, there are no embedded newlines, so a user command that ends with a `#` comment (e.g. `Write-Output 'hello' # greet`) will cause the `#` to span to the *end of the entire joined string*. Because the user command is the last element in the join this is safe today, but it is a subtle invariant that must be maintained if the order of elements ever changes.

A more defensive approach would be to append the user command on its own line (using `\n` as the separator before it) rather than a semicolon, which would contain any trailing comment to the user's line:

```ts
return [
  "$__openclawUtf8 = [System.Text.UTF8Encoding]::new($false)",
  "[Console]::InputEncoding = $__openclawUtf8",
  "[Console]::OutputEncoding = $__openclawUtf8",
  "$OutputEncoding = $__openclawUtf8",
  "try { chcp 65001 > $null } catch {}",
].join("; ") + "\n" + command;
```

This makes the comment-safety property explicit and structural rather than reliant on ordering.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: 97ab01e

Comment on lines +217 to +231
it("wraps command with UTF-8 initialization on Windows PowerShell", () => {
const shell = process.platform === "win32" ? "powershell.exe" : "pwsh";
const wrapped = wrapCommandWithWindowsPowerShellUtf8("Write-Output 'ok'", shell);
const shouldWrap = process.platform === "win32";

if (shouldWrap) {
expect(wrapped).toContain("[Console]::OutputEncoding = $__openclawUtf8");
expect(wrapped).toContain("$OutputEncoding = $__openclawUtf8");
expect(wrapped).toContain("chcp 65001 > $null");
expect(wrapped).toContain("Write-Output 'ok'");
return;
}

expect(wrapped).toBe("Write-Output 'ok'");
});
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.

Test only covers wrapping logic on Windows

On non-Windows CI, this test passes "pwsh" as the shell, but isWindowsPowerShellShell returns false immediately when process.platform !== "win32". The function returns the command unchanged, and the test just asserts wrapped === "Write-Output 'ok'". The actual UTF-8 initialization path (all five lines of setup code) is never exercised outside a Windows runner.

Consider splitting this into a platform-agnostic test that verifies the output shape of the wrapper directly (bypassing the process.platform guard), rather than relying on the test runner being on Windows to exercise that branch. For example, extracting a pure helper that builds the wrapped string and unit-testing it independently would let non-Windows CI catch regressions in the wrapper content.

As it stands, the test named "wraps command with UTF-8 initialization on Windows PowerShell" only truly tests that description on Windows; on all other platforms it silently tests the no-op path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/shell-utils.test.ts
Line: 217-231

Comment:
**Test only covers wrapping logic on Windows**

On non-Windows CI, this test passes `"pwsh"` as the shell, but `isWindowsPowerShellShell` returns `false` immediately when `process.platform !== "win32"`. The function returns the command unchanged, and the test just asserts `wrapped === "Write-Output 'ok'"`. The actual UTF-8 initialization path (all five lines of setup code) is never exercised outside a Windows runner.

Consider splitting this into a platform-agnostic test that verifies the *output shape* of the wrapper directly (bypassing the `process.platform` guard), rather than relying on the test runner being on Windows to exercise that branch. For example, extracting a pure helper that builds the wrapped string and unit-testing it independently would let non-Windows CI catch regressions in the wrapper content.

As it stands, the test named *"wraps command with UTF-8 initialization on Windows PowerShell"* only truly tests that description on Windows; on all other platforms it silently tests the no-op path.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +90 to +97
return [
"$__openclawUtf8 = [System.Text.UTF8Encoding]::new($false)",
"[Console]::InputEncoding = $__openclawUtf8",
"[Console]::OutputEncoding = $__openclawUtf8",
"$OutputEncoding = $__openclawUtf8",
"try { chcp 65001 > $null } catch {}",
command,
].join("; ");
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.

Trailing #-comment in user command could silently drop setup lines

PowerShell treats # as a line comment — everything from # to the next newline is ignored. When all setup statements and the user command are joined with "; " into a single -Command string, there are no embedded newlines, so a user command that ends with a # comment (e.g. Write-Output 'hello' # greet) will cause the # to span to the end of the entire joined string. Because the user command is the last element in the join this is safe today, but it is a subtle invariant that must be maintained if the order of elements ever changes.

A more defensive approach would be to append the user command on its own line (using \n as the separator before it) rather than a semicolon, which would contain any trailing comment to the user's line:

return [
  "$__openclawUtf8 = [System.Text.UTF8Encoding]::new($false)",
  "[Console]::InputEncoding = $__openclawUtf8",
  "[Console]::OutputEncoding = $__openclawUtf8",
  "$OutputEncoding = $__openclawUtf8",
  "try { chcp 65001 > $null } catch {}",
].join("; ") + "\n" + command;

This makes the comment-safety property explicit and structural rather than reliant on ordering.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/shell-utils.ts
Line: 90-97

Comment:
**Trailing `#`-comment in user command could silently drop setup lines**

PowerShell treats `#` as a line comment — everything from `#` to the next newline is ignored. When all setup statements and the user command are joined with `"; "` into a single `-Command` string, there are no embedded newlines, so a user command that ends with a `#` comment (e.g. `Write-Output 'hello' # greet`) will cause the `#` to span to the *end of the entire joined string*. Because the user command is the last element in the join this is safe today, but it is a subtle invariant that must be maintained if the order of elements ever changes.

A more defensive approach would be to append the user command on its own line (using `\n` as the separator before it) rather than a semicolon, which would contain any trailing comment to the user's line:

```ts
return [
  "$__openclawUtf8 = [System.Text.UTF8Encoding]::new($false)",
  "[Console]::InputEncoding = $__openclawUtf8",
  "[Console]::OutputEncoding = $__openclawUtf8",
  "$OutputEncoding = $__openclawUtf8",
  "try { chcp 65001 > $null } catch {}",
].join("; ") + "\n" + command;
```

This makes the comment-safety property explicit and structural rather than reliant on ordering.

How can I resolve this? If you propose a fix, please make it concise.

xuwei-xy pushed a commit to xuwei-xy/openclaw that referenced this pull request Mar 15, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa954184e7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +95 to +97
"try { chcp 65001 > $null } catch {}",
command,
].join("; ");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve directive-first PowerShell commands

Prepending the UTF-8 setup statements changes parsing rules for commands that must begin at the first statement in a PowerShell script context (for example param(...) blocks or #requires directives). With this wrapper, those commands are no longer first and can fail on Windows even though they worked before, so exec behavior is no longer command-semantic-preserving for a real class of scripts.

Useful? React with 👍 / 👎.

@easyteacher
Copy link
Copy Markdown

I have an alternate solution: #52154

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants