Desktop & Electron

Electron mit Claude Code: Desktop Apps mit Web-Technologien 2026

Electron: Main-Process, Renderer, IPC, Native Menus, Auto-Update, Code-Signing, Vite-Integration — Claude Code baut plattformübergreifende Desktop-Apps mit React und TypeScript.

6. Mai 2026 11 min Lesezeit

Electron ist 2026 noch immer das meistgenutzte Framework für Cross-Platform-Desktop-Apps mit Web-Technologien. VS Code, Slack, Figma, Discord — alle basieren auf Electron. Mit Claude Code lassen sich komplexe Electron-Applikationen mit Main-Process-Logik, IPC-Kommunikation, nativen APIs und automatischen Updates deutlich schneller entwickeln. Dieser Guide zeigt die sechs zentralen Bausteine einer modernen Electron-App.

Voraussetzungen: Node.js 20+, npm 10+, TypeScript-Grundkenntnisse. Electron-Version in diesem Guide: Electron 30 mit electron-vite 2.x und React 18.

1. Electron-Architektur: Main vs. Renderer Process

Electron trennt strikt zwischen zwei Prozessen. Der Main Process läuft in Node.js und steuert Fenster, Menus und native OS-Funktionen. Der Renderer Process ist eine Chromium-Instanz und rendert die React-UI. Die Brücke zwischen beiden ist das Preload-Script über contextBridge.

Main Process Renderer Process Preload Script
Node.js Full Access Browser-Sandbox Sichere Brücke
BrowserWindow, Menu React, DOM, CSS contextBridge API
ipcMain.handle() ipcRenderer.invoke() exposeInMainWorld()
fs, path, os fetch, localStorage Typen-sichere API
app, dialog, shell window.electronAPI Validierung/Filter

main.ts — BrowserWindow erstellen

// src/main/main.ts — Electron Main Process import { app, BrowserWindow, Menu, shell, nativeImage } from 'electron' import { join } from 'path' import { registerIpcHandlers } from './ipc-handlers' import { buildAppMenu } from './menu' let mainWindow: BrowserWindow | null = null function createWindow(): void { mainWindow = new BrowserWindow({ width: 1280, height: 800, minWidth: 800, minHeight: 600, show: false, // Verhindert weißes Flash titleBarStyle: 'hiddenInset', // macOS-spezifisch icon: nativeImage.createFromPath( join(__dirname, '../resources/icon.png') ), webPreferences: { preload: join(__dirname, '../preload/preload.js'), contextIsolation: true, // PFLICHT seit Electron 12+ nodeIntegration: false, // Node.js NICHT im Renderer! sandbox: true, // Zusätzliche Sicherheit webSecurity: true, }, }) // Fenster erst zeigen wenn DOM bereit (verhindert flackern) mainWindow.once('ready-to-show', () => { mainWindow!.show() if (process.env.NODE_ENV === 'development') { mainWindow!.webContents.openDevTools() } }) // URL laden — Dev: Vite Dev Server / Prod: lokale HTML-Datei if (process.env.ELECTRON_RENDERER_URL) { mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL) } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html')) } // Externe Links im System-Browser öffnen mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('https:')) shell.openExternal(url) return { action: 'deny' } }) } app.whenReady().then(() => { createWindow() registerIpcHandlers() Menu.setApplicationMenu(buildAppMenu()) app.on('activate', () => { // macOS: Fenster neu erstellen wenn Dock-Icon geklickt if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) app.on('window-all-closed', () => { // macOS behält App aktiv bis explizit beendet if (process.platform !== 'darwin') app.quit() })

Wichtige BrowserWindow-Optionen 2026

preload.ts — contextBridge-Typen

