Help us improve
Share bugs, ideas, or general feedback.
From aai-stack-electron
Implements secure IPC via contextBridge, keychain credential storage, and protected Electron APIs with React hooks and TypeScript types for Electron apps.
npx claudepluginhub bradtaylorsf/alphaagent-teamHow this skill is triggered — by the user, by Claude, or both
Slash command
/aai-stack-electron:electron-securityThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Security is critical in Electron apps. This skill covers secure IPC, credential storage, and protecting sensitive data.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Breaks plans, specs, or PRDs into thin vertical-slice issues on the project issue tracker using tracer bullets. Useful for converting high-level work into grabbable implementation tickets.
Share bugs, ideas, or general feedback.
Security is critical in Electron apps. This skill covers secure IPC, credential storage, and protecting sensitive data.
NEVER expose Node.js directly to the renderer. Use contextBridge:
// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
// Define allowed channels
const validSendChannels = [
'keychain:set',
'keychain:delete',
'cli:detect',
'app:quit',
];
const validReceiveChannels = [
'keychain:result',
'cli:status',
'update:available',
];
const validInvokeChannels = [
'keychain:get',
'keychain:set',
'keychain:delete',
'cli:detect-claude',
'app:get-version',
'app:get-paths',
];
contextBridge.exposeInMainWorld('electronAPI', {
// Invoke pattern (request-response)
invoke: (channel: string, ...args: unknown[]) => {
if (validInvokeChannels.includes(channel)) {
return ipcRenderer.invoke(channel, ...args);
}
throw new Error(`Invalid channel: ${channel}`);
},
// Send pattern (fire-and-forget)
send: (channel: string, ...args: unknown[]) => {
if (validSendChannels.includes(channel)) {
ipcRenderer.send(channel, ...args);
}
},
// Receive pattern (listen for events)
on: (channel: string, callback: (...args: unknown[]) => void) => {
if (validReceiveChannels.includes(channel)) {
const subscription = (_event: Electron.IpcRendererEvent, ...args: unknown[]) =>
callback(...args);
ipcRenderer.on(channel, subscription);
return () => ipcRenderer.removeListener(channel, subscription);
}
throw new Error(`Invalid channel: ${channel}`);
},
// One-time listener
once: (channel: string, callback: (...args: unknown[]) => void) => {
if (validReceiveChannels.includes(channel)) {
ipcRenderer.once(channel, (_event, ...args) => callback(...args));
}
},
});
// src/client/types/electron.d.ts
export interface ElectronAPI {
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
send: (channel: string, ...args: unknown[]) => void;
on: (channel: string, callback: (...args: unknown[]) => void) => () => void;
once: (channel: string, callback: (...args: unknown[]) => void) => void;
}
declare global {
interface Window {
electronAPI?: ElectronAPI;
}
}
export {};
// src/client/hooks/useElectron.ts
import { useCallback, useEffect } from 'react';
export function useElectron() {
const isElectron = typeof window !== 'undefined' && !!window.electronAPI;
const invoke = useCallback(
async <T>(channel: string, ...args: unknown[]): Promise<T | null> => {
if (!isElectron) return null;
try {
return (await window.electronAPI!.invoke(channel, ...args)) as T;
} catch (error) {
console.error(`IPC invoke error on ${channel}:`, error);
throw error;
}
},
[isElectron]
);
const send = useCallback(
(channel: string, ...args: unknown[]) => {
if (isElectron) {
window.electronAPI!.send(channel, ...args);
}
},
[isElectron]
);
const on = useCallback(
(channel: string, callback: (...args: unknown[]) => void) => {
if (!isElectron) return () => {};
return window.electronAPI!.on(channel, callback);
},
[isElectron]
);
return { isElectron, invoke, send, on };
}
// Keychain-specific hooks
export const KEY_NAMES = {
ANTHROPIC_API_KEY: 'anthropic-api-key',
GEMINI_API_KEY: 'gemini-api-key',
OPENAI_API_KEY: 'openai-api-key',
} as const;
export function useKeychain() {
const { isElectron, invoke } = useElectron();
const getKey = async (keyName: string): Promise<string | null> => {
if (!isElectron) return null;
return invoke<string | null>('keychain:get', keyName);
};
const setKey = async (keyName: string, value: string): Promise<boolean> => {
if (!isElectron) return false;
return (await invoke<boolean>('keychain:set', keyName, value)) ?? false;
};
const deleteKey = async (keyName: string): Promise<boolean> => {
if (!isElectron) return false;
return (await invoke<boolean>('keychain:delete', keyName)) ?? false;
};
return { isElectron, getKey, setKey, deleteKey };
}
// electron/ipc-handlers.ts
import { ipcMain, app } from 'electron';
import { getCredential, setCredential, deleteCredential, KEY_NAMES } from './keychain';
// Map keychain keys to environment variables
const KEY_TO_ENV: Record<string, string> = {
[KEY_NAMES.ANTHROPIC_API_KEY]: 'ANTHROPIC_API_KEY',
[KEY_NAMES.GEMINI_API_KEY]: 'GEMINI_API_KEY',
[KEY_NAMES.OPENAI_API_KEY]: 'OPENAI_API_KEY',
};
export function registerIpcHandlers(): void {
// Get credential from keychain
ipcMain.handle('keychain:get', async (_event, keyName: string) => {
return getCredential(keyName);
});
// Set credential in keychain AND environment
ipcMain.handle('keychain:set', async (_event, keyName: string, value: string) => {
const result = await setCredential(keyName, value);
// Also set as environment variable so server can use it immediately
if (result && KEY_TO_ENV[keyName]) {
process.env[KEY_TO_ENV[keyName]] = value;
}
return result;
});
// Delete credential from keychain AND environment
ipcMain.handle('keychain:delete', async (_event, keyName: string) => {
const result = await deleteCredential(keyName);
if (result && KEY_TO_ENV[keyName]) {
delete process.env[KEY_TO_ENV[keyName]];
}
return result;
});
// App info
ipcMain.handle('app:get-version', () => app.getVersion());
ipcMain.handle('app:get-paths', () => ({
userData: app.getPath('userData'),
temp: app.getPath('temp'),
}));
}
// electron/main.ts
import { app } from 'electron';
import { getCredential, KEY_NAMES } from './keychain';
async function loadCredentials(): Promise<void> {
const keyMappings = [
{ key: KEY_NAMES.ANTHROPIC_API_KEY, env: 'ANTHROPIC_API_KEY' },
{ key: KEY_NAMES.GEMINI_API_KEY, env: 'GEMINI_API_KEY' },
{ key: KEY_NAMES.OPENAI_API_KEY, env: 'OPENAI_API_KEY' },
];
for (const { key, env } of keyMappings) {
try {
const value = await getCredential(key);
if (value) {
process.env[env] = value;
console.log(`Loaded ${key} from keychain`);
}
} catch (error) {
console.error(`Failed to load ${key}:`, error);
}
}
}
app.whenReady().then(async () => {
await loadCredentials();
// ... rest of startup
});
// src/client/pages/Settings.tsx
import React, { useState, useEffect } from 'react';
import { useKeychain, KEY_NAMES } from '../hooks/useElectron';
interface KeyConfig {
name: string;
keyName: string;
placeholder: string;
}
const API_KEYS: KeyConfig[] = [
{
name: 'Anthropic API Key',
keyName: KEY_NAMES.ANTHROPIC_API_KEY,
placeholder: 'sk-ant-...',
},
{
name: 'Google Gemini API Key',
keyName: KEY_NAMES.GEMINI_API_KEY,
placeholder: 'AIza...',
},
];
export function Settings() {
const { isElectron, getKey, setKey, deleteKey } = useKeychain();
const [values, setValues] = useState<Record<string, string>>({});
const [saved, setSaved] = useState<Record<string, boolean>>({});
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadKeys() {
if (!isElectron) return;
const loaded: Record<string, string> = {};
const savedState: Record<string, boolean> = {};
for (const key of API_KEYS) {
const value = await getKey(key.keyName);
if (value) {
loaded[key.keyName] = '••••••••••••' + value.slice(-4);
savedState[key.keyName] = true;
}
}
setValues(loaded);
setSaved(savedState);
setLoading(false);
}
loadKeys();
}, [isElectron, getKey]);
const handleSave = async (keyName: string, value: string) => {
if (!value.startsWith('••••')) {
const success = await setKey(keyName, value);
if (success) {
setValues((prev) => ({
...prev,
[keyName]: '••••••••••••' + value.slice(-4),
}));
setSaved((prev) => ({ ...prev, [keyName]: true }));
}
}
};
const handleDelete = async (keyName: string) => {
const success = await deleteKey(keyName);
if (success) {
setValues((prev) => ({ ...prev, [keyName]: '' }));
setSaved((prev) => ({ ...prev, [keyName]: false }));
}
};
if (!isElectron) {
return <div>Settings are only available in the desktop app.</div>;
}
if (loading) {
return <div>Loading...</div>;
}
return (
<div className="settings-page">
<h1>Settings</h1>
<section>
<h2>API Keys</h2>
<p>Your API keys are stored securely in your system keychain.</p>
{API_KEYS.map((key) => (
<div key={key.keyName} className="key-input-group">
<label>{key.name}</label>
<input
type="password"
placeholder={key.placeholder}
value={values[key.keyName] || ''}
onChange={(e) =>
setValues((prev) => ({ ...prev, [key.keyName]: e.target.value }))
}
/>
<button onClick={() => handleSave(key.keyName, values[key.keyName] || '')}>
Save
</button>
{saved[key.keyName] && (
<button onClick={() => handleDelete(key.keyName)}>Delete</button>
)}
{saved[key.keyName] && <span className="saved-badge">Saved</span>}
</div>
))}
</section>
</div>
);
}
Prevent navigation to untrusted URLs:
// electron/main.ts
app.on('web-contents-created', (_, contents) => {
// Block navigation to external URLs
contents.on('will-navigate', (event, navigationUrl) => {
const parsedUrl = new URL(navigationUrl);
const allowedOrigins = ['localhost', '127.0.0.1'];
if (!allowedOrigins.includes(parsedUrl.hostname)) {
event.preventDefault();
console.warn('Blocked navigation to:', navigationUrl);
}
});
// Block new window creation
contents.setWindowOpenHandler(({ url }) => {
// Open external links in default browser
if (url.startsWith('https://')) {
require('electron').shell.openExternal(url);
}
return { action: 'deny' };
});
});
// electron/window.ts
import { BrowserWindow } from 'electron';
export function createMainWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
// CRITICAL: Enable context isolation
contextIsolation: true,
// CRITICAL: Disable Node.js integration in renderer
nodeIntegration: false,
// Use preload script for IPC
preload: preloadPath,
// Disable remote module
enableRemoteModule: false,
// Sandbox renderer process
sandbox: true,
// Disable webview tag
webviewTag: false,
},
});
return win;
}
// NEVER do these:
// 1. Expose ipcRenderer directly
contextBridge.exposeInMainWorld('ipc', ipcRenderer); // DANGEROUS!
// 2. Allow any channel
ipcRenderer.invoke(anyUserProvidedChannel); // DANGEROUS!
// 3. Eval user input
eval(userInput); // DANGEROUS!
// 4. Disable security features
webPreferences: {
nodeIntegration: true, // DANGEROUS!
contextIsolation: false, // DANGEROUS!
}
// 5. Store credentials in localStorage
localStorage.setItem('apiKey', key); // Use keychain instead!
Used by:
electron-converter agent