Use Cases (Aktualisiert: 1.6.2026)

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 Desktop Apps mit Claude Code entwickeln: IPC, preload, Packaging und Auto Update

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

SchichtVerantwortungGute Aufgabe für Claude CodeReview-Fokus
main processBrowserWindow, OS-APIs, Dateien, Menüs, UpdatesEinen IPC handler ergänzenValidierung, Rechte, Fehler, Pfadregeln
preloadMinimaler Übergang zum rendererFunktionen mit contextBridge freigebenKein rohes ipcRenderer
rendererUI, Formulare, Zustand, DarstellungScreen und Interaktion bauenKein 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.

#Claude Code #Electron #desktop apps #TypeScript #cross-platform
Kostenlos

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.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.