// src/preload/preload.ts — Sichere Brücke Main ↔ Renderer import { contextBridge, ipcRenderer } from 'electron' // Typen-Definition für window.electronAPI im Renderer export type ElectronAPI = { readFile: (path: string) => Promise<string> writeFile: (path: string, content: string) => Promise<void> selectFile: (options?: FileDialogOptions) => Promise<string | null> showNotification: (title: string, body: string) => void getAppVersion: () => Promise<string> onUpdateAvailable: (callback: (version: string) => void) => void platform: string } contextBridge.exposeInMainWorld('electronAPI', { readFile: (path: string) => ipcRenderer.invoke('fs:read-file', path), writeFile: (path: string, content: string) => ipcRenderer.invoke('fs:write-file', { path, content }), selectFile: (options?) => ipcRenderer.invoke('dialog:open-file', options), showNotification: (title: string, body: string) => ipcRenderer.send('notification:show', { title, body }), getAppVersion: () => ipcRenderer.invoke('app:get-version'), onUpdateAvailable: (callback) => ipcRenderer.on('update:available', (_event, version) => callback(version)), // Statische Werte direkt übergeben (kein IPC nötig) platform: process.platform, } satisfies ElectronAPI)

2. IPC-Kommunikation: Main ↔ Renderer

IPC (Inter-Process Communication) ist das Herzstück jeder Electron-App. Das moderne Pattern seit Electron 13 ist ipcMain.handle im Main Process und ipcRenderer.invoke im Preload — beide arbeiten mit Promises, kein Callback-Hell.

IPC-Pattern-Empfehlung 2026: Immer invoke/handle für bidirektionale Kommunikation. send/on nur für Fire-and-Forget-Nachrichten (z.B. Notifications). Niemals sendSync — das blockiert den Renderer-Thread!

ipc-handlers.ts — Alle Handler registrieren

// src/main/ipc-handlers.ts import { ipcMain, app, dialog, Notification, BrowserWindow } from 'electron' import { promises as fs } from 'fs' import { join } from 'path' export function registerIpcHandlers(): void { // ── Dateisystem ──────────────────────────────────────── ipcMain.handle('fs:read-file', async (_event, path: string) => { // Sicherheits-Check: Nur erlaubte Pfade if (!isAllowedPath(path)) { throw new Error(`Zugriff verweigert: ${path}`) } return fs.readFile(path, 'utf-8') }) ipcMain.handle( 'fs:write-file', async (_event, { path, content }: { path: string; content: string }) => { if (!isAllowedPath(path)) throw new Error(`Zugriff verweigert: ${path}`) await fs.writeFile(path, content, 'utf-8') } ) // ── Dialog ───────────────────────────────────────────── ipcMain.handle('dialog:open-file', async (_event, options = {}) => { const win = BrowserWindow.getFocusedWindow() const result = await dialog.showOpenDialog(win!, { properties: ['openFile'], filters: options.filters ?? [{ name: 'Alle Dateien', extensions: ['*'] }], }) return result.canceled ? null : result.filePaths[0] }) ipcMain.handle('dialog:save-file', async (_event, options = {}) => { const win = BrowserWindow.getFocusedWindow() const result = await dialog.showSaveDialog(win!, { defaultPath: options.defaultPath ?? 'datei.txt', }) return result.canceled ? null : result.filePath }) // ── App-Infos ─────────────────────────────────────────── ipcMain.handle('app:get-version', () => app.getVersion()) ipcMain.handle('app:get-path', (_event, name: string) => app.getPath(name as any)) // ── Notification (Fire & Forget) ──────────────────────── ipcMain.on('notification:show', (_event, { title, body }) => { if (Notification.isSupported()) { new Notification({ title, body }).show() } }) } // Sicherheits-Helper: Nur Pfade in userData/Downloads erlaubt function isAllowedPath(filePath: string): boolean { const allowedRoots = [ app.getPath('userData'), app.getPath('downloads'), app.getPath('documents'), ] return allowedRoots.some(root => filePath.startsWith(root)) }

React-Hook für IPC-Aufrufe

