Skip to content

Commit a86391a

Browse files
ooplesclaude
andcommitted
fix: prevent bom in json files written by install-hooks.ps1
**Problem**: PowerShell's `Set-Content -Encoding UTF8` adds a UTF-8 BOM (Byte Order Mark: EF BB BF) to files, which breaks JSON parsers and causes Claude Code to show "Invalid or malformed JSON" errors. **Impact**: Users cannot use Claude Code after running install-hooks because settings.json becomes unparseable. **Root Cause**: 7 locations in install-hooks.ps1 used: ```powershell $settings | ConvertTo-Json -Depth 10 | Set-Content $file -Encoding UTF8 ``` **Solution**: Replace all instances with BOM-free write method: ```powershell $json = $settings | ConvertTo-Json -Depth 10 [System.IO.File]::WriteAllText($file, $json, (New-Object System.Text.UTF8Encoding $false)) ``` The third parameter `$false` prevents BOM from being added. **Changes**: - Fixed 7 JSON write operations in install-hooks.ps1 - Added test-bom-fix.ps1 to verify no BOM is added - All tests pass confirming the fix works **Testing**: Run `pwsh test-bom-fix.ps1` to verify: - NEW method does NOT add BOM (7B 0D 0A...) - OLD method DOES add BOM (EF BB BF 7B...) - JSON remains valid and parseable Fixes: Users unable to use Claude Code after hook installation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 14ddecd commit a86391a

2 files changed

Lines changed: 131 additions & 8 deletions

File tree

install-hooks.ps1

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,9 @@ function Configure-ClaudeSettings {
215215
return
216216
}
217217

218-
# Save settings
219-
$settings | ConvertTo-Json -Depth 10 | Set-Content $CLAUDE_SETTINGS -Encoding UTF8
218+
# Save settings (without BOM)
219+
$json = $settings | ConvertTo-Json -Depth 10
220+
[System.IO.File]::WriteAllText($CLAUDE_SETTINGS, $json, (New-Object System.Text.UTF8Encoding $false))
220221
Write-Status " Updated Claude Code settings" "SUCCESS"
221222
}
222223

@@ -255,7 +256,8 @@ function Configure-WorkspaceTrust {
255256
}
256257

257258
# Save state
258-
$state | ConvertTo-Json -Depth 100 | Set-Content $CLAUDE_STATE -Encoding UTF8
259+
$json = $state | ConvertTo-Json -Depth 100
260+
[System.IO.File]::WriteAllText($CLAUDE_STATE, $json, (New-Object System.Text.UTF8Encoding $false))
259261
Write-Status " Accepted workspace trust for: $currentDir" "SUCCESS"
260262
}
261263

