Develop Electron Desktop App dengan Claude Code: IPC, preload, packaging, dan auto update
Panduan praktis Electron dengan Claude Code: contextIsolation, preload, IPC, akses file, packaging, dan update otomatis.
Electron memudahkan satu UI web menjadi desktop app untuk Windows, macOS, dan Linux. Namun app Electron punya akses yang lebih kuat daripada website biasa. Ia bisa membuka file lokal, memanggil dialog native, mengatur menu, dan mendistribusikan update. Karena itu, batas keamanan harus dirancang sejak awal.
Claude Code efektif jika dipakai sebagai worker implementasi dengan scope kecil: satu IPC channel, satu API preload, satu aturan akses file, atau satu konfigurasi packaging. Jangan langsung meminta “buat seluruh desktop app lengkap dengan installer dan auto update”, karena diff akan terlalu besar untuk direview.
Baseline Electron yang aman adalah menjaga contextIsolation, mematikan nodeIntegration di renderer, mengekspos API sekecil mungkin lewat preload, lalu memvalidasi ulang input di main process. Rujukan resmi yang perlu dibuka adalah Context Isolation, IPC, Distribution Overview, dan autoUpdater.
Kalau kamu sedang membandingkan dengan Tauri, baca panduan development Tauri. Untuk typing di batas preload dan renderer, lihat juga tips TypeScript.
Pisahkan tiga layer
| Layer | Tanggung jawab | Task yang cocok untuk Claude Code | Fokus review |
|---|---|---|---|
| main process | BrowserWindow, OS API, file, menu, update | Tambah satu IPC handler | Validasi, permission, error, aturan path |
| preload | Bridge minimal ke renderer | Expose fungsi dengan contextBridge | Tidak mengekspos ipcRenderer mentah |
| renderer | UI, form, state, tampilan | Bangun layar dan interaksi | Tidak akses Node/Electron langsung |
Renderer tidak perlu menyentuh file system. Ia memanggil fungsi preload yang spesifik, preload meneruskan IPC yang spesifik, lalu main process melakukan validasi sebelum menjalankan operasi OS.
flowchart LR
User["Aksi user"] --> Renderer["Renderer\nHTML / React"]
Renderer --> Preload["Preload\ncontextBridge"]
Preload --> IPC["IPC channel\npayload tervalidasi"]
IPC --> Main["Main process\nOS dan akses file"]
Main --> Result["Typed result"]
Result --> Renderer
Main --> Update["Packaging / auto update"]
Starter minimal yang bisa dicopy
Contoh ini sengaja memakai Electron + TypeScript tanpa React. Setelah boundary jelas, kamu bisa menambahkan Vite, React, atau framework lain.
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"]
}
Main process menjadi satu-satunya layer yang menyentuh OS dan file.
// 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 hanya mengekspos kemampuan yang jelas.
// 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 }>;
}
});
Renderer cukup memakai window.desktop.
// 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 untuk Claude Code
Tambahkan fitur membuka dan menyimpan Markdown pada app Electron + TypeScript yang sudah ada.
Batasan:
- jangan expose API Node.js ke renderer
- pertahankan contextIsolation: true, nodeIntegration: false, sandbox: true
- preload hanya expose window.desktop.openTextFile dan window.desktop.saveTextFile
- jangan expose ipcRenderer mentah atau invoke(channel, payload) generik
- hanya izinkan file md/txt/json
- jangan ubah kode auto update yang sudah ada
Selesai jika:
- npm run typecheck lolos
- jelaskan singkat perubahan main/preload/renderer
- sebutkan 3 poin review security
Tiga use case nyata
Use case pertama adalah app catatan Markdown internal. Dokumen tetap lokal tanpa upload ke server. Simpan daftar file terakhir, bukan isi dokumen sensitif, dan validasi ulang path saat app dibuka.
Use case kedua adalah log viewer untuk tim support. Log bisa sangat besar dan berisi email, token, atau ID pelanggan. Baca per halaman di main process, kirim hanya slice yang terlihat ke renderer, dan mask secrets sebelum copy atau export.
Use case ketiga adalah desktop AI prompt workbench. App ini menggabungkan template lokal, clipboard, dan API eksternal. API key harus tetap di main process; renderer hanya menerima status koneksi dan hasil.
Kesalahan yang sering terjadi
Kesalahan paling berbahaya adalah main process membaca path apa pun yang dikirim renderer. UI boleh terlihat aman, tetapi renderer tidak boleh dianggap trusted. Validasi extension, ukuran, dan allowed root di main process.
Kesalahan kedua adalah mengekspos window.api.invoke(channel, payload). Ini terlihat praktis, tetapi menjadi tunnel generik yang sulit diaudit. Lebih baik satu method untuk satu capability.
Kesalahan ketiga adalah path yang hanya bekerja saat development. Jangan bergantung pada folder src saat runtime dan jangan menulis ke dalam asar. Data yang bisa ditulis sebaiknya berada di app.getPath("userData").
Kesalahan keempat adalah menunda signing dan auto update sampai minggu rilis. Feed, sertifikat, permission GitHub Release, dan perilaku installer Windows perlu diuji lebih awal di CI. Untuk pipeline, baca panduan CI/CD.
Packaging dan auto update
Dengan Electron Forge, mulai dari sini:
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;
autoUpdater bawaan terutama ditujukan untuk macOS dan Windows. Di Linux, package manager distribusi biasanya lebih tepat. macOS membutuhkan signing untuk update otomatis, sementara Windows bergantung pada MSIX atau Squirrel.Windows.
// 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);
}
Minta Claude Code juga mendokumentasikan bagian yang belum siap produksi: signing identity, owner feed, permission release, secrets di CI, dan strategi rollback.
Penutup
Claude Code bisa mempercepat development Electron, tetapi kualitasnya ditentukan oleh boundary: renderer untuk UI, preload sebagai bridge sempit, main process untuk akses OS, dan packaging diuji sebelum rilis.
ClaudeCodeLab membantu desain arsitektur Electron/Tauri, review IPC, alur auto update, dan training tim agar memakai Claude Code dengan aman. Untuk konsultasi praktis, kirim struktur repo saat ini, platform target, dan permission yang paling berisiko.
Saat mencoba isi artikel ini, pastikan npm run dev lolos, renderer tidak import Electron atau Node, arbitrary path tidak bisa dibaca, path tetap benar setelah packaging, dan auto update baru dianggap production setelah signing dan feed benar-benar tersedia.
PDF gratis: cheatsheet Claude Code
Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.
Kami menjaga datamu dan tidak mengirim spam.
Tentang penulis
Masa
Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.
Artikel terkait
Workflow Obsidian ke CLAUDE.md untuk Claude Code
Ubah catatan kerja Obsidian menjadi operating note CLAUDE.md agar konteks tidak dijelaskan ulang.
Claude Code Revenue CTA Routing: dari artikel ke PDF, Gumroad, dan konsultasi
Workflow Claude Code untuk mengarahkan pembaca ke PDF gratis, Gumroad, atau konsultasi sesuai intent.
Aturan handoff tim Claude Code: bukti review, permission, rollback, dan jalur revenue
Format handoff Claude Code untuk tim: bukti, permission rule, rollback, PDF gratis, Gumroad, dan konsultasi.