This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Openwork is a standalone desktop automation assistant built with Electron. The app hosts a local React UI (bundled via Vite), communicating with the main process through contextBridge IPC. The main process spawns the OpenCode CLI (via node-pty) to execute user tasks. Users provide their own API key (Anthropic, OpenAI, Google, or xAI) on first launch, stored securely in the OS keychain.
pnpm dev # Run desktop app in dev mode (Vite + Electron)
pnpm dev:clean # Dev mode with CLEAN_START=1 (clears stored data)
pnpm build # Build all workspaces
pnpm build:desktop # Build desktop app only
pnpm lint # TypeScript checks
pnpm typecheck # Type validation
pnpm clean # Clean build outputs and node_modules
pnpm -F @accomplish/desktop test:e2e # Playwright E2E tests
pnpm -F @accomplish/desktop test:e2e:ui # E2E with Playwright UI
pnpm -F @accomplish/desktop test:e2e:debug # E2E in debug modeapps/desktop/ # Electron app (main/preload/renderer)
packages/shared/ # Shared TypeScript types
Main Process (main/):
index.ts- Electron bootstrap, single-instance enforcement,accomplish://protocol handleripc/handlers.ts- IPC handlers for task lifecycle, settings, onboarding, API keysopencode/adapter.ts- OpenCode CLI wrapper usingnode-pty, streams output and handles permissionsstore/secureStorage.ts- API key storage viakeytar(OS keychain)store/appSettings.ts- App settings viaelectron-store(debug mode, onboarding state)store/taskHistory.ts- Task history persistence
Preload (preload/index.ts):
- Exposes
window.accomplishAPI viacontextBridge - Provides typed IPC methods for task operations, settings, events
Renderer (renderer/):
main.tsx- React entry with HashRouterApp.tsx- Main routing + onboarding gatepages/- Home, Execution, History, Settings pagesstores/taskStore.ts- Zustand store for task/UI statelib/accomplish.ts- Typed wrapper for the IPC API
Renderer (React)
↓ window.accomplish.* calls
Preload (contextBridge)
↓ ipcRenderer.invoke
Main Process
↓ Native APIs (keytar, node-pty, electron-store)
↑ IPC events
Preload
↑ ipcRenderer.on callbacks
Renderer
node-pty- PTY for OpenCode CLI spawningkeytar- Secure API key storage (OS keychain)electron-store- Local settings/preferencesopencode-ai- Bundled OpenCode CLI (multi-provider: Anthropic, OpenAI, Google, xAI)
- TypeScript everywhere (no JS for app logic)
- Use
pnpm -F @accomplish/desktop ...for desktop-specific commands - Shared types go in
packages/shared/src/types/ - Renderer state via Zustand store actions
- IPC handlers in
src/main/ipc/handlers.tsmust matchwindow.accomplishAPI in preload
IMPORTANT: Always use ES module imports for images in the renderer, never absolute paths.
// CORRECT - Use ES imports
import logoImage from '/assets/logo.png';
<img src={logoImage} alt="Logo" />
// WRONG - Absolute paths break in packaged app
<img src="/assets/logo.png" alt="Logo" />Why: In development, Vite serves /assets/... from the public folder. But in the packaged Electron app, the renderer loads via file:// protocol, and absolute paths like /assets/logo.png resolve to the filesystem root instead of the app bundle. ES imports are processed by Vite to use import.meta.url, which works correctly in both environments.
Static assets go in apps/desktop/public/assets/.
CLEAN_START=1- Clear all stored data on app startE2E_SKIP_AUTH=1- Skip onboarding flow (for testing)
- E2E tests:
pnpm -F @accomplish/desktop test:e2e - Tests use Playwright with serial execution (Electron requirement)
- Test config:
apps/desktop/playwright.config.ts
The packaged app bundles standalone Node.js v20.18.1 binaries to ensure MCP servers work on machines without Node.js installed.
src/main/utils/bundled-node.ts- Utility to get bundled node/npm/npx pathsscripts/download-nodejs.cjs- Downloads Node.js binaries for all platformsscripts/after-pack.cjs- Copies correct binary into app bundle during build
IMPORTANT: When spawning npx or node in the main process, you MUST add the bundled Node.js bin directory to PATH. This is because npx uses a #!/usr/bin/env node shebang which looks for node in PATH.
import { spawn } from 'child_process';
import { getNpxPath, getBundledNodePaths } from '../utils/bundled-node';
// Get bundled paths
const npxPath = getNpxPath();
const bundledPaths = getBundledNodePaths();
// Build environment with bundled node in PATH
let spawnEnv: NodeJS.ProcessEnv = { ...process.env };
if (bundledPaths) {
const delimiter = process.platform === 'win32' ? ';' : ':';
spawnEnv.PATH = `${bundledPaths.binDir}${delimiter}${process.env.PATH || ''}`;
}
// Spawn with the modified environment
spawn(npxPath, ['-y', 'some-package@latest'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: spawnEnv,
});Why: Without adding bundledPaths.binDir to PATH, the spawned process will fail with exit code 127 ("node not found") on machines that don't have Node.js installed system-wide.
When generating MCP server configurations, pass NODE_BIN_PATH in the environment so spawned servers can add it to their PATH:
environment: {
NODE_BIN_PATH: bundledPaths?.binDir || '',
}- Single-instance enforcement - second instance focuses existing window
- API keys stored in OS keychain (macOS Keychain, Windows Credential Vault, Linux Secret Service)
- API key validation via test request to respective provider API
- OpenCode CLI permissions are bridged to UI via IPC
permission:request/permission:respond - Task output streams through
task:updateandtask:progressIPC events