fix(windows): strip UTF-8 BOM in all settings.json readers (#3013)#3033
fix(windows): strip UTF-8 BOM in all settings.json readers (#3013)#3033Neclode wants to merge 1 commit into
Conversation
…k#3013) On Windows, external tooling (PowerShell 5.1, some editors/formatters) rewrites ~/.claude-mem/settings.json with a UTF-8 BOM (EF BB BF). Node/Bun decode it to U+FEFF at offset 0 on `utf-8` read, and JSON.parse rejects it, so the worker fails to load config on every hook — eventually tripping CAPTURE_BROKEN and disabling capture entirely. BOM stripping had been added to exactly one of seven readers (SettingsDefaultsManager). The other six still parsed raw and threw, including logger.getLevel() — the source of the "Failed to load log level from settings" error in the report. Our own writes use writeFileSync(..., 'utf-8'), which never emits a BOM, so a write-side fix is a no-op and would not repair already-BOM'd files on disk. The fix is read-side resilience. - add shared stripBom() to utils/json-utils and route readJsonSafe through it - strip BOM in the six remaining settings.json readers - collapse SettingsDefaultsManager's inline BOM regex onto the shared helper - drop a dead logger import so json-utils stays an import-cycle-free leaf - test: readJsonSafe parses a BOM-prefixed settings.json
Greptile SummaryThis PR fixes a Windows-only crash where PowerShell 5.1 (and some editors) rewrites
Confidence Score: 5/5Safe to merge — the change is additive, touches only the string passed to JSON.parse, and has no effect on BOM-free files. All seven settings readers are covered, the import-cycle risk is explicitly resolved by removing the old logger import from json-utils, the implementation is a single well-understood character comparison, and the new test reproduces the exact PowerShell BOM scenario described in the bug report. The only finding is a cosmetic test constant that could be silently stripped by tooling. tests/utils/json-utils.test.ts — the literal U+FEFF constant is worth replacing with '\uFEFF' to guard against invisible-character stripping. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["settings.json on disk\n(may have UTF-8 BOM\nfrom PowerShell / editor)"] --> B["readFileSync(..., 'utf-8')"]
B --> C["stripBom(text)\ncharCodeAt(0) === 0xFEFF\n→ slice(1) : text"]
C --> D["JSON.parse(stripped)"]
D --> E1["logger.getLevel()"]
D --> E2["paths.resolveDataDir()"]
D --> E3["plugin-state\nisPluginDisabled()"]
D --> E4["SettingsDefaultsManager\nloadSettings()"]
D --> E5["SettingsRoutes\nGET /settings"]
D --> E6["server-beta-bootstrap\npersistServerBetaSettings()"]
D --> E7["server.ts\nrunServerBetaKeysRotateCommand()"]
D --> E8["readJsonSafe()\n(generic helper)"]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["settings.json on disk\n(may have UTF-8 BOM\nfrom PowerShell / editor)"] --> B["readFileSync(..., 'utf-8')"]
B --> C["stripBom(text)\ncharCodeAt(0) === 0xFEFF\n→ slice(1) : text"]
C --> D["JSON.parse(stripped)"]
D --> E1["logger.getLevel()"]
D --> E2["paths.resolveDataDir()"]
D --> E3["plugin-state\nisPluginDisabled()"]
D --> E4["SettingsDefaultsManager\nloadSettings()"]
D --> E5["SettingsRoutes\nGET /settings"]
D --> E6["server-beta-bootstrap\npersistServerBetaSettings()"]
D --> E7["server.ts\nrunServerBetaKeysRotateCommand()"]
D --> E8["readJsonSafe()\n(generic helper)"]
Reviews (1): Last reviewed commit: "fix(windows): strip UTF-8 BOM in all set..." | Re-trigger Greptile |
| import { tmpdir } from 'os'; | ||
| import { readJsonSafe, stripBom } from '../../src/utils/json-utils.js'; | ||
|
|
||
| const BOM = ''; |
There was a problem hiding this comment.
The BOM constant is defined using a literal U+FEFF character, which is invisible in virtually every editor and can be silently stripped by formatters, linters, or copy-paste. If it disappears,
stripBom tests pass vacuously — the "removes a leading BOM" case would compare '{"a":1}' to '{"a":1}' and still succeed, hiding a broken implementation. Using '\uFEFF' makes the intent explicit and immune to tool interference.
| const BOM = ''; | |
| const BOM = '\uFEFF'; |
Fixes #3013.
Problem. On Windows, external tooling (PowerShell 5.1, some editors/formatters) rewrites
~/.claude-mem/settings.jsonwith a UTF-8 BOM (EF BB BF). Node/Bun decode it toU+FEFFat offset 0 onutf-8read, andJSON.parserejects it — so the worker fails to load config on every hook, eventually trippingCAPTURE_BROKENand disabling capture entirely.Root cause. BOM stripping was added to exactly one of seven settings readers (
SettingsDefaultsManager). The other six still parsed raw and threw — includinglogger.getLevel(), which is the source of theFailed to load log level from settingserror in the report.Why read-side, not write-side. Our own writes use
writeFileSync(..., 'utf-8'), which never emits a BOM — so a write-side fix is a no-op and wouldn't repair the already-BOM'd files sitting on users' disks. The fix has to be read-side resilience.Changes
stripBom()toutils/json-utilsand routereadJsonSafethrough itsettings.jsonreaders (logger,paths,plugin-state, npxserver,server-beta-bootstrap,SettingsRoutes)SettingsDefaultsManager's inline BOM regex onto the shared helperloggerimport sojson-utilsstays an import-cycle-free leafreadJsonSafeparses a BOM-prefixedsettings.jsonVerification:
bun test39/39 pass ·tsc --noEmitclean.