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

Electron.js ermöglicht es, plattformübergreifende Desktop-Apps mit HTML, CSS und JavaScript zu bauen — und wird von VS Code, Slack, Figma und Discord eingesetzt. Mit Claude Code entstehen produktionsreife Electron-Apps inklusive IPC, nativer APIs, Auto-Updater und Code-Signing in Stunden statt Wochen.

Electron-Architektur — Main Process, Renderer Process, Preload Script, IPC

Electron kombiniert Chromium (für die UI) mit Node.js (für native OS-Zugriffe) in einer einzigen Runtime. Das Sicherheitsmodell trennt dabei strikt, welcher Code auf welche Ressourcen zugreifen darf.

ArchitekturDie drei Kernkomponenten von Electron

# Electron Prozess-Architektur 2026 # ┌──────────────────────────────────────────────────────┐ # │ MAIN PROCESS (Node.js) │ # │ main.ts — App-Lifecycle, Window-Management │ # │ Vollzugriff auf Node.js + Electron APIs │ # │ ┌────────────────────┐ ┌────────────────────────┐ │ # │ │ BrowserWindow 1 │ │ BrowserWindow 2 │ │ # │ │ ┌──────────────┐ │ │ ┌──────────────────┐ │ │ # │ │ │ Preload.ts │ │ │ │ Preload.ts │ │ │ # │ │ │ contextBridge│ │ │ │ contextBridge │ │ │ # │ │ └──────────────┘ │ │ └──────────────────┘ │ │ # │ │ Renderer Process │ │ Renderer Process │ │ # │ │ (Chromium + React)│ │ (Chromium + React) │ │ # │ └────────────────────┘ └────────────────────────┘ │ # └──────────────────────────────────────────────────────┘ # # IPC (Inter-Process Communication): # Renderer → Main: ipcRenderer.invoke('channel', data) # Main → Renderer: mainWindow.webContents.send('channel', data) # Preload = sichere Bridge (kein direktes Node.js im Renderer!)
Security-Prinzip: Seit Electron 12 ist contextIsolation: true Standard. Der Renderer-Prozess hat keinen direkten Zugriff auf Node.js — nur auf das, was du explizit via contextBridge freigibst. Claude Code kennt dieses Modell und generiert immer sicheren Code.

Main Processmain.ts — App-Lifecycle und Window-Management

// src/main/main.ts import { app, BrowserWindow, ipcMain, shell } from 'electron' import { join } from 'path' function createWindow() { const win = new BrowserWindow({ width: 1280, height: 800, minWidth: 800, minHeight: 600, titleBarStyle: 'hiddenInset', // macOS: natives Fenster-Look trafficLightPosition: { x: 16, y: 16 }, webPreferences: { preload: join(__dirname, '../preload/preload.js'), contextIsolation: true, // PFLICHT für Sicherheit nodeIntegration: false, // NIEMALS true in Produktion! sandbox: true, // Zusätzliche Isolation }, }) // Entwicklung: Vite Dev Server if (process.env.NODE_ENV === 'development') { win.loadURL('http://localhost:5173') win.webContents.openDevTools() } else { win.loadFile(join(__dirname, '../renderer/index.html')) } // Externe Links im System-Browser öffnen win.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url) return { action: 'deny' } }) return win } app.whenReady().then(() => { createWindow() app.on('activate', () => { // macOS: Fenster öffnen wenn Dock-Icon angeklickt if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) app.on('window-all-closed', () => { // macOS: App bleibt im Dock auch ohne Fenster if (process.platform !== 'darwin') app.quit() })

Preloadpreload.ts — die sichere Bridge zum Renderer