// src/renderer/hooks/useElectron.ts import { useCallback } from 'react' import type { ElectronAPI } from '../../preload/preload' // TypeScript: window.electronAPI typen declare global { interface Window { electronAPI: ElectronAPI } } export function useElectron() { const selectAndReadFile = useCallback(async () => { const path = await window.electronAPI.selectFile({ filters: [{ name: 'Textdateien', extensions: ['txt', 'md', 'json'] }], }) if (!path) return null const content = await window.electronAPI.readFile(path) return { path, content } }, []) const saveFile = useCallback(async (path: string, content: string) => { await window.electronAPI.writeFile(path, content) window.electronAPI.showNotification('Gespeichert', `${path} wurde gespeichert.`) }, []) return { selectAndReadFile, saveFile, platform: window.electronAPI.platform } }

IPC IPC-Channels: Namenskonvention

Verwende das Format namespace:action für alle IPC-Channels: fs:read-file, dialog:open-file, app:get-version, update:check. Dadurch bleiben Channels organisiert und skalierbar. Dokumentiere alle Channels in einer zentralen ipc-channels.ts-Datei mit Typexporten.

3. Vite + React Integration mit electron-vite

electron-vite ist 2026 der Standard für Electron-Build-Setups. Es konfiguriert automatisch separate Vite-Instanzen für Main Process, Preload und Renderer — mit Hot Module Replacement (HMR) im Renderer, TypeScript-Support und optimierten Produktions-Bundles.

Projekt-Setup mit electron-vite

# Neues Projekt erstellen npm create electron-vite@latest meine-app -- --template react-ts cd meine-app npm install npm run dev # Startet App + HMR # Projektstruktur: # src/ # main/ → Main Process (Node.js) # preload/ → Preload Script (contextBridge) # renderer/ → React App (Chromium) # resources/ → Icons, native Assets # electron.vite.config.ts

electron.vite.config.ts

// electron.vite.config.ts import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import react from '@vitejs/plugin-react' import { resolve } from 'path' export default defineConfig({ main: { plugins: [externalizeDepsPlugin()], // Node-Deps extern halten build: { rollupOptions: { input: { index: resolve('src/main/main.ts') }, }, }, }, preload: { plugins: [externalizeDepsPlugin()], build: { rollupOptions: { input: { index: resolve('src/preload/preload.ts') }, }, }, }, renderer: { resolve: { alias: { '@': resolve('src/renderer/src'), '@components': resolve('src/renderer/src/components'), '@hooks': resolve('src/renderer/src/hooks'), }, }, plugins: [react()], server: { port: 5173, strictPort: true, }, build: { outDir: 'out/renderer', rollupOptions: { input: { index: resolve('src/renderer/index.html') }, }, }, }, })

HMR-Workflow im Development

// package.json Scripts { "scripts": { "dev": "electron-vite dev", // Startet Vite Dev Server + Electron "build": "electron-vite build", // Produktions-Bundle "start": "electron-vite preview", // Preview des Produktions-Builds "dist": "npm run build && electron-builder", "typecheck": "tsc --noEmit" } }

HMR-Vorteile mit electron-vite

React-App im Renderer: App.tsx

// src/renderer/src/App.tsx import { useState } from 'react' import { useElectron } from '@hooks/useElectron' import { FileEditor } from '@components/FileEditor' export default function App() { const [file, setFile] = useState<{ path: string; content: string } | null>(null) const { selectAndReadFile, saveFile, platform } = useElectron() const handleOpen = async () => { const result = await selectAndReadFile() if (result) setFile(result) } return ( <div className="app"> <header> <h1>Meine Desktop-App</h1> <span className="platform-badge">{platform}</span> <button onClick={handleOpen}>Datei öffnen</button> </header> {file && ( <FileEditor path={file.path} content={file.content} onSave={content => saveFile(file.path, content)} /> )} </div> ) }

4. Native Features: Tray, Dialoge, Notifications

