# Electron Main Process, Preload, and Renderer Architecture in Craft Agents

> Understand the Electron main process, preload, and renderer architecture in Craft Agents. Learn how this secure three-layer system enhances performance and security.

- Repository: [Craft Ai Agents/craft-agents-oss](https://github.com/craft-ai-agents/craft-agents-oss)
- Tags: architecture
- Published: 2026-07-03

---

**Craft Agents implements a secure three-layer Electron architecture where the main process manages the native runtime and RPC server, the preload script constructs a typed RPC bridge exposing a `window.craft` API, and the sandboxed React renderer communicates exclusively through that API without direct access to Node.js or Electron APIs.**

Craft Agents ships as a full-stack Electron application that strictly separates concerns across the main process, preload script, and renderer. This architecture follows Electron security best practices by enforcing context isolation and disabling node integration, while providing a type-safe RPC layer that allows the React UI to communicate with backend services. Understanding how these three layers interact is essential for developers extending the desktop client or debugging transport issues.

## Main Process: Bootstrapping the Native Runtime

The **main process** serves as the entry point and native runtime controller. Located in [`apps/electron/src/main/index.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/apps/electron/src/main/index.ts), it initializes the Electron app, creates the browser window, loads shell environment variables, and registers all RPC handlers.

### Shell Environment and CLI Tooling

Before creating any windows, the main process loads the user’s shell environment to ensure tools like `nvm` or Homebrew are available to the embedded agent runtime. It also initializes Sentry conditionally and exposes bundled CLI tools through environment variables.

```typescript
// apps/electron/src/main/index.ts (excerpt)
import { app, BrowserWindow, ipcMain } from 'electron';
import { loadShellEnv } from './shell-env';
import { registerAllRpcHandlers } from './handlers/index';

// 1. Load shell environment (nvm, Homebrew, etc.)
loadShellEnv();

// 2. Initialize Sentry only when DSN is provided
Sentry.init({ dsn: process.env.SENTRY_ELECTRON_INGEST_URL, ... });

// 3. Expose bundled CLI tools to agents
process.env.CRAFT_UV = ...;
process.env.CRAFT_BUN = ...;

// 4. Create BrowserWindow with security settings
const win = new BrowserWindow({
  webPreferences: {
    preload: join(__dirname, '..', 'preload', 'bootstrap.js'),
    contextIsolation: true,
    nodeIntegration: false,
  },
});
win.loadURL(`file://${join(__dirname, '..', 'renderer', 'index.html')}`);

// 5. Register RPC handlers
registerAllRpcHandlers();

```

Key responsibilities include:
- **Shell environment loading** via `loadShellEnv()` ensures Python and Node.js version managers are accessible to spawned agent processes.
- **Security configuration** with `contextIsolation: true` and `nodeIntegration: false` prevents the renderer from accessing Node.js APIs directly.
- **Preload injection** through `webPreferences.preload` establishes the bridge before the renderer loads.

## Preload Script: The Typed RPC Bridge

The **preload script** runs in a privileged context with access to both Electron APIs and the renderer’s DOM. Located in [`apps/electron/src/preload/bootstrap.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/apps/electron/src/preload/bootstrap.ts), it builds a **typed RPC client** that communicates with the main process over IPC, then exposes a safe `window.craft` API via `contextBridge`.

### Dual-Mode Transport Architecture

The preload detects whether to run in **thin-client mode** (remote server only) or **normal mode** (local server + optional remote workspace):

```typescript
// apps/electron/src/preload/bootstrap.ts (excerpt)
import { contextBridge, ipcRenderer, shell } from 'electron';
import { WsRpcClient } from '../transport/client';
import { RoutedClient } from '../transport/routed-client';
import { buildClientApi } from '../transport/build-api';
import { CHANNEL_MAP } from '../transport/channel-map';

// Detect thin-client mode via environment variable
const isClientOnly = !!process.env.CRAFT_SERVER_URL;

let client: TransportClient;
if (isClientOnly) {
  // Direct connection to remote server
  client = new WsRpcClient(process.env.CRAFT_SERVER_URL!, ...);
} else {
  // Local WebSocket server with optional remote workspace routing
  const local = new WsRpcClient(`ws://127.0.0.1:${wsPort}`, ...);
  const routed = new RoutedClient(local, initialWorkspaceClient);
  client = routed;
}

// Register capability handlers for server-to-client calls
client.handleCapability(CLIENT_OPEN_EXTERNAL, (url) => shell.openExternal(url));
client.handleCapability(CLIENT_SHOW_IN_FOLDER, (path) => shell.showItemInFolder(path));
client.handleCapability(CLIENT_BROWSER_INVOKE, (req) => 
  ipcRenderer.invoke('__browser:invoke', req)
);

// Build and expose the typed API
const api = buildClientApi(client, CHANNEL_MAP, (ch) => client.isChannelAvailable(ch));
api.getRuntimeEnvironment = () => 'electron';
contextBridge.exposeInMainWorld('craft', api);

```

Key concepts:
- **WsRpcClient** handles the underlying WebSocket communication for both local and remote connections.
- **RoutedClient** multiplexes calls between a local server and remote workspace when operating in hybrid mode.
- **Capability handlers** allow the server to request privileged actions like opening external URLs or showing files in the system folder.
- **buildClientApi** generates a strongly-typed proxy object that mirrors the server’s RPC surface, providing methods like `api.session.list()` or `api.file.open()`.

## Renderer Process: The Sandboxed React UI

The **renderer** is a Vite-built React application that runs in a sandboxed web page. Located in `apps/electron/src/renderer/`, it never imports Electron directly; all communication flows through the `window.craft` object injected by the preload.

### Transport State Management

The renderer monitors connection health using the `useTransportConnectionState` hook:

```typescript
// apps/electron/src/renderer/hooks/useTransportConnectionState.ts
import { useEffect, useState } from 'react';
import { TransportConnectionState } from '@craft-agent/server-core/transport';

