Desenvolvendo apps desktop Electron com Claude Code: IPC, preload, empacotamento e auto update
Guia prático de Electron com Claude Code: contextIsolation, preload, IPC, arquivos, packaging e auto update.
Electron transforma uma interface web em aplicativo desktop para Windows, macOS e Linux. A vantagem vem com responsabilidade: a aplicação pode abrir arquivos locais, chamar diálogos nativos, controlar menus e distribuir atualizações. Por isso, a fronteira de segurança precisa ser tratada como parte central da arquitetura.
Claude Code ajuda bastante quando você o usa como worker de implementação em tarefas pequenas: um canal IPC, uma API de preload, uma regra de acesso a arquivo, uma configuração de empacotamento. Pedir uma aplicação completa com instalador e auto update de uma vez costuma gerar um diff grande demais para revisar.
A linha de base atual é manter contextIsolation, deixar nodeIntegration desligado no renderer, expor apenas APIs pequenas via preload e validar novamente no main process. Use como referência os documentos oficiais de Context Isolation, IPC, distribuição e autoUpdater.
Se você está comparando com Tauri, leia o guia de desenvolvimento Tauri. Para tipar a fronteira entre preload e renderer, veja também as dicas de TypeScript.
Separe as três camadas
| Camada | Responsabilidade | Boa tarefa para Claude Code | Revisão humana |
|---|---|---|---|
| main process | BrowserWindow, APIs do sistema, arquivos, menus, updates | Adicionar um handler IPC por vez | Validação, permissões, erros, regras de caminho |
| preload | Ponte mínima para o renderer | Expor funções com contextBridge | Não expor ipcRenderer cru |
| renderer | UI, formulários, estado, visualização | Tela e interação | Não acessar Node ou Electron diretamente |
O renderer não deve tocar o sistema de arquivos. Ele chama uma função específica do preload, o preload envia uma mensagem IPC específica e o main process valida antes de executar.
flowchart LR
User["Ação do usuário"] --> Renderer["Renderer\nHTML / React"]
Renderer --> Preload["Preload\ncontextBridge"]
Preload --> IPC["Canal IPC\npayload validado"]
IPC --> Main["Main process\nSO e arquivos"]
Main --> Result["Resultado tipado"]
Result --> Renderer
Main --> Update["Packaging / auto update"]
Starter mínimo copiável
Este exemplo usa apenas Electron + TypeScript. Depois de entender a fronteira, você pode adicionar Vite, React ou outro framework.
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"]
}
O main process mantém o acesso ao sistema operacional e valida arquivos.
// 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();
}
});
O preload expõe capacidades específicas, não um túnel genérico de IPC.
// 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 }>;
}
});
O renderer usa somente 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 para Claude Code
Adicione suporte para abrir e salvar Markdown no app Electron + TypeScript existente.
Restrições:
- não expor APIs Node.js ao renderer
- manter contextIsolation: true, nodeIntegration: false e sandbox: true
- preload expõe apenas window.desktop.openTextFile e window.desktop.saveTextFile
- não expor ipcRenderer cru nem invoke(channel, payload) genérico
- permitir somente md/txt/json
- não alterar o código de auto update existente
Pronto quando:
- npm run typecheck passa
- explicar brevemente as mudanças em main/preload/renderer
- listar 3 pontos de revisão de segurança
Três casos de uso
O primeiro caso é um app interno de notas Markdown. Documentos ficam locais, sem upload para servidor. Salve apenas a lista de recentes, não o conteúdo confidencial, e valide os caminhos ao iniciar.
O segundo é um visualizador de logs para suporte. Logs podem ser grandes e conter e-mails, tokens ou IDs de clientes. Leia por páginas no main process, envie ao renderer apenas o trecho visível e mascare segredos ao copiar ou exportar.
O terceiro é uma bancada desktop de prompts de IA. Ela combina templates locais, clipboard e APIs externas. Chaves de API devem ficar no main process; o renderer recebe somente status e resultados.
Falhas comuns
A falha mais perigosa é o main process ler qualquer caminho enviado pelo renderer. Mesmo com UI simples, trate o renderer como não confiável. Extensão, tamanho e raiz permitida precisam ser verificados no main process.
Outra falha é expor window.api.invoke(channel, payload). Parece prático, mas vira um túnel genérico. Prefira uma função para cada capacidade.
Também é comum funcionar no desenvolvimento e quebrar no pacote. Ler src em runtime ou escrever dentro de asar não é confiável. Dados graváveis devem ficar em app.getPath("userData").
Por fim, não deixe assinatura e updates para a semana do lançamento. Feed, certificados, permissões do GitHub Release e comportamento no Windows devem entrar cedo no CI. Veja o guia de CI/CD.
Packaging e auto update
Com Electron Forge, comece assim:
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;
O autoUpdater integrado é voltado principalmente para macOS e Windows. No Linux, normalmente o gerenciador de pacotes da distribuição é mais adequado. No macOS, assinatura é requisito para update automático; no Windows, o comportamento depende de MSIX ou 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);
}
Peça para Claude Code documentar o que ainda falta para produção: identidade de assinatura, dono do feed, permissões de release, secrets no CI e rollback.
Fechamento
Claude Code acelera Electron, mas a qualidade vem das fronteiras: renderer para UI, preload como ponte estreita, main process para acesso ao sistema e packaging testado antes do lançamento.
ClaudeCodeLab ajuda com arquitetura Electron/Tauri, revisão de IPC, fluxo de auto update e treinamento de equipes para usar Claude Code com segurança. Para uma consultoria objetiva, compartilhe a estrutura atual, plataformas alvo e permissões críticas.
Ao testar este artigo, confirme que npm run dev compila, que o renderer não importa Electron nem Node, que caminhos arbitrários não podem ser lidos, que os caminhos funcionam após empacotar e que auto update só vira produção depois de assinatura e feed reais.
Artigos relacionados
Desenvolvimento Tauri com Claude Code: Vite, comandos Rust e permissoes
Guia pratico de Tauri v2 com Claude Code: Vite, Rust commands, invoke, permissoes de arquivos, testes e review.
Codex Desktop em agências: revisão de diff, PR e publicação
Checklist para agências revisarem diff, PR, staging URL e publicação com Codex e aprovação humana.
GCP Cloud Functions com Claude Code: HTTP, Secret Manager e Cloud Logging
Crie Cloud Run functions com Claude Code: HTTP, segredos, logs, verificação de deploy e armadilhas.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.