Electron bietet Zugriff auf alle nativen Betriebssystem-APIs — System Tray, Dateidialoge, Desktop-Notifications, Clipboard und Shell-Integration. Diese machen den Unterschied zwischen einer Web-App und einer echten Desktop-Applikation.

Native Native API-Übersicht

System Tray mit Context Menu

// src/main/tray.ts import { Tray, Menu, nativeImage, BrowserWindow, app } from 'electron' import { join } from 'path' export function createTray(mainWindow: BrowserWindow): Tray { const icon = nativeImage.createFromPath( join(__dirname, '../../resources/tray-icon.png') ).resize({ width: 16, height: 16 }) const tray = new Tray(icon) const contextMenu = Menu.buildFromTemplate([ { label: 'Fenster anzeigen', click: () => { mainWindow.show() mainWindow.focus() }, }, { type: 'separator' }, { label: 'Synchronisierung', submenu: [ { label: 'Jetzt synchronisieren', click: () => mainWindow.webContents.send('sync:trigger') }, { label: 'Automatisch', type: 'checkbox', checked: true }, ], }, { type: 'separator' }, { label: 'Beenden', click: () => app.quit(), accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Alt+F4', }, ]) tray.setContextMenu(contextMenu) tray.setToolTip('Meine App — läuft im Hintergrund') // Doppelklick zeigt Fenster (Windows-Konvention) tray.on('double-click', () => mainWindow.show()) return tray }

Native App-Menu mit Tastaturkürzeln

// src/main/menu.ts import { Menu, MenuItemConstructorOptions, app, shell, BrowserWindow } from 'electron' export function buildAppMenu(): Menu { const isMac = process.platform === 'darwin' const template: MenuItemConstructorOptions[] = [ // macOS-spezifisches App-Menu ...(isMac ? [{ label: app.name, submenu: [ { role: 'about' as const }, { type: 'separator' as const }, { role: 'hide' as const }, { role: 'hideOthers' as const }, { type: 'separator' as const }, { role: 'quit' as const }, ], }] : []), { label: 'Datei', submenu: [ { label: 'Neue Datei', accelerator: 'CmdOrCtrl+N', click: () => BrowserWindow.getFocusedWindow()?.webContents.send('file:new'), }, { label: 'Öffnen...', accelerator: 'CmdOrCtrl+O', click: () => BrowserWindow.getFocusedWindow()?.webContents.send('file:open'), }, { label: 'Speichern', accelerator: 'CmdOrCtrl+S', click: () => BrowserWindow.getFocusedWindow()?.webContents.send('file:save'), }, { type: 'separator' }, isMac ? { role: 'close' as const } : { role: 'quit' as const }, ], }, { label: 'Hilfe', submenu: [ { label: 'Dokumentation', click: () => shell.openExternal('https://agentic-movers.com/docs'), }, { role: 'toggleDevTools' }, ], }, ] return Menu.buildFromTemplate(template) }

5. Auto-Update mit electron-updater

Automatische Updates sind für Desktop-Apps entscheidend. electron-updater (Teil von electron-builder) unterstützt GitHub Releases, S3 und eigene Update-Server. Im Main Process prüft autoUpdater beim Start und benachrichtigt den Renderer über verfügbare Updates.

Wichtig: Auto-Updates funktionieren nur bei signierten und notarisierten Apps (macOS) bzw. signierten Installern (Windows NSIS). Im Development-Modus wird Auto-Update automatisch deaktiviert.

updater.ts — Update-Logik im Main Process

