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)
- Langage dominant
- Python
- Étoiles
- 145k
- Forks
- 23.1k
- Métriques de merge des PR
- Métriques de PR en attente
Description
## 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
Guide de contribution
Aucun guide de contribution indexé pour ce dépôt
Piste de recherche
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.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- macos
- Domaine
- desktop-dev, operating-systems
- Type d'issue
- Bug
- Difficulté
- 5/5
- Temps estimé
- Plus d'une semaine
- Activité
- Active
- Clarté
- Plutôt claire
- Accessibilité débutants
- 35/100