# Can You Skin Claude Code? We Researched Every Approach

> Research into skinning Claude Code's terminal UI: React+Ink internals, 9 community GUI wrappers, the Agent SDK streaming protocol and a multi-skin design.

Source: https://synabun.ai/blog/claude-code-skins-research · By SynaBun · Published 2026-03-08

Claude Code is one of the most powerful agentic coding tools available. It reads your files, writes code, runs commands, and manages entire development workflows from a terminal. But the interface is a terminal. A terminal constrained by the same limitations terminals have had for decades: no rich markdown rendering, no inline diffs, no collapsible tool calls, no visual feedback beyond what ANSI escape codes can do.

We wanted to know: can you skin it? Can you replace the terminal UI with something better while keeping the full power of Claude Code underneath? We spent multiple research sessions tearing apart Claude Code's architecture, studying every community GUI wrapper, and designing a multi-skin framework from scratch. This is everything we found.

## How Claude Code's TUI Actually Works

Before trying to replace the UI, we needed to understand what we were replacing. Claude Code is not a simple terminal application. It is a custom **React + Ink** renderer built from scratch.

The Anthropic team rewrote Ink's rendering engine entirely. Their custom renderer uses a cell-based screen buffer with `Uint32Array` for characters, `Uint16Array` for styles, and `Uint8Array` for character widths. Layout is handled by **Yoga WASM**, Meta's flexbox engine compiled to WebAssembly. Rendering is differential, meaning only changed cells emit new ANSI escape sequences. This is how they achieved an 85% reduction in terminal flickering. They also support DEC mode 2026 for synchronized terminal output.

The entire application is bundled by Bun into a single 10.5MB `cli.js` file. It vendors its own copies of ripgrep, Tree-sitter WASM, Yoga WASM, and resvg WASM at runtime. Zero external dependencies. The codebase is TypeScript, and roughly 90% of it was written by Claude Code itself.

> The Anthropic team's philosophy: "Less infrastructure enables the model to deliver its full potential." They actively delete code and system prompts with each model release.

This architecture means you cannot simply inject CSS or swap a theme file. The rendering pipeline is deeply integrated into the React component tree and the custom Ink renderer. Skinning Claude Code from the inside is not viable.

## The Way In: Claude Agent SDK