// src/main/updater.ts import { autoUpdater } from 'electron-updater' import { BrowserWindow, dialog } from 'electron' import log from 'electron-log' export function setupAutoUpdater(mainWindow: BrowserWindow): void { // Logging aktivieren autoUpdater.logger = log log.transports.file.level = 'info' // Update-URL (GitHub Releases) autoUpdater.setFeedURL({ provider: 'github', owner: 'meinorg', repo: 'meine-app', private: false, }) // Events → Renderer weiterleiten autoUpdater.on('update-available', (info) => { log.info(`Update verfügbar: ${info.version}`) mainWindow.webContents.send('update:available', info.version) }) autoUpdater.on('update-not-available', () => { log.info('App ist aktuell.') }) autoUpdater.on('download-progress', (progress) => { mainWindow.webContents.send('update:progress', Math.round(progress.percent)) mainWindow.setProgressBar(progress.percent / 100) // Windows Taskbar-Progress }) autoUpdater.on('update-downloaded', (info) => { log.info(`Update ${info.version} heruntergeladen`) mainWindow.setProgressBar(-1) // Taskbar-Progress entfernen const response = dialog.showMessageBoxSync(mainWindow, { type: 'info', buttons: ['Jetzt neu starten', 'Später'], title: 'Update verfügbar', message: `Version ${info.version} wurde heruntergeladen.`, detail: 'Jetzt neu starten um das Update zu installieren?', }) if (response === 0) autoUpdater.quitAndInstall() }) autoUpdater.on('error', (err) => { log.error('Auto-Updater Fehler:', err) mainWindow.webContents.send('update:error', err.message) }) // Beim Start nach Updates suchen (kurze Verzögerung) setTimeout(() => { if (process.env.NODE_ENV !== 'development') { autoUpdater.checkForUpdatesAndNotify() } }, 3000) }

Update-Banner in React

// src/renderer/src/components/UpdateBanner.tsx import { useState, useEffect } from 'react' export function UpdateBanner() { const [updateVersion, setUpdateVersion] = useState<string | null>(null) const [progress, setProgress] = useState<number | null>(null) useEffect(() => { window.electronAPI.onUpdateAvailable((version) => setUpdateVersion(version)) // Progress-Event direkt über ipcRenderer (via erweitertes Preload) return () => { /* cleanup listeners */ } }, []) if (!updateVersion) return null return ( <div className="update-banner"> <span>Update {updateVersion} verfügbar</span> {progress !== null && ( <div className="progress-bar"> <div style={{ width: `${progress}%` }} /> </div> )} </div> ) }

6. Build & Code-Signing: Windows, macOS, Linux

electron-builder erzeugt plattform-spezifische Distributionen. Code-Signing ist auf macOS (Notarisierung Pflicht seit macOS Catalina) und Windows (SmartScreen-Filter umgehen) entscheidend für eine professionelle User-Experience ohne Sicherheitswarnungen.

electron-builder.yml

# electron-builder.yml appId: com.meinefirma.meineapp productName: Meine Desktop App copyright: Copyright © 2026 Meine Firma GmbH # Build-Artefakte Verzeichnis directories: output: release # Dateien die in die App eingebunden werden files: - out/**/* - resources/**/* - "!**/*.{map,ts}" # Auto-Update: publish auf GitHub Releases publish: - provider: github owner: meinorg repo: meine-app # macOS Build + Code-Signing mac: category: public.app-category.productivity target: - target: dmg arch: [x64, arm64] # Universal Binary - target: zip hardenedRuntime: true gatekeeperAssess: false entitlements: resources/entitlements.mac.plist entitlementsInherit: resources/entitlements.mac.plist # macOS Notarisierung (Apple ID + App-spezifisches Passwort) afterSign: scripts/notarize.js # Windows Build + NSIS Installer + Code-Signing win: target: nsis certificateFile: ${env.WIN_CERT_FILE} # aus Umgebungsvariable certificatePassword: ${env.WIN_CERT_PASS} signingHashAlgorithms: [sha256] nsis: oneClick: false allowToChangeInstallationDirectory: true createDesktopShortcut: true runAfterFinish: true # Linux: AppImage + Debian-Paket linux: target: - AppImage - deb category: Utility maintainer: info@meinefirma.de desktop: Name: Meine Desktop App Comment: Produktivitäts-Tool 2026

