May 29, 2026·13 min read

Debugging Claude Code's Blank Panel in Safari and iPad

Two upstream bugs, one blank panel. Root cause and the one-byte fix for the Claude Code VS Code extension on WebKit.

Status: Resolved (May 2026). Two independent upstream bugs, one version pin, and a one-byte patch to a minified bundle.

Affected surface: The Claude Code VS Code extension panel — and any other webview-heavy extension sidebar — when accessing code-server from any WebKit-based browser. That means Mac Safari, iPad Safari, iPhone Safari, and iOS Chrome/Brave/Firefox (which are all forced to use WebKit by Apple's App Store policy).

Not affected: Chrome, Chromium, Brave, Edge on macOS/Linux/Windows (all use Blink). Firefox on desktop (Gecko).


While working on my containerized remote dev environments, I ran into this issue with the Claude Code VS Code extension. The symptom is always the same: Claude Code activates, the panel slot opens, and then nothing renders. No login prompt, no chat UI, no spinner. It looks like the extension is "loading forever" or doing nothing. No visible error — you need Safari Web Inspector open to see what's actually happening.

What's actually happening is two completely independent bugs that compound each other.

Bug #1 (Anthropic): Claude Code 2.1.78+ regressed the webview init handshake. The extension sends init and gets no response, and the panel stays blank on every browser — not just WebKit. Tracked in anthropics/claude-code#45729. Last known working version: 2.1.77. The fix: pin to 2.1.77.

Bug #2 (Microsoft, shipped inside code-server): VS Code's VSBuffer.prototype.slice was changed from a copying .slice() to a non-copying .subarray() for performance (microsoft/vscode#76076). The optimization is correct for the common case. It's wrong when the underlying ArrayBuffer gets transferred to another execution context via postMessage — which code-server does constantly inside its iframe-based extension host architecture. On V8 (Chrome), operating on a detached buffer view fails silently with an empty result. On JavaScriptCore (Safari, all iOS browsers), it throws Underlying ArrayBuffer has been detached from the view or out-of-bounds, which kills the IPC message channel. The fix: a single character change in the minified bundle — subarrayslice.

You need both fixes. Clearing bug #1 reveals bug #2 underneath.


Symptoms

Open code-server in Safari or on iPad. Sign in. The main UI loads — file tree, editor, terminal all work. Click the Claude Code icon in the activity bar. The panel slot opens. Nothing renders.

No spinner. No error. Just the empty background.

The extension is actually doing work. It's just that the IPC channel dies before the webview content gets through. From Safari's Web Inspector:

[Log] [Extension Host] Extension activated!
[Log] [Extension Host] Created lock file at /home/[user]/.claude/ide/<port>.lock
[Log] [Extension Host] Set CLAUDE_CODE_SSE_PORT=<port> in terminal environment (in-memory)
[Error]   ERR – "Underlying ArrayBuffer has been detached from the view or out-of-bounds:
  subarray@[native code]
  slice@…/workbench.js:406:97282
  read@…/workbench.js:1032:107875
  A9e@…/workbench.js:1032:108819
  onRawMessage@…/workbench.js:1032:109928

  acceptChunk@…/workbench.js:1034:6504
  …"

Multiple errors fire in cascade — one per IPC chunk on the management socket, then one per chunk on the extension-host socket. After that, no further messages get delivered.

The extension activated. It wrote the lock file. It set the SSE port. Then the first binary IPC chunk that needed to be split by ChunkStream._read exploded, and everything downstream was dead.


Diagnostic journey

This took several rounds. The dead ends are worth documenting because they're easy to repeat.

Round 1 — assumed this was iPad-specific

First hypothesis: an iPad WebKit quirk specific to Claude Code. We confirmed Mac Chrome (Blink) worked and updated Claude Code to the latest CLI version, expecting the newest VSIX to be fine. Neither helped, but "it works on Chrome, broken on iPad" reinforced an "iPad-only WebKit quirk" hypothesis that turned out to be wrong — we just hadn't tested Mac Safari yet.

Round 2 — read the HAR

Exported a HAR from the iPad Safari network panel. Key finding:

GET .../webview/browser/pre/index.html?...&extensionId=Anthropic.claude-code   → 200 OK
GET .../vscode-remote-resource?path=…claude-logo.svg                           → 200 OK

The webview iframe HTML was loading. The extension was registering. The Claude logo was loading. The failure was inside webview content delivery, not at the network layer. This ruled out TLS/cert issues (several hours had been spent there), Caddy proxy misbehavior, and the extension failing to install or activate.

Round 3 — searched the issue tracker

Searched GitHub for "claude code" extension webview ipad safari blank. Found:

  • anthropics/claude-code#45729"Claude Code for VS Code hangs in 2.1.78+", open as of May 2026. Confirmed on macOS, Windows, WSL. Last known working: 2.1.77. Symptom: extension activates, sends init, gets no response.
  • anthropics/claude-code#13130 — webview blank after auto-update due to stale extension directories accumulating across versions. Different cause, same surface symptom.

We were on 2.1.89 — squarely in the broken range. Pinned to 2.1.77 from Open VSX's linux-x64 build.

Partially fixed: on iPad Safari, the panel now showed the Anthropic logo with a spinner and a blue progress bar, then went blank. Different failure point — we'd cleared bug #1 and exposed a different error underneath. The console showed the ArrayBuffer detached cascade. That was bug #2.

Round 4 — bypassed the reverse proxy to rule it out

code-server was fronted by a Caddy reverse proxy (real Let's Encrypt cert via Route 53 DNS-01). Two TLS hops and two WebSocket frame layers — a plausible source of binary-frame corruption.

Tested direct-IP HTTPS to the code-server port, bypassing Caddy entirely. Same error, same stack trace, same column numbers. Caddy was innocent.

Round 5 — Mac Safari with full Web Inspector

We'd only tested iPad. Updated the Caddy landing pages to include a direct link into code-server (they previously only had "Add to Home Screen" instructions, leaving Mac Safari with no path through to the login screen).

Results:

  • Mac Chrome ✓
  • Mac Safari ✗ — same ArrayBuffer detached errors as iPad
  • iPad Safari ✗
  • iPad Chrome ✗ (iOS forces WebKit regardless of browser branding)

This changed the classification from "iPad mobile WebKit quirk" to "all of WebKit." Mac Safari has full DevTools.

Round 6 — read the minified bundle

Found the offending line by extracting it from the running container:

ssh [user]@[server] 'docker exec <container> bash -c \
  "sed -n 406p /usr/lib/code-server/lib/vscode/out/vs/code/browser/workbench/workbench.js \
   | cut -c 96900-97500"'

Returned (abbreviated):

slice(i,e){return new a(this.buffer.subarray(i,e))}

That's the minified VSBuffer.prototype.slice. It claims to return a sliced buffer but actually returns a new VSBuffer wrapping a view into the original ArrayBuffer — not a copy. Any downstream code that transfers the buffer via postMessage will detach the view and crash anything still holding a reference to it.

Cross-referenced with microsoft/vscode#76076:

The problem was that VSBuffer#slice called this.buffer.slice, which creates a copy. On the web, using slice caused poor performance because it unnecessarily copies data. The solution was to use subarray instead, which creates a view without copying the underlying ArrayBuffer data.

The optimization is correct. It just breaks on any JS engine that throws when you operate on a detached buffer — which JavaScriptCore does, per spec.


Root cause

How V8 and JavaScriptCore differ

When you do new Uint8Array(buffer).subarray(i, e) you get a view. The view shares its underlying ArrayBuffer with its parent.

When the ArrayBuffer is transferred to another context (iframe, worker, MessagePort) via the transfer list of postMessage, the original buffer becomes detachedbyteLength becomes 0, and any held view into it becomes "out of bounds."

In V8 (Chrome, Edge, Node, Bun): calling .slice() on a detached view returns an empty or zero-filled buffer and does not throw. The IPC layer silently slides past zero-length chunks.

In JavaScriptCore (Safari, all iOS browsers): calling .slice() on a detached view throws Underlying ArrayBuffer has been detached from the view or out-of-bounds. The IPC layer's delivery queue unwinds, fires handleUnexpectedError, and the message channel is dead.

JavaScriptCore is being spec-compliant here — TC39 explicitly says operations on detached buffers should throw a TypeError. V8's leniency is what most of the ecosystem has tested against, so the bug only surfaces on WebKit.

Where the detach happens inside code-server

Binary messages flow through code-server's IPC layer like this:

WebSocket binary frame (ArrayBuffer)

ChunkStream.acceptChunk(VSBuffer.wrap(new Uint8Array(event.data)))
   ↓  queues VSBuffer in _chunks — each is a Uint8Array view into the WS frame

ChunkStream._read(n) / .read(n)
   ↓  slices off the next message via VSBuffer.slice(0, n)  ← here
   ↓  returns new VSBuffer (currently a view, not a copy)

PersistentProtocol / ProtocolReader decodes header + body

Message delivered to consumer

Consumer transfers data into extension-host iframe via postMessage with transfer list

Underlying ArrayBuffer is now detached.
The view still in _chunks (for the next message) is now invalid.

Next acceptChunk call processes the next WS frame. _read() tries to slice
the still-pending view → JavaScriptCore throws.

The detach happens downstream of ChunkStream, somewhere inside the extension-host postMessage layer that code-server doesn't directly own. But ChunkStream holds the only surviving reference to a slice of the buffer at the moment it gets detached, so that's where the throw materializes.

Why this specifically kills Claude Code

Routine code-server activity sends small messages that fit in a single chunk — dispatch and consume quickly, nothing lingers.

Webview-heavy extensions like Claude Code send the initial webview HTML and JS as a series of larger binary IPC frames. These split across ChunkStream boundaries, which is exactly the path that calls VSBuffer.slice. And the receiving side immediately transfers the buffer into the webview iframe to render. By the time the next _read call comes through for the next chunk, the held view is invalid.

Typing in the editor: single-chunk messages, immediately consumed, nothing transferred. Works fine.

Loading a webview extension's initial content: multi-chunk messages, immediately transferred. Triggers the bug.


The fix

What the patch does

A single-character change inside the minified workbench.js bundle, baked into the Docker image at build time:

- slice(i,e){return new a(this.buffer.subarray(i,e))}
+ slice(i,e){return new a(this.buffer.slice(i,e))}

Uint8Array.prototype.slice(start, end) allocates a fresh ArrayBuffer and copies the requested range into it. The new VSBuffer is backed by a buffer nothing else holds a reference to, so nothing downstream can detach it.

Same signature. Same semantics. One buffer copy per IPC chunk-split.

Where it lives

In base.Dockerfile, in the layer immediately after the code-server install:

# WebKit ArrayBuffer-detach workaround for code-server's IPC layer.
RUN cd /usr/lib/code-server/lib/vscode/out/vs/code/browser/workbench && \
    test "$(grep -c 'slice(i,e){return new a(this.buffer.subarray(i,e))}' workbench.js)" = "1" && \
    sed -i 's/slice(i,e){return new a(this.buffer.subarray(i,e))}/slice(i,e){return new a(this.buffer.slice(i,e))}/' workbench.js && \
    grep -q 'slice(i,e){return new a(this.buffer.slice(i,e))}' workbench.js && \
    echo "VSBuffer.slice WebKit patch applied OK"

The two grep calls are load-bearing:

  1. Pre-condition: the unpatched pattern must appear exactly once. If a future code-server release ships with different minified identifiers, test ... -eq 1 fails and docker build fails loudly — prompting a re-derivation from the new bundle rather than silently producing a broken image.
  2. Post-condition: after sed, the patched pattern must be present. Guards against a regex no-match leaving the file unchanged.

Performance cost

Negligible. The patch adds one Uint8Array.prototype.slice — one memcpy — per ChunkStream._read call. Typical chunk sizes are sub-kilobyte. The IPC layer was already doing .set() copies in the multi-chunk path. The patch only adds work in the single-chunk fast path, and only for the IPC layer in browser mode.

VS Code's #76076 measured an improvement on the order of milliseconds on very large file transfers when going from copy → view. We're walking that back for the IPC layer in browser mode only. We're not moving multi-MB files through it.


Bug #1: the Claude Code 2.1.78+ regression

Independent of the VS Code WebKit issue, but both produce the same visible symptom and you need to resolve both.

The 2.1.78+ regression is tracked via a CLAUDE_CODE_VERSION="2.1.77" pin in the container setup script. The script downloads the pinned VSIX from Open VSX's linux-x64 build and reinstalls if the pin changes.

When Anthropic ships a fix (anthropics/claude-code#45729):

  • Bump CLAUDE_CODE_VERSION to the new version, or set it to latest to track the VSIX bundled with the claude CLI.
  • Restart containers. The sentinel mismatch triggers a clean reinstall.

Confirmation that the regression is fixed: the panel renders past the init message in Chrome as well as Safari. The 2.1.78+ hang affects Chrome too — it's not WebKit-specific.


Verifying both fixes are applied

On the server

WB=/usr/lib/code-server/lib/vscode/out/vs/code/browser/workbench/workbench.js

docker exec <container> grep -c 'buffer.subarray(i,e)' $WB   # expect 0
docker exec <container> grep -c 'buffer.slice(i,e)'    $WB   # expect 1

In the browser

Open code-server in Safari, open Web Inspector → Console, click the Claude Code icon. The following lines are expected and harmless — don't let them distract:

Failed to load resource: 404 (workbench.css.map)       ← sourcemap not served, harmless
Failed to load resource: 404 (vsda_bg.wasm)            ← proprietary module, not in OSS code-server
Failed to load resource: 404 (vsda.js)                 ← same
Refused to load data:font/ttf;base64,…                 ← CSP rejects inline font, falls back normally
[Warning] The web worker extension host is started in a same-origin iframe!  ← always present

What you want to see:

[Extension Host] Extension activated!
[Extension Host] Created lock file at ~/.claude/ide/<port>.lock
[Extension Host] Set CLAUDE_CODE_SSE_PORT=<port>

And zero Underlying ArrayBuffer has been detached errors. If they do appear, it's almost always one of:

  1. Stale browser or PWA cache. Safari aggressively caches workbench.js. Open regular Safari (not a Home Screen shortcut) and test there first.
  2. Container running an old image. After rebuilding base.Dockerfile, recreate (not just restart): docker compose up -d --force-recreate.
  3. Patch failed silently. The pre/post grep checks should make this impossible, but re-run the build and watch for VSBuffer.slice WebKit patch applied OK to confirm.

PWA cache reset (when needed)

With no-cache, must-revalidate headers on the Caddy proxy, Safari re-fetches workbench.js on each open and stale-cache issues after deploys should be rare.

If the server is verified healthy but Claude Code still shows blank only from a Home Screen shortcut (works in regular Safari), the shortcut has a stale PWA cache:

  1. Delete the Home Screen shortcut (long-press → Remove App).
  2. Settings → Safari → Clear History and Website Data, or remove the specific domain from Advanced → Website Data.
  3. Open the URL in regular Safari first, log in, confirm Claude Code renders.
  4. Share → Add to Home Screen to recreate the shortcut.

On Mac Safari: Develop → Empty Caches, then reload.


When to remove the patch

Remove the VSBuffer.slice WebKit patch block from base.Dockerfile when:

  1. code-server ships a WebKit-safe IPC bundle. Check coder/code-server releases for notes about VSBuffer.slice or "Safari webview blank." If unsure, grep the new bundle: grep 'buffer.subarray(i,e)' workbench.js — zero matches means code-server fixed it (and the pre-condition test -eq 1 will fail the build, signaling time to remove the patch block).
  2. VS Code reverts or refactors VSBuffer.slice in a way that doesn't hold a view across a transferable boundary. Watch for follow-ups to microsoft/vscode#76076.
  3. Migration off code-server entirely.

Sources

← All posts