// src/preload/preload.ts import { contextBridge, ipcRenderer } from 'electron' // Nur explizit freigegebene APIs sind im Renderer verfügbar contextBridge.exposeInMainWorld('electronAPI', { // Filesystem readFile: (path: string) => ipcRenderer.invoke('fs:readFile', path), writeFile: (path: string, content: string) => ipcRenderer.invoke('fs:writeFile', path, content), // Dialog openFileDialog: (options?: Electron.OpenDialogOptions) => ipcRenderer.invoke('dialog:openFile', options), saveFileDialog: (options?: Electron.SaveDialogOptions) => ipcRenderer.invoke('dialog:saveFile', options), // App-Infos getVersion: () => ipcRenderer.invoke('app:getVersion'), getPlatform: () => process.platform, // Events vom Main-Process empfangen onUpdateAvailable: (callback: (info: unknown) => void) => { ipcRenderer.on('update:available', (_event, info) => callback(info)) }, removeAllListeners: (channel: string) => ipcRenderer.removeAllListeners(channel), }) // TypeScript-Typen für den Renderer // → in src/renderer/electron.d.ts: // interface Window { electronAPI: typeof window.electronAPI }
Sicherheitsregel: Niemals ipcRenderer direkt im Renderer-Code importieren. Alle IPC-Calls laufen ausschließlich über contextBridge. Claude Code hält diese Regel automatisch ein und warnt bei Abweichungen.

electron-vite Setup — Vite als Build-Tool, Hot Reload, TypeScript, React

electron-vite ist der moderne Build-Stack für Electron-Apps: Vite für Main, Preload und Renderer — mit Hot Module Replacement im Renderer und automatischem Neustart des Main Process bei Änderungen.

SetupNeues Electron-Projekt mit electron-vite

# Prompt für Claude Code: # "Erstelle ein neues Electron-Projekt mit electron-vite, # React + TypeScript, shadcn/ui und Tailwind CSS" # 1. Scaffold mit offiziellem Template npm create @quick-start/electron@latest my-electron-app -- \ --template react-ts cd my-electron-app && npm install # Projektstruktur die electron-vite erstellt: # src/ # main/ → main.ts (Node.js + Electron APIs) # preload/ → index.ts (contextBridge) # renderer/ → React-App (index.html, main.tsx) # electron.vite.config.ts # electron-builder.yml # 2. Dev-Modus starten (Hot Reload + DevTools) npm run dev # 3. Produktions-Build npm run build # TypeScript kompilieren + Vite bundlen npm run preview # Build lokal testen

Configelectron.vite.config.ts — Vite für alle drei Prozesse

// electron.vite.config.ts import { resolve } from 'path' import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import react from '@vitejs/plugin-react' export default defineConfig({ main: { plugins: [externalizeDepsPlugin()], // node_modules nicht bundlen build: { rollupOptions: { input: { index: resolve(__dirname, 'src/main/index.ts') } } } }, preload: { plugins: [externalizeDepsPlugin()], build: { rollupOptions: { input: { index: resolve(__dirname, 'src/preload/index.ts') } } } }, renderer: { resolve: { alias: { '@renderer': resolve('src/renderer/src'), '@shared': resolve('src/shared'), // Shared Types } }, plugins: [react()], server: { port: 5173, strictPort: true, }, build: { rollupOptions: { input: { index: resolve(__dirname, 'src/renderer/index.html') } } } } })

TypeScriptShared Types zwischen Main und Renderer

// src/shared/ipc-channels.ts — Typen für BEIDE Prozesse export const IPC = { FS_READ: 'fs:readFile', FS_WRITE: 'fs:writeFile', DIALOG_OPEN: 'dialog:openFile', DIALOG_SAVE: 'dialog:saveFile', APP_VERSION: 'app:getVersion', UPDATE_CHECK: 'update:check', UPDATE_INSTALL: 'update:install', } as const export type IpcChannel = (typeof IPC)[keyof typeof IPC] export interface FileReadResult { path: string content: string size: number mtime: number } export interface UpdateInfo { version: string releaseDate: string releaseNotes: string downloadUrl: string } // src/renderer/src/electron.d.ts — Window-Typen für Renderer interface Window { electronAPI: { readFile(path: string): Promise<FileReadResult> writeFile(path: string, content: string): Promise<void> openFileDialog(opts?: Electron.OpenDialogOptions): Promise<string[]> saveFileDialog(opts?: Electron.SaveDialogOptions): Promise<string | null> getVersion(): Promise<string> getPlatform(): NodeJS.Platform onUpdateAvailable(cb: (info: UpdateInfo) => void): void removeAllListeners(channel: string): void } }
Shared Types: Typen in src/shared/ können von Main und Renderer importiert werden — ohne Duplikation. Claude Code legt diesen Ordner automatisch an und nutzt ihn konsequent für Kanalnamen und Interfaces.

