Claude
Skills
Sign in
Back

electron-security

Included with Lifetime
$97 forever

Secure IPC patterns, credential storage, and API key management for Electron apps

Backend & APIs

What this skill does


# Electron Security Skill

Security is critical in Electron apps. This skill covers secure IPC, credential storage, and protecting sensitive data.

## Secure IPC with contextBridge

NEVER expose Node.js directly to the renderer. Use contextBridge:

### Preload Script

```typescript
// 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));
    }
  },
});
```

### TypeScript Declarations

```typescript
// 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 {};
```

### React Hook for Electron API

```typescript
// 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 };
}
```

## Secure Credential Storage

### Main Process IPC Handlers

```typescript
// 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'),
  }));
}
```

### Load Credentials at Startup

```typescript
// 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
});
```

## Settings UI Component

```typescript
// 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

Related in Backend & APIs