> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗

Back to Articles

Building a Permission Gateway for AI Coding Agents: Inside The Companion's WebSocket Bridge Architecture

[ View on GitHub ]

Building a Permission Gateway for AI Coding Agents: Inside The Companion's WebSocket Bridge Architecture

Hook

When an AI agent wants to delete your production database configuration, wouldn't you like a big red "Approve" button instead of blind terminal trust? The Companion adds human-in-the-loop control to autonomous coding tools.

Context

AI coding assistants like Claude Code and Codex have evolved from simple code completion to autonomous agents that can read files, execute shell commands, and modify entire codebases. These CLI tools operate in your terminal, streaming responses and executing operations with whatever permissions your shell has. The problem? You're often reviewing a wall of terminal output trying to catch what the AI is about to do before it does it.

The terminal interface creates a fundamental tension: AI agents work best when they can operate autonomously, but developers need visibility and control over potentially destructive operations. Reading NDJSON event streams scrolling past in your terminal while trying to code is cognitive overload. The Companion emerged as a solution to this visibility problem, providing a web and mobile interface that sits between you and the AI agent, capturing every event, displaying it in a structured UI, and most importantly, gating dangerous operations behind approval workflows.

Technical Insight

The Companion's architecture is built on a clever interception pattern. AI CLI tools like Claude Code accept a --sdk-url parameter that points to their WebSocket server. The Companion runs its own local server (built with Bun and Hono) that becomes the new --sdk-url target, then proxies those WebSocket messages to the real AI backend while simultaneously streaming them to a browser-based React UI.

The core server maintains two WebSocket connections for each session: one to your browser, one to the AI service. It translates between the NDJSON format the CLI expects and structured events the UI can render. Here's how a typical session initialization looks:

// When a CLI tool connects with --sdk-url=http://localhost:3000
const sessionId = generateSessionId();
const clientWs = new WebSocket(CLAUDE_API_URL);
const browserWs = connections.get(sessionId);

// Proxy messages bidirectionally
clientWs.on('message', (data) => {
  const event = JSON.parse(data);
  
  // Check if this requires approval
  if (requiresPermission(event)) {
    // Hold the event, send to browser for approval
    pendingApprovals.set(event.id, event);
    browserWs.send(JSON.stringify({
      type: 'approval_required',
      operation: event.tool,
      params: event.params
    }));
  } else {
    // Forward directly to CLI
    clientWs.send(data);
    // Also send to browser for visibility
    browserWs.send(JSON.stringify({
      type: 'event',
      data: event
    }));
  }
});

The permission gating system is where The Companion adds real value. When the AI wants to execute a shell command or modify files, that event gets intercepted and displayed in the UI with Approve/Deny buttons. Your approval response flows back through the WebSocket bridge to resume execution. This creates a human-in-the-loop workflow without requiring changes to the underlying AI tools.

Session management is surprisingly sophisticated. The Companion persists session metadata to disk, allowing you to run multiple AI agents in parallel and recover sessions after server restarts. Each session tracks its own conversation history, tool calls, and approval decisions. The state management looks roughly like:

interface Session {
  id: string;
  created: number;
  status: 'active' | 'paused' | 'completed';
  events: Event[];
  pendingApprovals: Map<string, ToolCall>;
  cliConnection?: WebSocket;
  browserConnections: Set<WebSocket>;
}

// Multiple browser tabs can connect to the same session
function addBrowserConnection(sessionId: string, ws: WebSocket) {
  const session = sessions.get(sessionId);
  session.browserConnections.add(ws);
  
  // Send full event history to new connection
  ws.send(JSON.stringify({
    type: 'session_init',
    events: session.events
  }));
}

The service management integration demonstrates production-ready thinking. On macOS, The Companion automatically creates a launchd plist file; on Linux, it generates a systemd unit. This means the WebSocket bridge runs as a background service, starting on boot and respawning if it crashes. You're not manually starting a server every time you want to use Claude Code—it's just always available.

The dual update channel system uses GitHub releases for stable versions and automatically builds preview versions from every commit to main, tagging them with the commit SHA. This gives power users access to bleeding-edge features while maintaining a stable default experience. The CI/CD pipeline publishes both channels, and the client can switch between them with a simple configuration flag.

Gotcha

The Companion is tightly coupled to the Bun runtime—you can't run it with Node.js or Deno. Bun's WebSocket implementation and performance characteristics are baked into the architecture. If your team is standardized on Node, or you're deploying to an environment where Bun isn't available or approved, you're out of luck. This isn't a trivial dependency you can swap out.

The bigger limitation is the local-only, single-user architecture. The authentication is a simple auto-generated token stored in a local config file, suitable for protecting against accidental local access but not designed for security. There's no concept of user accounts, team collaboration, or remote access. If you want to review an AI coding session from your phone while away from your desk, you'll need to expose your local server to the internet yourself (with all the security implications). The architecture assumes you're one person running AI coding agents on one machine, which is fine for individual developers but doesn't scale to team workflows where you might want to review or approve operations collaboratively.

Verdict

Use if: You're actively working with Claude Code or Codex CLI tools and need granular visibility and control over tool executions, you run Bun in your development environment, you manage multiple concurrent AI coding sessions and need better organization than terminal tabs provide, or you want human-in-the-loop approval for potentially destructive operations. Skip if: You don't use Claude Code or Codex specifically, you're locked into Node.js and can't adopt Bun, you need team collaboration features or remote access capabilities, you prefer IDE-integrated solutions like Continue.dev or Cline, or you're looking for a cloud-hosted service rather than running local infrastructure. The Companion is a power tool for individual developers who want a control panel for autonomous AI agents—it solves a real problem elegantly but within a narrow use case.