If you cannot skin from the inside, you go around it. The official **[Claude Agent SDK](https://code.claude.com/docs/en/agent-sdk/overview)** (`@anthropic-ai/claude-agent-sdk`) exposes the full power of Claude Code as a programmable subprocess. It spawns the CLI with `--output-format stream-json --verbose` and communicates via stdin/stdout using NDJSON (newline-delimited JSON).

The core function is `query()`. It returns an async generator that yields typed `SDKMessage` events as the model thinks, writes code, and uses tools:

```javascript
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const msg of query({
  prompt: "Fix the bug in auth.ts",
  options: {
    model: "opus",
    allowedTools: ["Read", "Edit", "Bash"],
    mcpServers: { synabun: { command: "node", args: ["./mcp/preload.js"] } },
    canUseTool: async (toolName, input) => {
      // Show approval dialog in your custom UI
      return await showPermissionDialog(toolName, input);
    }
  }
})) {
  renderMessage(msg); // Your custom rendering
}
```

The SDK gives you everything: session management with resume and fork capabilities, granular permission control through `canUseTool` callbacks, MCP server integration, hook interception, subagent spawning, and full streaming access to every token the model produces.

## The Streaming Protocol

Understanding the NDJSON streaming protocol is essential for building a custom frontend. Each line the SDK emits is a complete JSON object. A typical turn flows through these message types:

1. **SDKSystemMessage** at session start with session ID and capabilities
1. **SDKPartialAssistantMessage** for real-time streaming with nested Anthropic API events: `message_start`, `content_block_start`, `content_block_delta` (where `text_delta` carries actual text chunks and `input_json_delta` carries tool input), `content_block_stop`, `message_delta`, `message_stop`
1. **SDKAssistantMessage** with the complete response including all content blocks, model info, and token usage
1. **SDKResultMessage** at the end with total cost in USD, duration, session ID, and success or error subtype

For tool use, the flow forks: the model emits a `content_block_start` with `type: "tool_use"`, streams the tool input as JSON deltas, then the tool executes and results flow back. If your custom UI needs to approve tool use, you intercept the `canUseTool` callback, display your own dialog, and return `"allow"` or `"deny"`.

## The Community Landscape: 9 GUI Wrappers

We were not the first to want a better Claude Code interface. We deep-dived into nine community projects that have already built GUI wrappers. The landscape breaks down by framework:

### Tauri-based (Rust + React) — lightweight

**Claudia** is the most notable. Backed by Y Combinator (S24), it wraps Claude Code in a Tauri 2 shell with React 18, Vite 6, Tailwind CSS 4, and shadcn/ui. The Rust backend handles CLI invocation via the Tauri shell plugin, with SQLite for project metadata, session checkpoints, and usage analytics. Key features include session versioning with timeline navigation, fork from checkpoint, a visual diff viewer, and an MCP management UI. It is the most polished project in the space.

Other Tauri wrappers include **Opcode** (project discovery, background tasks, token tracking), **yume** (multi-provider streaming), **cc-switch** (multi-CLI supporting Claude, Codex, and Gemini with unified MCP and skills), and **claude-code-desktop** (minimal wrapper preserving native capabilities).

### Electron-based

**CodePilot** uses Electron 40 with Next.js 16 and React 19. Its `claude-client.ts` is 41KB of streaming logic. It has a 12-table SQLite schema, 52 REST API endpoints, Telegram notification hooks, and a child process model using `utilityProcess.fork()`. It is the heaviest and most feature-complete project, but also the largest codebase.

### Web-based

**Claudex** (React 19 + FastAPI) introduced a clever innovation: an "Anthropic Bridge" sidecar that lets you route Claude Agent SDK calls through non-Anthropic providers like OpenRouter or GitHub Copilot. It runs on `127.0.0.1:3456` inside a Docker sandbox and modifies `ANTHROPIC_BASE_URL` to intercept API calls. Other web projects include **claude-code-webui** (Deno + Vite, mobile-friendly) and **CloudCLI** (remote session management).

The **pattern across all nine projects** is identical: spawn the `claude` CLI as a subprocess, capture its `stream-json` output, and render it in a custom UI. Tauri 2 has won over Electron for new projects. React 18+ with Vite, shadcn/ui, and Tailwind CSS 4 is the standard frontend stack. SQLite handles local persistence everywhere.

## Three Approaches We Evaluated

With the research complete, we had three clear paths forward:

**Option A: [Extend the existing Neural Interface](https://synabun.ai/docs#section-sidepanels).** SynaBun already runs a web UI on port 3344. It has an Express server, WebSocket infrastructure, terminal sessions with ConPTY, a file explorer, settings UI, SynaBun memory integration, and a session resume system. Adding a chat-mode skin means adding Agent SDK subprocess management on the server and a new rendering layer in the browser.

**Option B: Build a new Tauri or Electron app.** Start fresh with a dedicated desktop wrapper, following the pattern established by Claudia and CodePilot. Clean architecture but duplicates all the infrastructure we already have.

**Option C: Fork and modify Claude Code directly.** Patch the React + Ink renderer to support custom themes or alternative renderers. Theoretically possible but practically unmaintainable. Every Claude Code update would break the fork.

We chose **Option A**. The reasoning was straightforward: we already have a running Express server with WebSocket, a terminal session manager, a file explorer, settings, memory, and resume functionality. Zero new frameworks needed. The chat skin is just another view in the existing application.

## The Multi-Skin Framework

We did not just want one alternative skin. We wanted a system where anyone can build and swap custom skins. The research phase produced a five-part architecture:

### 1. Skin = Variant Pattern

SynaBun's Neural Interface already has a variant system. The 3D memory graph and the 2D graph are both "variants" registered through `registry.js` with capabilities declared via `registerVariant()`. Each variant gets its own navbar buttons, help sections, settings tabs, and keyboard shortcuts. The event bus in `state.js` handles communication between shared modules and variant-specific renderers.

A Claude Code skin is just another variant. The 3D graph provides `graph.js`, a chat skin provides `ui-claude-skin.js`. Same contract, different renderer. No new plugin system needed.

### 2. Skin Manifest and Discovery

Each skin lives in its own folder under `neural-interface/public/skins/<name>/` with a `skin.json` manifest. The server discovers skins at boot by scanning the directory and reading manifests. Drop a folder, it works. No marketplace, no install flow, no configuration. Like Claude Code skills in `.claude/commands/` or VS Code extensions in their extensions folder.

```json
{
  "name": "SynaBun Chat",
  "id": "chat",
  "version": "1.0.0",
  "author": "SynaBun",
  "entry": "index.js",
  "css": "styles.css",
  "capabilities": ["streaming", "diffs", "permissions", "cost-tracking"],
  "requires": ["skin-bridge"]
}
```

### 3. SkinBridge: The Abstraction Layer

This is the core architectural insight. Raw SDK events like `content_block_delta` are too low-level for UI code. Terminal renderers tangle resize, scroll, and rendering logic. The **SkinBridge** sits between the Agent SDK streaming and the skin renderer. It owns the WebSocket connection, parses NDJSON, accumulates text deltas, tracks tool calls, and dispatches high-level callbacks.

The skin contract is a set of lifecycle callbacks that any skin must implement:

```javascript
export function createSkin(container, bridge) {
  return {
    mount(container) {},
    unmount() {},
    onTextChunk(text, messageId) {},
    onTextComplete(fullText, messageId) {},
    onToolStart(toolName, toolInput, toolId) {},
    onToolComplete(toolName, result, toolId) {},
    onPermissionRequest(toolName, input, respond) {},
    onResult({ cost, tokens, duration, sessionId }) {},
    onError(message, subtype) {},
  };
}
```

The bridge handles all connection complexity. Skins never touch WebSocket. They never parse NDJSON. They just implement render callbacks. This separation means a minimal skin can be built in about 100 lines of code.

### 4. Composable Components

Building a skin from scratch is too much work for most developers. A fully templated approach produces cookie-cutter results. The middle ground is a library of composable UI components that skins import selectively. Need to override just message rendering? Import the default diff viewer and permission dialog. Want a completely custom tool call display? Write your own but keep the default session picker.

The component library includes `MessageBubble` for user and assistant messages with markdown, `ToolCallCard` for collapsible tool calls with input and output panels, `DiffView` for side-by-side or inline diffs, `BashOutput` for ANSI-colored command output, `PermissionDialog` for tool approval modals, `CostCounter` for token and cost tracking, `SessionPicker` for resume, fork, and new session flows, and `StreamingText` for typewriter effects with cursor.

Each component exports a `render(container, data, options)` function and uses CSS variables for theming. Change the colors, spacing, and fonts without touching component logic.

### 5. Three Built-in Skins

**Terminal skin** wraps the existing xterm.js + ConPTY integration. It preserves the full CLI experience with tab completion and vim mode. This is the fallback for developers who want raw terminal access. No Agent SDK needed since it connects directly via PTY.

**Chat skin (SynaBun Chat)** is the reference implementation and product differentiator. Streaming markdown via streaming-markdown + marked.js. Code highlighting via highlight.js with auto-detection. Collapsible tool call cards. File diffs via diff2html. ANSI-colored bash output via ansi_up.js. Permission approval modal dialogs. Cost and token counter. Session picker with resume and fork. Model selector. Design follows tight spacing, no card-in-card nesting, text-only utility buttons, and intentional visual rhythm.

**Minimal skin** is a bare-bones 100-line implementation of the full contract. Plain text output, `alert()` dialogs for permissions, no external dependencies. It exists purely as a tutorial for skin developers to see the minimum viable implementation.

## Frontend Tech Stack Decisions

Every community project uses React. We are building in **vanilla JavaScript** with zero framework dependencies. This is a deliberate choice. SynaBun's Neural Interface is already vanilla JS with intentional, hand-crafted design. None of the nine community wrappers are vanilla JS. This is the differentiator.

For **markdown rendering**, marked.js wins with 19.3 million weekly downloads, 36,000 GitHub stars, and 97% GFM compliance. Combined with streaming-markdown for real-time rendering during token streaming, it produces the ChatGPT-style typewriter effect. The streaming-markdown library is optimistic (renders partial markdown immediately) and non-destructive (only adds DOM elements, never modifies existing ones, so users can copy text during streaming).

For **syntax highlighting**, highlight.js supports 190+ languages with auto-detection at roughly 20KB bundled. Prism.js is faster but lacks auto-detection. Shiki uses VS Code's TextMate grammars for perfect accuracy but requires 250KB+ of WASM. highlight.js is the right balance.

For **diff rendering**, diff2html provides both line-by-line and side-by-side views with syntax highlighting built in. For **terminal output**, ansi_up.js converts ANSI escape sequences to HTML with zero overhead. No cursor emulation, no screen buffer, just colorized text.

For **performance**, virtual scrolling via HyperList handles 10,000+ items in under 100ms with zero dependencies and only 300 lines of code. `requestAnimationFrame` batching collects multiple streaming chunks into single layout passes. `IntersectionObserver` defers markdown parsing until messages enter the viewport.

Total frontend bundle: approximately **100KB gzipped**. That is marked.js, highlight.js, streaming-markdown, DOMPurify, diff2html, and ansi_up.js combined.

## The Permission Round-Trip

One of the trickiest parts of building a custom Claude Code frontend is handling permissions. In the terminal, Claude Code prompts you inline and waits for a keypress. In a custom UI, this needs to become a visual dialog with an asynchronous response.

The flow works like this: the Agent SDK hits `canUseTool` on the server, which sends a `claude:permission` event over WebSocket to the browser. The active skin renders a permission dialog showing the tool name and input. The user clicks allow or deny. The browser sends `claude:permission:response` back over WebSocket. The server resolves the `canUseTool` promise. Claude Code continues or stops.

The same pattern handles `AskUserQuestion` interception, where Claude asks clarifying questions. The SDK sends the questions, your UI displays them, the user answers, and the responses flow back.

## What This Means For Claude Code Users

The short answer to "can you skin Claude Code" is **no, not from the inside**. The React + Ink renderer is a sealed system. But you can build an entirely separate frontend that talks to Claude Code through the Agent SDK, and the experience can be significantly better than the terminal.

The community has already proven this with nine different projects. What they have not done is build a system where skins are interchangeable, composable, and framework-free. That is what we are building.

The key insight from this research is that you do not need to understand Claude Code's internals to build a custom frontend. The Agent SDK abstracts everything into a clean streaming protocol. The hard part is not connecting to Claude Code. It is building a rendering layer that handles streaming markdown, tool call visualization, inline diffs, permission dialogs, and session management without becoming a 40KB spaghetti file.

The SkinBridge pattern solves this by drawing a clean line between protocol complexity and rendering. Skins stay simple. The bridge handles the mess. Components let you mix and match.

---

This research is stored across 21 memories in SynaBun's persistent vector memory under the `synabun-skins` category. The full implementation plan exists. No code has been written yet. When we build it, we will document every step here.

If you are building your own Claude Code frontend or thinking about customizing the agentic coding experience, the Agent SDK documentation and the community projects listed above are the best starting points. And if you want a skin system that just works out of the box, follow along. We are building it in the open.