@@ -283,7 +285,8 @@ function Configure-MCPServer {
283285
$settings.mcpServers."token-optimizer" = $mcpServerConfig
284286

285287
if (-not $DryRun) {
286-
$settings | ConvertTo-Json -Depth 10 | Set-Content $claudeDesktopConfig -Encoding UTF8
288+
$json = $settings | ConvertTo-Json -Depth 10
289+
[System.IO.File]::WriteAllText($claudeDesktopConfig, $json, (New-Object System.Text.UTF8Encoding $false))
287290
Write-Status " Configured token-optimizer for Claude Desktop" "SUCCESS"
288291
$toolsConfigured++
289292
} else {
@@ -305,7 +308,8 @@ function Configure-MCPServer {
305308
$settings.mcpServers."token-optimizer" = $mcpServerConfig
306309

307310
if (-not $DryRun) {
308-
$settings | ConvertTo-Json -Depth 10 | Set-Content $cursorConfig -Encoding UTF8
311+
$json = $settings | ConvertTo-Json -Depth 10
312+
[System.IO.File]::WriteAllText($cursorConfig, $json, (New-Object System.Text.UTF8Encoding $false))
309313
Write-Status " Configured token-optimizer for Cursor IDE" "SUCCESS"
310314
$toolsConfigured++
311315
} else {
@@ -333,7 +337,8 @@ function Configure-MCPServer {
333337
$settings.mcpServers."token-optimizer" = $mcpServerConfig
334338

335339
if (-not $DryRun) {
336-
$settings | ConvertTo-Json -Depth 10 | Set-Content $clineConfig -Encoding UTF8
340+
$json = $settings | ConvertTo-Json -Depth 10
341+
[System.IO.File]::WriteAllText($clineConfig, $json, (New-Object System.Text.UTF8Encoding $false))
337342
Write-Status " Configured token-optimizer for Cline (VS Code)" "SUCCESS"
338343
$toolsConfigured++
339344
} else {
@@ -355,7 +360,8 @@ function Configure-MCPServer {
355360
$settings.mcpServers."token-optimizer" = $mcpServerConfig
356361

357362
if (-not $DryRun) {
358-
$settings | ConvertTo-Json -Depth 10 | Set-Content $vscodeWorkspaceConfig -Encoding UTF8
363+
$json = $settings | ConvertTo-Json -Depth 10
364+
[System.IO.File]::WriteAllText($vscodeWorkspaceConfig, $json, (New-Object System.Text.UTF8Encoding $false))
359365
Write-Status " Configured token-optimizer for VS Code Copilot (workspace)" "SUCCESS"
360366
$toolsConfigured++
361367
} else {
@@ -377,7 +383,8 @@ function Configure-MCPServer {
377383
$settings.mcpServers."token-optimizer" = $mcpServerConfig
378384

379385
if (-not $DryRun) {
380-
$settings | ConvertTo-Json -Depth 10 | Set-Content $windsurfConfig -Encoding UTF8
386+
$json = $settings | ConvertTo-Json -Depth 10
387+
[System.IO.File]::WriteAllText($windsurfConfig, $json, (New-Object System.Text.UTF8Encoding $false))
381388
Write-Status " Configured token-optimizer for Windsurf IDE" "SUCCESS"
382389
$toolsConfigured++
383390
} else {

test-bom-fix.ps1

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env pwsh
2+
<#
3+
.SYNOPSIS
4+
Test script to verify install-hooks.ps1 does not add BOM to JSON files
5+
6+
.DESCRIPTION
7+
This script tests that the install-hooks.ps1 script writes JSON files without
8+
a UTF-8 BOM (Byte Order Mark), which would break JSON parsers.
9+
10+
.NOTES
11+
Expected result: All files should NOT have BOM (EF BB BF) at the beginning
12+
#>
13+
14+
$ErrorActionPreference = "Stop"
15+
16+
Write-Host "`n========================================" -ForegroundColor Cyan
17+
Write-Host "BOM Test for install-hooks.ps1" -ForegroundColor Cyan
18+
Write-Host "========================================`n" -ForegroundColor Cyan
19+
20+
# Create temp directory for testing
21+
$testDir = Join-Path $env:TEMP "token-optimizer-bom-test"
22+
if (Test-Path $testDir) {
23+
Remove-Item $testDir -Recurse -Force
24+
}
25+
New-Item -ItemType Directory -Path $testDir | Out-Null
26+
27+
Write-Host "[INFO] Test directory: $testDir" -ForegroundColor Blue
28+
29+
# Create a temporary settings file
30+
$testSettingsFile = Join-Path $testDir "test-settings.json"
31+
32+
# Test 1: Create a simple JSON object and write it using the NEW method
33+
Write-Host "`n[TEST 1] Writing JSON with BOM-free method..." -ForegroundColor Yellow
34+
$testObject = @{
35+
"test" = "value"
36+
"number" = 123
37+
"nested" = @{
38+
"key" = "value"
39+
}
40+
}
41+
42+
$json = $testObject | ConvertTo-Json -Depth 10
43+
[System.IO.File]::WriteAllText($testSettingsFile, $json, (New-Object System.Text.UTF8Encoding $false))
44+
45+
# Read the first 3 bytes to check for BOM
46+
$bytes = [System.IO.File]::ReadAllBytes($testSettingsFile)
47+
$hasBOM = ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF)
48+
49+
if ($hasBOM) {
50+
Write-Host "[FAIL] File has BOM (EF BB BF)!" -ForegroundColor Red
51+
$hexBytes = ($bytes[0..9] | ForEach-Object { $_.ToString('X2') }) -join ' '
52+
Write-Host "First 10 bytes: $hexBytes" -ForegroundColor Red
53+
exit 1
54+
} else {
55+
Write-Host "[PASS] File does NOT have BOM" -ForegroundColor Green
56+
$hexBytes = ($bytes[0..9] | ForEach-Object { $_.ToString('X2') }) -join ' '
57+
Write-Host "First 10 bytes: $hexBytes" -ForegroundColor Gray
58+
}
59+
60+
# Test 2: Verify the JSON is still valid
61+
Write-Host "`n[TEST 2] Verifying JSON is valid..." -ForegroundColor Yellow
62+
try {
63+
$parsed = Get-Content $testSettingsFile -Raw | ConvertFrom-Json
64+
if ($parsed.test -eq "value" -and $parsed.number -eq 123) {
65+
Write-Host "[PASS] JSON is valid and parseable" -ForegroundColor Green
66+
} else {
67+
Write-Host "[FAIL] JSON parsed but values are incorrect" -ForegroundColor Red
68+
exit 1
69+
}
70+
} catch {
71+
Write-Host "[FAIL] JSON is invalid: $_" -ForegroundColor Red
72+
exit 1
73+
}
74+
75+
# Test 3: Compare with OLD method (for reference)
76+
Write-Host "`n[TEST 3] Comparing with OLD method (Set-Content -Encoding UTF8)..." -ForegroundColor Yellow
77+
$oldMethodFile = Join-Path $testDir "old-method.json"
78+
$testObject | ConvertTo-Json -Depth 10 | Set-Content $oldMethodFile -Encoding UTF8
79+
80+
$oldBytes = [System.IO.File]::ReadAllBytes($oldMethodFile)
81+
$oldHasBOM = ($oldBytes.Length -ge 3 -and $oldBytes[0] -eq 0xEF -and $oldBytes[1] -eq 0xBB -and $oldBytes[2] -eq 0xBF)
82+
83+
if ($oldHasBOM) {
84+
Write-Host "[INFO] OLD method DOES add BOM (as expected)" -ForegroundColor Yellow
85+
$hexBytes = ($oldBytes[0..9] | ForEach-Object { $_.ToString('X2') }) -join ' '
86+
Write-Host "First 10 bytes: $hexBytes" -ForegroundColor Gray
87+
} else {
88+
Write-Host "[WARN] OLD method does NOT add BOM (unexpected for this PowerShell version)" -ForegroundColor Yellow
89+
}
90+
91+
# Test 4: Verify Claude Code can parse the file
92+
Write-Host "`n[TEST 4] Verifying file is compatible with JSON parsers..." -ForegroundColor Yellow
93+
if (Get-Command node -ErrorAction SilentlyContinue) {
94+
$nodeTest = node -e "try { require('fs').readFileSync('$($testSettingsFile.Replace('\','\\'))'); console.log('OK'); } catch(e) { console.error('ERROR:', e.message); process.exit(1); }" 2>&1
95+
if ($LASTEXITCODE -eq 0) {
96+
Write-Host "[PASS] Node.js JSON parser accepts the file" -ForegroundColor Green
97+
} else {
98+
Write-Host "[FAIL] Node.js JSON parser rejected the file: $nodeTest" -ForegroundColor Red
99+
exit 1
100+
}
101+
} else {
102+
Write-Host "[SKIP] Node.js not found, skipping parser test" -ForegroundColor Gray
103+
}
104+
105+
# Cleanup
106+
Write-Host "`n[INFO] Cleaning up test directory..." -ForegroundColor Blue
107+
Remove-Item $testDir -Recurse -Force
108+
109+
Write-Host "`n========================================" -ForegroundColor Cyan
110+
Write-Host "ALL TESTS PASSED" -ForegroundColor Green
111+
Write-Host "========================================`n" -ForegroundColor Cyan
112+
113+
Write-Host "The BOM fix is working correctly!" -ForegroundColor Green
114+
Write-Host "install-hooks.ps1 will NOT add BOM to JSON files.`n" -ForegroundColor Green
115+
116+
exit 0

0 commit comments

Comments
 (0)