export function useTransportConnectionState() {
  const [state, setState] = useState<TransportConnectionState>('disconnected');

  useEffect(() => {
    const client = (window as any).craft;
    const unsubscribe = client.onConnectionStateChanged(setState);
    return unsubscribe;
  }, []);

  return { 
    state, 
    reconnect: () => (window as any).craft.reconnectNow() 
  };
}

```

Usage in a React component:

```tsx
// apps/electron/src/renderer/App.tsx (simplified)
function App() {
  const { state, reconnect } = useTransportConnectionState();

  if (state === 'disconnected') {
    return <button onClick={reconnect}>Reconnect</button>;
  }

  // All backend calls go through window.craft
  const sessions = await (window as any).craft.session.list();
  
  return <div>{/* React components */}</div>;
}

```

## How the Three Layers Communicate

The architecture enforces strict boundaries while enabling rich functionality:

1. **Main → Preload**: The `BrowserWindow` loads the preload script via `webPreferences.preload`. The preload can call `ipcRenderer.invoke` to request data from the main process.

2. **Preload → Main**: The transport client sends RPC messages over Electron’s IPC channels. Handlers registered via `registerAllRpcHandlers()` in the main process execute requests and return results.

3. **Preload → Renderer**: The `contextBridge.exposeInMainWorld('craft', api)` call makes the RPC client available as a read-only global object. The renderer calls methods like `window.craft.session.create()`, which are serialized, routed through the transport layer, and processed by the main process.

This separation ensures that even if the renderer is compromised, it cannot access the file system or shell, as all privileged operations must traverse the capability-based RPC system defined in the preload.

## Practical Code Examples

### Creating a Session from the UI

```typescript
// In a React component (renderer layer)
async function createSession(name: string) {
  const result = await (window as any).craft.session.create({ name });
  console.log('Created session:', result);
}

```

The `session.create` RPC surface is defined in [`packages/server-core/src/handlers/session.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/packages/server-core/src/handlers/session.ts) and exposed through the preload’s `buildClientApi` function.

### Opening Files in the System Explorer

```typescript
// Renderer calls the capability exposed by preload
async function revealFile(path: string) {
  await (window as any).craft.client.showInFolder(path);
}

```

This invokes the `CLIENT_SHOW_IN_FOLDER` capability handler registered in [`bootstrap.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/bootstrap.ts), which forwards to Electron’s `shell.showItemInFolder`.

### Handling Connection State Changes

```typescript
useEffect(() => {
  const unsubscribe = (window as any).craft
    .onConnectionStateChanged((state) => {
      if (state === 'disconnected') {
        showReconnectDialog();
      }
    });
  return unsubscribe;
}, []);

```

## Summary

- **Main Process**: Controls the Electron runtime in [`apps/electron/src/main/index.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/apps/electron/src/main/index.ts), loads shell environments, bundles CLI tools (uv, bun), and hosts the RPC server.
- **Preload Script**: Acts as a privileged bridge in [`apps/electron/src/preload/bootstrap.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/apps/electron/src/preload/bootstrap.ts), creating either a local `WsRpcClient` or remote connection, and exposing a type-safe `window.craft` API via `contextBridge`.
- **Renderer**: A sandboxed React app built with Vite that communicates exclusively through `window.craft`, maintaining security through `contextIsolation` and `nodeIntegration: false`.
- **Transport Layer**: Supports both embedded server mode and thin-client mode (`CRAFT_SERVER_URL`), with `RoutedClient` handling multiplexing between local and remote workspaces.
- **Security Model**: All privileged operations (file system, shell, external links) are capability-based, registered in the preload, and invoked by the main process only through validated IPC channels.

## Frequently Asked Questions

### How does the preload script maintain security while enabling powerful features?

The preload script runs in a privileged context with access to both Node.js and the DOM, but it exposes only a curated API via `contextBridge.exposeInMainWorld()`. This creates a **read-only, type-safe bridge** (`window.craft`) that the renderer can call, but the renderer cannot access Electron or Node APIs directly. All privileged actions like `shell.openExternal` are wrapped as capability handlers that validate requests before execution.

### Can Craft Agents run as a thin client without the local Electron server?

Yes. When the `CRAFT_SERVER_URL` environment variable is set, the preload script in [`bootstrap.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/bootstrap.ts) creates a direct `WsRpcClient` connection to the remote server instead of starting a local WebSocket server. In this mode, the main process still manages the window lifecycle, but all RPC traffic routes directly to the remote Craft Agents server, enabling a lightweight client architecture.

### Why does the renderer use `window.craft` instead of importing Electron directly?

The renderer explicitly disables `nodeIntegration` and enables `contextIsolation` for security. This prevents arbitrary code execution if the web content is compromised. By forcing all communication through the `window.craft` object exposed by the preload, the architecture ensures that **only predefined, type-safe RPC methods** are available, eliminating direct access to the file system, shell, or other native resources.

### How does the main process make shell tools like nvm or brew available to agents?

The main process calls `loadShellEnv()` from [`apps/electron/src/main/shell-env.ts`](https://github.com/craft-ai-agents/craft-agents-oss/blob/main/apps/electron/src/main/shell-env.ts) before creating the browser window. This function loads the user’s shell environment variables (including PATH modifications from `.bashrc`, `.zshrc`, or similar), ensuring that version managers and package managers are available in the environment where agent processes execute.