macOS Notarisierung: notarize.js

// scripts/notarize.js — Wird von electron-builder aufgerufen const { notarize } = require('@electron/notarize') module.exports = async function (context) { const { electronPlatformName, appOutDir } = context if (electronPlatformName !== 'darwin') return const appName = context.packager.appInfo.productFilename const appBundleId = 'com.meinefirma.meineapp' console.log(`Notarisierung: ${appName}.app`) await notarize({ tool: 'notarytool', // Xcode 13+: notarytool appPath: `${appOutDir}/${appName}.app`, appleId: process.env.APPLE_ID, // Apple Developer ID appleIdPassword: process.env.APPLE_ID_PASS, // App-spezifisches Passwort teamId: process.env.APPLE_TEAM_ID, }) console.log('Notarisierung erfolgreich!') }

GitHub Actions CI/CD Build-Pipeline

# .github/workflows/release.yml name: Release Build on: push: tags: ['v*'] jobs: build: strategy: matrix: os: [macos-latest, windows-latest, ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Dependencies installieren run: npm ci - name: TypeScript prüfen run: npm run typecheck - name: App bauen & publishen env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASS: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} WIN_CERT_FILE: ${{ secrets.WIN_CERTIFICATE_FILE }} WIN_CERT_PASS: ${{ secrets.WIN_CERTIFICATE_PASSWORD }} CSC_LINK: ${{ secrets.MAC_CERTS }} CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }} run: npm run dist -- --publish always

Build Code-Signing Checkliste

electron-vite Build-Optimierungen

// src/main/main.ts — Produktions-Optimierungen // 1. Splash Screen bei langen Ladezeiten const splash = new BrowserWindow({ width: 400, height: 300, frame: false, transparent: true, alwaysOnTop: true, }) splash.loadFile('resources/splash.html') // 2. Main Window nach Splash schließen mainWindow.once('ready-to-show', () => { splash.destroy() mainWindow!.show() }) // 3. Session-Cache für Assets konfigurieren mainWindow.webContents.session.setPreloads([ join(__dirname, '../preload/preload.js') ]) // 4. Zoom-Level sperren (Accessibility bewahren) mainWindow.webContents.on('did-finish-load', () => { mainWindow!.webContents.setZoomLevel(0) }) // 5. Fensterposition persistieren const bounds = store.get('windowBounds', { width: 1280, height: 800 }) mainWindow.setBounds(bounds) mainWindow.on('close', () => store.set('windowBounds', mainWindow!.getBounds()))

Fazit: Electron 2026 mit Claude Code

Electron ist trotz seines Rufes als “zu schwer” nach wie vor die pragmatischste Wahl für Cross-Platform-Desktop-Apps wenn das Team Web-Skills mitbringt. Mit electron-vite, React 18 und TypeScript ist der Development-Workflow 2026 deutlich schneller als noch vor drei Jahren.

Claude Code beschleunigt die Electron-Entwicklung besonders in diesen Bereichen: IPC-Handler typen-sicher aufbauen, contextBridge-APIs konsistent halten, plattform-spezifische Conditional-Logik für macOS/Windows/Linux, und den oft komplexen Build/Code-Signing-Prozess in CI/CD abbilden.

Stack-Empfehlung 2026

Alternativen zu Electron: Tauri (Rust-basiert, deutlich kleiner, kein Chromium-Bundle) ist 2026 für neue Projekte eine starke Alternative wenn Bunde-Size und RAM-Verbrauch kritisch sind. Für Teams mit React-Erfahrung bleibt Electron die produktivere Wahl.

Desktop-Apps mit KI entwickeln?

Starte deinen kostenlosen Trial und lass Claude Code deine nächste Electron-App aufbauen — von der Architektur bis zum signierten Installer.

Kostenlos ausprobieren →