Electron Desktop Apps mit Claude Code entwickeln: IPC, preload, Packaging und Auto Update
Praxisguide für Electron mit Claude Code: contextIsolation, preload, IPC, Dateizugriff, Packaging und Updates.
Electron macht aus einer Weboberfläche eine Desktop-App für Windows, macOS und Linux. Der Preis dafür ist eine stärkere Verantwortung: Eine Electron-App kann lokale Dateien öffnen, Systemdialoge nutzen, Menüs verwalten und Updates ausliefern. Deshalb ist die Sicherheitsgrenze wichtiger als die Wahl des UI-Frameworks.
Claude Code ist in diesem Umfeld nützlich, wenn die Aufgaben klein bleiben: ein IPC-Kanal, eine preload-API, eine Dateizugriffsregel, eine Packaging-Anpassung. Eine komplette Desktop-App inklusive Installer und Auto Update in einem Schritt erzeugt zu große Diffs und erschwert die Prüfung.
Die aktuelle Grundlinie lautet: contextIsolation aktiv lassen, nodeIntegration im renderer deaktivieren, nur kleine APIs über preload freigeben und Eingaben im main process erneut validieren. Nutze dazu die offiziellen Dokumente zu Context Isolation, IPC, Distribution und autoUpdater.
Für die Abwägung gegen Tauri hilft der Tauri Development Guide. Für Typen an der preload/renderer-Grenze passt der Artikel zu TypeScript-Tipps.
Die drei Grenzen
| Schicht | Verantwortung | Gute Aufgabe für Claude Code | Review-Fokus |
|---|---|---|---|
| main process | BrowserWindow, OS-APIs, Dateien, Menüs, Updates | Einen IPC handler ergänzen | Validierung, Rechte, Fehler, Pfadregeln |
| preload | Minimaler Übergang zum renderer | Funktionen mit contextBridge freigeben | Kein rohes ipcRenderer |
| renderer | UI, Formulare, Zustand, Darstellung | Screen und Interaktion bauen | Kein direkter Node- oder Electron-Zugriff |
Der renderer soll das Dateisystem nicht direkt kennen. Er ruft eine konkrete preload-Funktion auf, preload sendet einen konkreten IPC-Aufruf, und der main process prüft und führt aus.
flowchart LR
User["User-Aktion"] --> Renderer["Renderer\nHTML / React"]
Renderer --> Preload["Preload\ncontextBridge"]
Preload --> IPC["IPC channel\nvalidierter payload"]
IPC --> Main["Main process\nOS und Dateien"]
Main --> Result["Typisiertes Ergebnis"]
Result --> Renderer
Main --> Update["Packaging / Auto Update"]
Minimaler Starter zum Kopieren
Der Starter nutzt bewusst nur Electron + TypeScript. Wenn diese Grenze sauber funktioniert, kannst du React, Vite oder ein anderes UI-Setup ergänzen.
mkdir electron-secure-notes
cd electron-secure-notes
npm init -y
npm install --save-dev electron typescript @types/node
npm pkg set main="dist/main.js"
npm pkg set scripts.dev="tsc -p tsconfig.json && electron ."
mkdir src
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"outDir": "dist",
"rootDir": "src",
"types": ["node", "electron"]
},
"include": ["src/**/*.ts"]
}
Der main process hält die Systemrechte und validiert Dateien.
// src/main.ts
import { app, BrowserWindow, dialog, ipcMain, session } from "electron";
import fs from "node:fs/promises";
import path from "node:path";
type OpenedTextFile = {
filePath: string;
content: string;
};
type SaveTextRequest = {
filePath: string;
content: string;
};
const allowedExtensions = new Set([".txt", ".md", ".json"]);
const allowedRoots = new Set<string>();
function createWindow() {
const win = new BrowserWindow({
width: 1100,
height: 760,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
});
win.loadFile(path.join(__dirname, "index.html"));
}
function registerCsp() {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
"Content-Security-Policy": [
"default-src 'self'; script-src 'self'; style-src 'self'"
]
}
});
});
}
function isInsideRoot(root: string, target: string) {
const relative = path.relative(root, target);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
async function assertOpenedPath(filePath: string) {
const extension = path.extname(filePath).toLowerCase();
if (!allowedExtensions.has(extension)) {
throw new Error("Unsupported file type.");
}
return filePath;
}
async function assertAllowedFile(filePath: string) {
const resolved = path.resolve(filePath);
const extension = path.extname(resolved).toLowerCase();
if (!allowedExtensions.has(extension)) {
throw new Error("Only txt, md, and json files are allowed.");
}
const insideAllowedRoot = Array.from(allowedRoots).some((root) =>
isInsideRoot(root, resolved)
);
if (!insideAllowedRoot) {
throw new Error("Open the file with the dialog before saving it.");
}
return resolved;
}
ipcMain.handle("dialog:openTextFile", async (): Promise<OpenedTextFile | null> => {
const result = await dialog.showOpenDialog({
title: "Open text file",
properties: ["openFile"],
filters: [{ name: "Text", extensions: ["txt", "md", "json"] }]
});
if (result.canceled || result.filePaths.length === 0) {
return null;
}
const filePath = path.resolve(result.filePaths[0]);
const safePath = await assertOpenedPath(filePath);
allowedRoots.add(path.dirname(safePath));
return {
filePath: safePath,
content: await fs.readFile(safePath, "utf8")
};
});
ipcMain.handle("file:saveText", async (_event, payload: SaveTextRequest) => {
if (
!payload ||
typeof payload.filePath !== "string" ||
typeof payload.content !== "string" ||
payload.content.length > 1_000_000
) {
throw new Error("Invalid save request.");
}
const filePath = await assertAllowedFile(payload.filePath);
await fs.writeFile(filePath, payload.content, "utf8");
return { ok: true };
});
app.whenReady().then(() => {
registerCsp();
createWindow();
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
preload gibt nur konkrete Fähigkeiten frei.
// src/preload.ts
import { contextBridge, ipcRenderer } from "electron";
type OpenedTextFile = {
filePath: string;
content: string;
};
type SaveTextRequest = {
filePath: string;
content: string;
};
contextBridge.exposeInMainWorld("desktop", {
openTextFile: () =>
ipcRenderer.invoke("dialog:openTextFile") as Promise<OpenedTextFile | null>,
saveTextFile: (payload: SaveTextRequest) => {
if (
!payload ||
typeof payload.filePath !== "string" ||
typeof payload.content !== "string"
) {
throw new Error("Invalid save payload.");
}
return ipcRenderer.invoke("file:saveText", payload) as Promise<{ ok: boolean }>;
}
});
Der renderer nutzt ausschließlich diese Oberfläche.
// src/renderer.ts
const openButton = document.querySelector<HTMLButtonElement>("#open");
const saveButton = document.querySelector<HTMLButtonElement>("#save");
const editor = document.querySelector<HTMLTextAreaElement>("#editor");
const status = document.querySelector<HTMLParagraphElement>("#status");
let currentFilePath: string | null = null;
if (!openButton || !saveButton || !editor || !status) {
throw new Error("Required UI elements are missing.");
}
openButton.addEventListener("click", async () => {
const file = await window.desktop.openTextFile();
if (!file) {
status.textContent = "Open canceled.";
return;
}
currentFilePath = file.filePath;
editor.value = file.content;
status.textContent = `Opened: ${file.filePath}`;
});
saveButton.addEventListener("click", async () => {
if (!currentFilePath) {
status.textContent = "Open a file first.";
return;
}
await window.desktop.saveTextFile({
filePath: currentFilePath,
content: editor.value
});
status.textContent = `Saved: ${currentFilePath}`;
});
Prompt für Claude Code
Ergänze in der bestehenden Electron + TypeScript App das Öffnen und Speichern von Markdown.
Constraints:
- keine Node.js APIs im renderer freigeben
- contextIsolation: true, nodeIntegration: false und sandbox: true beibehalten
- preload gibt nur window.desktop.openTextFile und window.desktop.saveTextFile frei
- kein rohes ipcRenderer und kein generisches invoke(channel, payload)
- nur md/txt/json zulassen
- bestehenden Auto-Update-Code nicht ändern
Definition of done:
- npm run typecheck läuft durch
- Änderungen in main/preload/renderer kurz erklären
- 3 Security-Review-Punkte nennen
Drei praktische Use Cases
Erster Use Case: eine interne Markdown-Notiz-App. Dokumente bleiben lokal, ohne Upload auf einen Server. Speichere nur die Liste zuletzt geöffneter Dateien, nicht den vertraulichen Inhalt, und prüfe Pfade beim Start erneut.
Zweiter Use Case: ein Log Viewer für Support-Teams. Logs können groß sein und E-Mails, Tokens oder Kundendaten enthalten. Lies sie im main process seitenweise und sende dem renderer nur den sichtbaren Abschnitt. Beim Kopieren oder Exportieren sollten Geheimnisse maskiert werden.
Dritter Use Case: eine Desktop-Workbench für AI Prompts. Lokale Vorlagen, Clipboard und externe APIs kommen zusammen. API Keys bleiben im main process; der renderer erhält nur Status und Ergebnisse.
Konkrete Fehler
Der gefährlichste Fehler ist, jeden Pfad zu lesen, den der renderer sendet. Auch eine sichere UI macht den renderer nicht vertrauenswürdig. Extension, Größe und erlaubtes Root müssen im main process geprüft werden.
Der zweite Fehler ist window.api.invoke(channel, payload). Das wirkt elegant, aber es ist ein generischer Tunnel. Besser ist eine Methode pro Fähigkeit.
Der dritte Fehler sind Pfade, die nur in der Entwicklung funktionieren. src zur Laufzeit zu lesen oder in asar zu schreiben bricht im Paket. Schreibbare Daten gehören unter app.getPath("userData").
Der vierte Fehler ist, Signierung und Updates erst kurz vor Release zu testen. Signatur, Feed, GitHub Release Rechte und Windows-Installer-Verhalten gehören früh in CI. Der CI/CD Guide passt dazu.
Packaging und Auto Update
Mit Electron Forge startest du so:
npm install --save-dev @electron-forge/cli @electron-forge/maker-squirrel @electron-forge/maker-zip @electron-forge/maker-deb @electron-forge/maker-rpm
npx electron-forge import
// forge.config.ts
import type { ForgeConfig } from "@electron-forge/shared-types";
const config: ForgeConfig = {
packagerConfig: {
asar: true,
icon: "assets/icon"
},
rebuildConfig: {},
makers: [
{
name: "@electron-forge/maker-squirrel",
config: {
name: "secure_notes"
}
},
{
name: "@electron-forge/maker-zip",
platforms: ["darwin"]
},
{
name: "@electron-forge/maker-deb",
config: {}
},
{
name: "@electron-forge/maker-rpm",
config: {}
}
]
};
export default config;
Der eingebaute autoUpdater ist vor allem für macOS und Windows gedacht. Unter Linux übernimmt häufig der Paketmanager. Auf macOS ist Signierung Voraussetzung, unter Windows hängt das Verhalten von MSIX oder Squirrel.Windows ab.
// src/update.ts
import { app, autoUpdater, BrowserWindow } from "electron";
function sendStatus(win: BrowserWindow, status: string) {
win.webContents.send("update:status", status);
}
export function configureAutoUpdate(win: BrowserWindow) {
if (!app.isPackaged) {
sendStatus(win, "updates disabled in development");
return;
}
if (process.platform === "linux") {
sendStatus(win, "use the distribution package manager on Linux");
return;
}
const owner = "YOUR_GITHUB_OWNER";
const repo = "YOUR_GITHUB_REPO";
const feedUrl = `https://update.electronjs.org/${owner}/${repo}/${process.platform}-${process.arch}/${app.getVersion()}`;
autoUpdater.setFeedURL({ url: feedUrl });
autoUpdater.on("checking-for-update", () => sendStatus(win, "checking"));
autoUpdater.on("update-available", () => sendStatus(win, "available"));
autoUpdater.on("update-not-available", () => sendStatus(win, "not available"));
autoUpdater.on("update-downloaded", () => sendStatus(win, "downloaded"));
autoUpdater.on("error", (error) => sendStatus(win, `error: ${error.message}`));
setTimeout(() => {
try {
autoUpdater.checkForUpdates();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
sendStatus(win, `error: ${message}`);
}
}, 10_000);
}
Lass Claude Code zusätzlich dokumentieren, was produktiv noch fehlt: Signing Identity, Feed Owner, Release-Rechte, Secrets in CI und Rollback-Strategie.
Fazit
Claude Code beschleunigt Electron-Projekte deutlich. Die Qualität entsteht aber durch klare Grenzen: renderer für UI, preload als schmaler Übergang, main process für OS-Zugriff und Packaging-Tests vor dem Release.
ClaudeCodeLab unterstützt bei Electron/Tauri-Architektur, IPC-Reviews, Auto-Update-Flows und Claude-Code-Schulungen. Für eine konkrete Beratung sind Repository-Struktur, Zielplattformen und die kritischsten Berechtigungen der beste Startpunkt.
Wenn du diesen Artikel praktisch testest, prüfe zuerst: npm run dev läuft, der renderer importiert weder Electron noch Node, beliebige Pfade lassen sich nicht lesen, Dateipfade funktionieren nach dem Packaging, und Auto Update gilt erst mit echter Signatur und echtem Feed als produktionsbereit.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.