IPC Communication — ipcMain/ipcRenderer, contextBridge, type-safe channels

IPC (Inter-Process Communication) ist der Herzschlag jeder Electron-App. Alle OS-nahen Operationen, die im Renderer angefragt werden, wandern über IPC in den Main Process — und das Ergebnis kommt typsicher zurück.

IPCipcMain-Handler im Main Process

// src/main/ipc-handlers.ts import { ipcMain, dialog, app, BrowserWindow } from 'electron' import { promises as fs } from 'fs' import { IPC } from '../../shared/ipc-channels' export function registerIpcHandlers() { // Datei lesen — invoke/handle (Request-Response) ipcMain.handle(IPC.FS_READ, async (_event, path: string) => { const content = await fs.readFile(path, 'utf-8') const stat = await fs.stat(path) return { path, content, size: stat.size, mtime: stat.mtimeMs } }) // Datei schreiben ipcMain.handle(IPC.FS_WRITE, async (_event, path: string, content: string) => { await fs.writeFile(path, content, 'utf-8') }) // Öffnen-Dialog ipcMain.handle(IPC.DIALOG_OPEN, async (_event, options) => { const win = BrowserWindow.getFocusedWindow()! const result = await dialog.showOpenDialog(win, { properties: ['openFile', 'multiSelections'], ...options, }) return result.canceled ? [] : result.filePaths }) // Speichern-Dialog ipcMain.handle(IPC.DIALOG_SAVE, async (_event, options) => { const win = BrowserWindow.getFocusedWindow()! const result = await dialog.showSaveDialog(win, options) return result.canceled ? null : result.filePath }) // App-Version ipcMain.handle(IPC.APP_VERSION, () => app.getVersion()) } // in main.ts registrieren: // import { registerIpcHandlers } from './ipc-handlers' // app.whenReady().then(() => { registerIpcHandlers(); createWindow() })

IPCIPC im Renderer — React Hook mit Typsicherheit

// src/renderer/src/hooks/useElectronFS.ts import { useState, useCallback } from 'react' import type { FileReadResult } from '@shared/ipc-channels' export function useElectronFS() { const [loading, setLoading] = useState(false) const [error, setError] = useState<string | null>(null) const openAndRead = useCallback(async () => { setLoading(true) setError(null) try { const paths = await window.electronAPI.openFileDialog({ filters: [{ name: 'Text', extensions: ['txt', 'md', 'json'] }] }) if (!paths.length) return null const result: FileReadResult = await window.electronAPI.readFile(paths[0]) return result } catch (e) { setError(e instanceof Error ? e.message : 'Unbekannter Fehler') return null } finally { setLoading(false) } }, []) const saveFile = useCallback(async (content: string) => { const path = await window.electronAPI.saveFileDialog({ defaultPath: 'document.txt', filters: [{ name: 'Text', extensions: ['txt'] }] }) if (!path) return false await window.electronAPI.writeFile(path, content) return true }, []) return { openAndRead, saveFile, loading, error } }

Native APIs — Filesystem, System-Tray, Notifications, Shell, Dialog

Electron gibt deiner Web-App vollen Zugriff auf Betriebssystem-Features: vom Dateisystem über System-Tray-Icons bis zu nativen Desktop-Benachrichtigungen. Claude Code kennt alle Electron-APIs und generiert plattformübergreifend kompatiblen Code.

NativeSystem-Tray mit Kontextmenü

