anthropics / anthropics/claude-code

macOS Desktop: ShipIt auto-update relaunch double-encodes non-ASCII environment variables for the whole app tree (UTF-8 read as Latin-1)

オープン
#93,265 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る
area:desktop bug has repro platform:macos
主要言語
Python
スター
145k
フォーク
23.1k
PR マージ指標
PR 指標を取得中

説明

## Summary

On macOS, when Claude Desktop is relaunched by its Squirrel updater (`ShipIt`) after an auto-update, every environment variable whose value contains non-ASCII bytes arrives in the new process **double-encoded**: the original UTF-8 bytes are decoded as Latin-1 and re-encoded as UTF-8. The corrupted environment is then inherited by everything the app spawns, including Claude Code sessions in the Code tab and every shell the Bash tool runs.

launchd's stored value is *not* corrupted. Only the copy the relaunched app receives is. That is what makes this expensive to diagnose: `launchctl getenv` keeps showing the correct value while the tool running inside Claude Code fails on a path that "does not exist".

## Byte-level evidence

Variable set through a LaunchAgent, value containing CJK characters (`launchctl setenv MYVAR "/tmp/项目/x"`):

| where | bytes for `项目` | verdict |
|---|---|---|
| LaunchAgent plist on disk | `e9 a1 b9 e7 9b ae` | correct UTF-8 |
| `launchctl getenv MYVAR` | `e9 a1 b9 e7 9b ae` | correct |
| Terminal.app, launched normally | `e9 a1 b9 e7 9b ae` | correct |
| a second Electron app, launched via LaunchServices | `e9 a1 b9 e7 9b ae` | correct |
| **Claude.app, relaunched by ShipIt** | **`c3 a9 c2 a1 c2 b9 c3 a7 c2 9b c2 ae`** | **corrupted** |
| └─ Claude Helper | same corrupted bytes | inherited |
| └─ claude-code process | same corrupted bytes | inherited |
| └─ zsh spawned by the Bash tool | same corrupted bytes | inherited |

`e9` → `c3 a9`, `a1` → `c2 a1`, `b9` → `c2 b9` is exactly a Latin-1 decode followed by a UTF-8 encode. It is applied **exactly once**, at the moment `Claude.app` was exec'd. Child processes are not corrupted again on each spawn, so the damage is introduced by the relaunch itself, not by the app's process-spawning code.

The timing matches the updater:

```
Claude.app main process start: Wed Sep 9 03:31:55 2026
ShipIt_stderr.log: 2026-09-09 03:31:55.741 ShipIt[39126:4812207]
Successfully launched application at file:///Applications/Claude.app/
```

Control: a second Electron app on the same machine, reading the same launchd variable but launched normally through LaunchServices, received the correct bytes. Same source, different launcher, different result.

## Likely location

`Squirrel.framework/Resources/ShipIt` relaunches the app with `-[NSWorkspace launchApplicationAtURL:options:configuration:error:]` (that selector is present in the binary, alongside `processInfo`). If the configuration dictionary carries an `NSWorkspaceLaunchConfigurationEnvironment` built by reading the updater's own `environ` and decoding each byte as a character, that round trip produces exactly this fingerprint.

Suggested fix: do not hand the relaunch an environment dictionary at all, and let LaunchServices/launchd supply the environment the way a Dock launch does. If it must be passed explicitly, carry it as raw `char **` bytes and avoid any intermediate string decode.

Untested prediction that may help confirm the mechanism: if a user never manually quits the app, successive auto-updates should compound the encoding, producing `c3 83 c2 a9 …` after the second one. I only ever observed a single application, on a process whose parent had been launched cleanly from the Dock.

## Reproduction

1. `launchctl setenv MYVAR "/tmp/项目/x"` — any value with non-ASCII bytes works, CJK or accented Latin.
2. Quit Claude Desktop and relaunch it from the Dock. In the Code tab, `printenv MYVAR | xxd` shows the correct bytes.
3. Let the app auto-update, so ShipIt performs the relaunch.
4. In the Code tab afterwards, `printenv MYVAR | xxd` shows the double-encoded value.

The corruption persists for the lifetime of the process, so a session that lives through an auto-update keeps a corrupted environment indefinitely. Quitting and relaunching from the Dock clears it.

## Why it matters

The symptom that led me here: a CLI run from a Claude Code session printed

```
WARNING: proceeding, even though we could not create PATH aliases: CODEX_HOME points to
"/Volumes/.../Coding项ç\u{9b}®/..." but that path does not exist
```

while the identical command in Terminal.app worked, and `launchctl getenv CODEX_HOME` returned the correct path. Every diagnostic that looks at the configuration rather than at the running process's memory says the setup is fine.

Anyone whose home directory, project path, or any `PATH` entry contains non-ASCII characters is exposed, and the failure presents as a broken tool or a missing path rather than as a corrupted environment. Users in CJK and accented-Latin locales should hit this routinely.

Workaround for anyone who lands here: re-export the affected variables with correct bytes from `~/.zshenv`, so each shell overrides whatever it inherited, or quit the app and relaunch it from the Dock.

## Possibly related

`#92026` (UTF-8 CSV upload double-encodes Japanese text) and `#79482` (`/copy` writes the clipboard as Latin-1) look like the same decode-as-Latin-1 family surfacing elsewhere, though they are almost certainly different code paths from this one.

## Environment

- macOS 26.5.2 (25F84), arm64
- Claude Desktop 1.49585.0
- claude-code 2.1.260
- Squirrel.framework 1.0; `ShipIt` binary dated 2026-09-08

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

調査の方向性

Start by inspecting Squirrel.framework/Resources/ShipIt and the NSWorkspace launchApplicationAtURL:options:configuration:error: entry point identified in the report. Reproduce with launchctl setenv and a non-ASCII value, then compare bytes before and after an auto-update; done means the relaunched app and its child processes preserve the original UTF-8 bytes.

索引モデルが issue の本文から書いたものです。

評価

技術スタック
macos
領域
desktop-dev, operating-systems
issue の種類
バグ
難易度
5/5
見積もり時間
1週間以上
活発さ
活発
明瞭さ
おおむね明確
初心者へのやさしさ
35/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。