# NousResearch/hermes-agent · TUI clipboard CJK/UTF-8 encoding fixes for PowerShell/WSL2

## Open now

- Nothing open on GitHub.

A bug report and multiple fix attempts addressing garbled CJK and emoji characters when copying text to the clipboard via PowerShell in TUI, particularly on WSL2. The fixes explore approaches such as base64 encoding to preserve UTF-8 integrity.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #35222. Verify it addresses your case.
- **Recommended:** read PR #35222 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-35222`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.50`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #35107: /copy in TUI produces garbled CJK text on WSL2

## Prior & competing PRs

- **#35122: fix(tui): preserve utf8 when copying via powershell**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#35124`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#35222: fix(tui): preserve UTF-8 in PowerShell clipboard writes (CJK/emoji)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `ui-tui/src/__tests__/clipboard.test.ts`, `ui-tui/src/lib/clipboard.ts`
  - fix hunks:
  - `ui-tui/src/__tests__/clipboard.test.ts`

    ```diff
    @@ -269,7 +269,14 @@ describe('writeClipboardText', () => {
           expect.arrayContaining(['-NoProfile', '-NonInteractive']),
           expect.anything()
         )
    -    expect(stdin.end).toHaveBeenCalledWith('wsl text')
    +    // PowerShell uses base64-encoded UTF-8 via command argument, not stdin
    +    expect(stdin.end).not.toHaveBeenCalled()
    +    const calledArgs = start.mock.calls[0][1] as string[]
    +    const commandIdx = calledArgs.indexOf('-Command')
    +    expect(commandIdx).toBeGreaterThan(-1)
    +    const script = calledArgs[commandIdx + 1]
    +    expect(script).toContain('FromBase64String')
    +    expect(script).toContain(Buffer.from('wsl text', 'utf8').toString('base64'))
       })
     
       it('prefers the Windows clipboard path over wl-copy inside WSLg', async () => {
    ```

    ```diff
    @@ -300,7 +307,13 @@ describe('writeClipboardText', () => {
           expect.arrayContaining(['-NoProfile', '-NonInteractive']),
           expect.anything()
         )
    -    expect(stdin.end).toHaveBeenCalledWith('wslg text')
    +    // PowerShell uses base64-encoded UTF-8 via command argument, not stdin
    +    expect(stdin.end).not.toHaveBeenCalled()
    +    const calledArgs = start.mock.calls[0][1] as string[]
    +    const commandIdx = calledArgs.indexOf('-Command')
    +    const script = calledArgs[commandIdx + 1]
    +    expect(script).toContain('FromBase64String')
    +    expect(script).toContain(Buffer.from('wslg text', 'utf8').toString('base64'))
       })
     
       it('uses PowerShell on Windows', async () => {
    ```

    ```diff
    @@ -325,5 +338,32 @@ describe('writeClipboardText', () => {
           expect.arrayContaining(['-NoProfile', '-NonInteractive']),
           expect.anything()
         )
    +    // PowerShell uses base64-encoded UTF-8 via command argument, not stdin
    +    expect(stdin.end).not.toHaveBeenCalled()
    +  })
    +
    +  it('preserves CJK text via base64 encoding in PowerShell on WSL', async () => {
    +    const stdin = { end: vi.fn() }
    +
    +    const child = {
    +      once: vi.fn((event: string, cb: (code?: number) => void) => {
    +        if (event === 'close') {
    +          cb(0)
    +        }
    +
    +        return child
    +      }),
    +      stdin
    +    }
    +
    +    const start = vi.fn().mockReturnValue(child)
    +    const cjkText = '你好世界，测试中文 🎉'
    +
    +    await expect(writeClipboardText(cjkText, 'linux', start as any, { WSL_INTEROP: '/tmp/socket' })).resolves.toBe(true)
    +    const calledArgs = start.mock.calls[0][1] as string[]
    +    const commandIdx = calledArgs.indexOf('-Command')
    +    const script = calledArgs[commandIdx + 1]
    +    expect(script).toContain(Buffer.from(cjkText, 'utf8').toString('base64'))
    +    expect(script).toContain('UTF8.GetString')
       })
     })
    ```
  - `ui-tui/src/lib/clipboard.ts`

    ```diff
    @@ -91,33 +91,42 @@ export async function readClipboardText(
       return null
     }
     
    +// PowerShell on Windows/WSL decodes piped stdin with the system ANSI code
    +// page (e.g. CP936), not UTF-8, so $input-based writes mangle CJK/emoji. We
    +// instead base64-encode the UTF-8 bytes and pass them as a -Command argument,
    +// decoding with UTF8.GetString — this removes the stdin-encoding variable
    +// entirely (also immune to BOM injection on redirect). PowerShell entries set
    +// stdin=false; every other backend reads UTF-8 stdin natively.
    +type WriteCmd = { args: readonly string[]; cmd: string; stdin: boolean }
    +
    +function _powershellWriteScript(b64: string): string {
    +  return `Set-Clipboard -Value ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64}')))`
    +}
    +
     function writeClipboardCommands(
       platform: NodeJS.Platform,
       env: NodeJS.ProcessEnv
    -): Array<{ args: readonly string[]; cmd: string }> {
    +): WriteCmd[] {
       if (platform === 'darwin') {
    -    return [{ cmd: 'pbcopy', args: [] }]
    +    return [{ cmd: 'pbcopy', args: [], stdin: true }]
       }
     
       if (platform === 'win32') {
    -    return [{ cmd: 'powershell', args: ['-NoProfile', '-NonInteractive', '-Command', 'Set-Clipboard -Value $input'] }]
    +    return [{ cmd: 'powershell', args: ['-NoProfile', '-NonInteractive'], stdin: false }]
       }
     
    -  const attempts: Array<{ args: readonly string[]; cmd: string }> = []
    +  const attempts: WriteCmd[] = []
     
       if (env.WSL_INTEROP || env.WSL_DISTRO_NAME) {
    -    attempts.push({
    -      cmd: 'powershell.exe',
    -      args: ['-NoProfile', '-NonInteractive', '-Command', 'Set-Clipboard -Value $input']
    -    })
    +    attempts.push({ cmd: 'powershell.exe', args: ['-NoProfile', '-NonInteractive'], stdin: false })
       }
     
       if (env.WAYLAND_DISPLAY) {
    -    attempts.push({ cmd: 'wl-copy', args: ['--type', 'text/plain'] })
    +    attempts.push({ cmd: 'wl-copy', args: ['--type', 'text/plain'], stdin: true })
       }
     
    -  attempts.push({ cmd: 'xclip', args: ['-selection', 'clipboard', '-in'] })
    -  attempts.push({ cmd: 'xsel', args: ['--clipboard', '--input'] })
    +  attempts.push({ cmd: 'xclip', args: ['-selection', 'clipboard', '-in'], stdin: true })
    +  attempts.push({ cmd: 'xsel', args: ['--clipboard', '--input'], stdin: true })
     
       return attempts
     }
    ```

    ```diff
    @@ -144,14 +153,21 @@ export async function writeClipboardText(
     ): Promise<boolean> {
       const candidates = writeClipboardCommands(platform, env)
     
    -  for (const { cmd, args } of candidates) {
    +  for (const cmdEntry of candidates) {
         try {
           const ok = await new Promise<boolean>(resolve => {
    -        const child = start(cmd, [...args], { stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true })
    -
    -        child.once('error', () => resolve(false))
    -        child.once('close', code => resolve(code === 0))
    -        child.stdin?.end(text)
    +        if (cmdEntry.stdin) {
    +          const child = start(cmdEntry.cmd, [...cmdEntry.args], { stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true })
    +          child.once('error', () => resolve(false))
    +          child.once('close', (code: number | null) => resolve(code === 0))
    +          child.stdin?.end(text)
    +        } else {
    +          const b64 = Buffer.from(text, 'utf8').toString('base64')
    +          const script = _powershellWriteScript(b64)
    +          const child = start(cmdEntry.cmd, [...cmdEntry.args, '-Command', script], { stdio: ['ignore', 'ignore', 'ignore'], windowsHide: true })
    +          child.once('error', () => resolve(false))
    +          child.once('close', (code: number | null) => resolve(code === 0))
    +        }
           })
     
           if (ok) {
    ```
- **#35124: fix(tui): use base64 encoding for PowerShell clipboard writes to preserve UTF-8**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#35222`

## Fix status

- Merged to main in PR #35222 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-30`.