// src/main/tray.ts import { Tray, Menu, app, nativeImage } from 'electron' import { join } from 'path' let tray: Tray | null = null export function createTray(showWindow: () => void) { // Icon: @2x für Retina-Displays (macOS) const iconPath = join(__dirname, '../../resources/tray-icon.png') const icon = nativeImage.createFromPath(iconPath) tray = new Tray(icon) tray.setToolTip('My Electron App') const contextMenu = Menu.buildFromTemplate([ { label: 'Öffnen', click() { showWindow() }, }, { type: 'separator' }, { label: 'Version', sublabel: app.getVersion(), enabled: false, }, { type: 'separator' }, { label: 'Beenden', role: 'quit', }, ]) tray.setContextMenu(contextMenu) // macOS: Klick auf Tray-Icon → Fenster anzeigen tray.on('click', showWindow) return tray } export function destroyTray() { tray?.destroy() tray = null }

NativeDesktop-Benachrichtigungen + Shell-Integration

// src/main/notifications.ts import { Notification, shell, app } from 'electron' import { join } from 'path' export function sendNotification(title: string, body: string, onClick?: () => void) { if (!Notification.isSupported()) return const notification = new Notification({ title, body, icon: join(__dirname, '../../resources/icon.png'), silent: false, }) if (onClick) notification.on('click', onClick) notification.show() } // Shell-Utilities — im Renderer via IPC aufrufen export function registerShellHandlers() { const { ipcMain } = require('electron') // URL im Standard-Browser öffnen ipcMain.handle('shell:openExternal', async (_e, url: string) => { await shell.openExternal(url) }) // Datei im Finder/Explorer anzeigen ipcMain.handle('shell:showItemInFolder', (_e, path: string) => { shell.showItemInFolder(path) }) // Datei mit zugehöriger App öffnen ipcMain.handle('shell:openPath', async (_e, path: string) => { const err = await shell.openPath(path) if (err) throw new Error(err) }) // App-Daten-Verzeichnis (plattformübergreifend) ipcMain.handle('app:getUserDataPath', () => app.getPath('userData')) ipcMain.handle('app:getDownloadsPath', () => app.getPath('downloads')) }
Platform-Pfade: app.getPath('userData') gibt den plattformspezifischen App-Daten-Ordner zurück: ~/Library/Application Support (macOS), %APPDATA% (Windows), ~/.config (Linux). Claude Code nutzt diese API statt hardcodierter Pfade.

NativeNatives Datei-Watching mit chokidar

// src/main/file-watcher.ts — Dateiänderungen in Echtzeit import chokidar from 'chokidar' import { BrowserWindow } from 'electron' const watchers = new Map<string, chokidar.FSWatcher>() export function watchDirectory(dirPath: string, win: BrowserWindow) { if (watchers.has(dirPath)) return const watcher = chokidar.watch(dirPath, { persistent: true, ignoreInitial: true, ignored: /(^|[\/\\])\../, // Dotfiles ignorieren awaitWriteFinish: { stabilityThreshold: 200 }, }) watcher .on('add', path => win.webContents.send('fs:change', { type: 'add', path })) .on('change', path => win.webContents.send('fs:change', { type: 'change', path })) .on('unlink', path => win.webContents.send('fs:change', { type: 'remove', path })) watchers.set(dirPath, watcher) } export function stopWatching(dirPath: string) { watchers.get(dirPath)?.close() watchers.delete(dirPath) } // Beim App-Beenden alle Watcher schließen export function closeAllWatchers() { watchers.forEach(w => w.close()) watchers.clear() }

Auto-Updater — electron-updater, GitHub Releases, delta updates

Mit electron-updater können Nutzer Updates im Hintergrund herunterladen und beim nächsten Start installieren. Claude Code konfiguriert den gesamten Update-Flow — inklusive UI-Benachrichtigung im Renderer und GitHub Releases als Update-Server.

Updaterelectron-updater Setup — GitHub Releases

