Back to Articles

Building a Web Interface for Claude Code: How sugyan/claude-code-webui Bridges CLI and Browser

[ View on GitHub ]

Building a Web Interface for Claude Code: How sugyan/claude-code-webui Bridges CLI and Browser

Hook

Claude Code is remarkably powerful as a CLI tool, but try using it on your phone or sharing a session with a non-technical stakeholder—suddenly you're back in 1995 explaining what a terminal emulator is.

Context

When Anthropic released Claude Code, they made a deliberate choice: CLI-first, no GUI. This decision makes sense for their target audience of developers who live in terminals, but it creates friction for scenarios beyond solo coding sessions. Want to review Claude's proposed changes during a standup? Pull out your laptop. Need to approve a file operation while commuting? Hope you have a terminal app configured. Want a junior developer to experiment with AI-assisted coding without teaching them shell navigation first? Good luck.

The sugyan/claude-code-webui project emerged from this gap. Rather than reimplementing Claude's capabilities, it takes a wrapper approach: spawn the existing Claude CLI as a child process, intercept its I/O streams, and expose everything through a web interface. It's the same philosophy that turned Git into GitHub's web UI or made Kubernetes manageable through Lens—take a powerful CLI tool and make it accessible to broader contexts without sacrificing the underlying functionality.

Technical Insight

The architecture splits cleanly into three layers: a backend server that manages Claude CLI processes, a streaming communication layer for real-time responses, and a React frontend that handles user interactions. The backend runs on either Deno or Node.js, with the main server spawning Claude Code instances using child_process and managing their lifecycle.

The streaming implementation is particularly interesting. When you send a message through the web UI, the backend doesn't just call Claude and wait for a complete response. Instead, it pipes stdout from the Claude CLI process through either Server-Sent Events (SSE) or WebSocket connections, depending on the endpoint. Here's a simplified view of how the backend handles streaming:

// Simplified streaming handler
async function handleChatStream(request: Request): Promise<Response> {
  const body = await request.json();
  const stream = new ReadableStream({
    async start(controller) {
      const claudeProcess = spawn('claude', [
        '--model', body.model,
        '--message', body.message
      ]);
      
      claudeProcess.stdout.on('data', (chunk) => {
        // Parse Claude's output and forward as SSE
        const event = `data: ${JSON.stringify({
          type: 'content',
          content: chunk.toString()
        })}\n\n`;
        controller.enqueue(new TextEncoder().encode(event));
      });
      
      claudeProcess.stderr.on('data', (chunk) => {
        // Tool usage requests come through stderr
        const toolRequest = parseToolRequest(chunk.toString());
        if (toolRequest) {
          controller.enqueue(new TextEncoder().encode(
            `data: ${JSON.stringify({
              type: 'tool_request',
              tool: toolRequest.name,
              params: toolRequest.params
            })}\n\n`
          ));
        }
      });
      
      claudeProcess.on('close', () => controller.close());
    }
  });
  
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive'
    }
  });
}

The permission management system is where this wrapper adds significant value beyond basic streaming. Claude Code can read files, write code, and execute commands—operations that need human approval. The CLI handles this through interactive prompts, but web interfaces can't block on stdin. Instead, claude-code-webui implements a permission queue. When Claude requests a file operation, the backend pauses the child process, sends a permission request to the frontend via the stream, and waits for user response through a separate API endpoint. The frontend renders these as modal dialogs with syntax-highlighted diffs for file changes or command previews for shell executions.

Conversation history is stored in the backend's file system, organized by project. Each project directory gets its own SQLite database (or JSON file in simpler implementations) tracking message history, tool usage, and approval decisions. This enables the 'plan mode' feature where you can review all proposed changes before executing any of them—effectively a dry-run that shows you what Claude wants to do without actually modifying your codebase.

The React frontend uses a standard chat interface pattern, but with domain-specific additions. Beyond message bubbles, it renders tool usage cards showing file diffs with syntax highlighting via Prism.js, collapsible sections for long outputs, and inline approval buttons. The component architecture separates concerns cleanly: a ChatContainer manages WebSocket connections and message state, MessageList handles rendering, and ToolRequestCard components deal with permission UI. State management uses React Context to avoid prop-drilling the WebSocket connection and permission handlers through multiple component layers.

Gotcha

The biggest limitation is security exposure. You're taking a tool designed for local CLI usage—with full file system access to your project—and exposing it through a web server. If you configure this to listen on anything other than localhost, you've created a web-accessible interface that can read and modify files on your system. The repository includes no authentication layer by default, so deployment requires careful consideration. Running this in Docker helps contain the blast radius, but then you're managing volume mounts for project access and dealing with the Claude CLI authentication inside containers.

Performance can also surprise you. Each conversation spawns a new Claude CLI process, and those processes aren't lightweight—they're initializing language models and setting up tool execution contexts. On resource-constrained systems or when managing multiple simultaneous conversations, you'll notice lag. The streaming helps mask some latency for response generation, but cold starts on first message can take several seconds. Additionally, because this is a wrapper around the CLI rather than direct API integration, you're inheriting all of Claude CLI's limitations: no ability to switch models mid-conversation, no direct control over token usage beyond what the CLI exposes, and you're dependent on the CLI's release cycle for new features or model access.

Verdict

Use if: you need mobile access to Claude Code, want to share AI coding sessions with stakeholders who shouldn't need terminal knowledge, or prefer visual conversation management with granular permission controls. It's particularly valuable if you're already invested in Claude Code workflows but find the CLI limiting for collaboration or cross-device usage. Skip if: you're comfortable in terminals and don't need the accessibility layer, require production-grade security for multi-user deployments (you'd need to add auth yourself), or want provider flexibility beyond Claude. Also skip if you're running on resource-constrained hardware where spawning multiple Node processes for chat sessions would cause performance issues. This tool excels at democratizing access to Claude Code, but it's a wrapper—you're trading deployment complexity and security considerations for interface convenience.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/ai-dev-tools/sugyan-claude-code-webui.svg)](https://starlog.is/api/badge-click/ai-dev-tools/sugyan-claude-code-webui)