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.
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.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.