# Installation npm install electron-updater # electron-builder.yml — Update-Konfiguration # publish: # provider: github # owner: mein-github-user # repo: meine-electron-app # releaseType: release
// src/main/updater.ts import { autoUpdater } from 'electron-updater' import { BrowserWindow, ipcMain } from 'electron' import log from 'electron-log' export function setupAutoUpdater(win: BrowserWindow) { autoUpdater.logger = log autoUpdater.autoDownload = true // Im Hintergrund laden autoUpdater.autoInstallOnAppQuit = true // Beim Beenden installieren // Events → an Renderer weiterleiten autoUpdater.on('checking-for-update', () => win.webContents.send('update:checking')) autoUpdater.on('update-available', (info) => win.webContents.send('update:available', info)) autoUpdater.on('update-not-available', () => win.webContents.send('update:not-available')) autoUpdater.on('download-progress', (progress) => win.webContents.send('update:progress', progress)) autoUpdater.on('update-downloaded', (info) => { win.webContents.send('update:downloaded', info) }) autoUpdater.on('error', (err) => win.webContents.send('update:error', err.message)) // IPC: Renderer kann manuell triggern ipcMain.handle('update:check', () => autoUpdater.checkForUpdates()) ipcMain.handle('update:install', () => autoUpdater.quitAndInstall()) // Automatisch beim Start prüfen (nach 3 Sekunden) setTimeout(() => autoUpdater.checkForUpdates(), 3000) }

UpdaterUpdate-Banner im Renderer — React-Komponente

// src/renderer/src/components/UpdateBanner.tsx import { useEffect, useState } from 'react' type UpdateState = 'idle' | 'available' | 'downloading' | 'ready' interface DownloadProgress { percent: number bytesPerSecond: number transferred: number total: number } export function UpdateBanner() { const [state, setState] = useState<UpdateState>('idle') const [version, setVersion] = useState<string>('') const [progress, setProgress] = useState<number>(0) useEffect(() => { window.electronAPI.onUpdateAvailable((info: any) => { setVersion(info.version) setState('available') }) // Weitere Events über contextBridge registrieren... return () => window.electronAPI.removeAllListeners('update:available') }, []) if (state === 'idle') return null return ( <div className="update-banner"> {state === 'available' && ( <> <span>Update {version} verfügbar</span> <button onClick={() => window.electronAPI.getVersion()}> Jetzt laden </button> </> )} {state === 'downloading' && ( <span>Download: {Math.round(progress)}%</span> )} {state === 'ready' && ( <button onClick={() => window.electronAPI.getVersion()}> Neustart & Update installieren </button> )} </div> ) }
Delta Updates: electron-updater unterstützt differenzielle Updates für macOS (NSIS auf Windows verfügbar). Nur geänderte Blöcke werden heruntergeladen — das spart Bandbreite und Zeit für deine Nutzer erheblich.

Packaging und Signing — electron-builder, Mac codesigning, Windows NSIS, Linux AppImage

electron-builder ist der Standard für Electron-Releases: ein Build für alle Plattformen, automatisches Code-Signing und Upload zu GitHub Releases. Claude Code generiert die komplette CI/CD-Pipeline inklusive Signing-Setup.

Buildelectron-builder.yml — vollständige Konfiguration

# electron-builder.yml appId: com.meinefirma.meinapp productName: Meine App copyright: Copyright © 2026 Meine Firma GmbH directories: buildResources: resources # Icons, Installer-Bilder output: dist-electron # Build-Output files: - dist/**/* - "!**/*.map" # Source Maps ausschließen - "!**/*.d.ts" # macOS Konfiguration mac: target: - target: dmg - target: zip # Für auto-updater category: public.app-category.productivity hardenedRuntime: true # Pflicht für Notarization gatekeeperAssess: false entitlements: resources/entitlements.mac.plist entitlementsInherit: resources/entitlements.mac.plist identity: "Developer ID Application: Meine Firma (TEAM123456)" dmg: contents: - x: 130, y: 220 - x: 410, y: 220, type: link, path: /Applications # Windows Konfiguration win: target: - target: nsis # Installer - target: portable # Portable EXE certificateSubjectName: "Meine Firma GmbH" signingHashAlgorithms: [sha256] timeStampServer: http://timestamp.digicert.com nsis: oneClick: false # Interaktiver Installer allowToChangeInstallationDirectory: true createDesktopShortcut: true createStartMenuShortcut: true shortcutName: Meine App # Linux Konfiguration linux: target: - AppImage - deb - rpm category: Office icon: resources/icon.png # GitHub Releases publish: provider: github owner: mein-github-user repo: meine-electron-app releaseType: release

