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 (@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:
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:
- SDKSystemMessage at session start with session ID and capabilities
- SDKPartialAssistantMessage for real-time streaming with nested Anthropic API events:
message_start,content_block_start,content_block_delta(wheretext_deltacarries actual text chunks andinput_json_deltacarries tool input),content_block_stop,message_delta,message_stop - SDKAssistantMessage with the complete response including all content blocks, model info, and token usage
- 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. 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.
{
"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:
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.