CI/CDGitHub Actions — Build, Sign & Release für alle Plattformen

# .github/workflows/release.yml name: Release on: push: tags: ['v*'] jobs: release: 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: Install dependencies run: npm ci - name: Build run: npm run build # macOS: Code-Signing + Notarization - name: Sign macOS (if macOS) if: matrix.os == 'macos-latest' env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_PW }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} CSC_LINK: ${{ secrets.MAC_CERT_BASE64 }} CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }} run: npm run release:mac # Windows: Code-Signing mit PFX-Zertifikat - name: Sign Windows (if Windows) if: matrix.os == 'windows-latest' env: CSC_LINK: ${{ secrets.WIN_CERT_BASE64 }} CSC_KEY_PASSWORD: ${{ secrets.WIN_CERT_PASSWORD }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm run release:win - name: Build Linux if: matrix.os == 'ubuntu-latest' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm run release:linux
macOS Notarization: Seit macOS 10.15 Catalina müssen Apps bei Apple notarisiert werden — sonst blockiert Gatekeeper die Installation. Der Prozess dauert 2–10 Minuten und läuft vollautomatisch über electron-builder wenn Apple ID und App-spezifisches Passwort als Secrets hinterlegt sind.

Buildpackage.json Scripts — Build-Workflow

// package.json — Scripts { "scripts": { "dev": "electron-vite dev", "build": "electron-vite build", "preview": "electron-vite preview", "typecheck": "tsc --noEmit", // Packaging (kein Upload) "pack": "npm run build && electron-builder --dir", // Release (Upload zu GitHub Releases) "release:mac": "npm run build && electron-builder --mac --publish always", "release:win": "npm run build && electron-builder --win --publish always", "release:linux":"npm run build && electron-builder --linux --publish always", // Version bumpen + Git Tag erstellen (triggert CI) "version:patch":"npm version patch && git push --follow-tags", "version:minor":"npm version minor && git push --follow-tags", "version:major":"npm version major && git push --follow-tags" } }
Claude Code Workflow: Prompt "Bumpe auf Version 1.2.0 und erstelle ein GitHub Release" — Claude Code führt npm version minor aus, pushed den Tag, und die CI/CD Pipeline baut automatisch signierte Installer für alle drei Plattformen.

VergleichElectron vs. Tauri vs. NeutralinoJS 2026

KriteriumElectronTauri 2NeutralinoJS
Bundle-Größe (min.)~80 MB~5 MB~2 MB
RAM-Verbrauch~120 MB~30 MB~20 MB
Node.js Backend✓ Voll✗ Rust~ Limitiert
npm-Ökosystem✓ Komplett~ Via Sidecar~ Eingeschränkt
Web-APIs Support✓ Chromium~ System WebView~ System WebView
macOS / Win / Linux✓ Alle✓ Alle✓ Alle
Lernkurve (Web-Devs)✓ Gering✗ Rust nötig~ Mittel
Produktionsreife✓ 10+ Jahre✓ Stabil~ Early
Wann Electron wählen: Du nutzt viel Node.js, brauchst das volle npm-Ökosystem, willst keine neue Sprache (Rust) lernen, oder migrierst eine bestehende Web-App. Für maximale Performance und kleine Bundle-Größe ist Tauri 2 die Alternative — erfordert aber Rust-Kenntnisse.

Desktop-Apps mit Claude Code entwickeln

Starte dein Electron-Projekt in Minuten — inklusive IPC, Auto-Updater und signierter Releases für Mac, Windows und Linux.

14 Tage kostenlos testen →