AGENTS.md
August 18, 2026 · View on GitHub
RunAnywhere AI, the Electron desktop app, built on the published
@runanywhere/electron SDK. CLAUDE.md is a symlink to this file.
This repo is standalone. It consumes the SDK from the npm registry and has no
checkout of, or path into, the RunanywhereAI/runanywhere-sdks monorepo. Where a
rule below cites a monorepo path, read it as "in the runanywhere-sdks repo",
not as a directory you will find here. Since the 0.20.17 restructure that repo's
layout is core/, bindings/, rcli/, idl/, engines/, runtimes/. The
consumer apps for iOS, Android, and Web live in their own repositories
(runanywhere-ios, runanywhere-android, runanywhere-web).
Everything runs on-device: chat, reasoning, retrieval over your own documents, voice, and vision. No prompt, document, or audio leaves the machine.
The two rules that govern this app
1. TypeScript only, strictly typed
Every authored file is TypeScript: main process, preload, renderer, shared
modules, build config, tests. There is no JavaScript in src/. The conventions
match the Electron SDK itself (bindings/electron/AGENTS.md in the monorepo) and
the Web SDK, so one habit set covers all three.
strict: truein all three tsconfigs (main, preload, renderer), never weakened per-file.- No
any.@typescript-eslint/no-explicit-anyis an error. Useunknownand narrow. - No
@ts-ignoreor@ts-expect-errorto silence a real error, and no non-null!to paper over a maybe. Narrow it, or handle the absence. - No raw JSON assumptions. Anything read from disk, IPC, or the network is
decoded into a declared type first. The IPC contract lives in exactly one
place,
src/shared/ipc-contract.ts, and both sides import it, so a channel cannot drift between main and renderer. consistent-type-imports,no-floating-promises,no-misused-promises, unused vars are errors (^_to opt out), andno-console(use the app logger, which routes to a main-side file log).- Proto types are the source of truth. Model categories, error codes, stream event shapes, audio formats, voice events all come from the SDK's re-exported generated types. Never hand-write an enum or string union the IDL defines.
- Discriminated unions for state,
readonlyfor anything the caller must not mutate,as constfor literal tables, exhaustiveswitchwith aneverfallthrough. - The renderer may not import
electron. Enforced byno-restricted-imports. Everything the renderer needs arrives through the typedwindow.runanywhereandwindow.appStorebridges.
Output format differs by target even though the source language does not. Main
and preload emit CommonJS (Electron loads them that way), the renderer emits an
ESM bundle, and the model catalog additionally emits a CommonJS file on disk
because the SDK's utility host require()s it by path.
npm run typecheck # all three projects
npm run lint # --max-warnings 0
npm run build # production bundle
npm test # vitest, test/unit/** only
npm run selftest and npm run test:e2e are the heavier gates. See the README.
2. Almost no logic lives here
Per the monorepo's root AGENTS.md, logic belongs at the lowest layer that can
serve all consumers:
C++ commons -> owns ALL AI logic (inference, lifecycle, registry, download, RAG, routing)
SDK -> thin bridge: platform I/O, process plumbing, typed API
this app -> UI rendering, navigation, copy, thin SDK calls. That is all.
Swift is the canonical reference. When behaviour is ambiguous, read the
runanywhere-ios app and copy its logic exactly, adapting syntax only. This app
should be visually and functionally indistinguishable from that app's macOS
target.
If you find yourself writing any of the following, stop. It is a bug one layer down:
- a multi-step bootstrap sequence before a feature works
- a hardcoded model id, framework, or filesystem path pattern
- post-processing of model output
- a workaround for an SDK or commons defect
- a re-implementation of something the SDK already does privately
- a hand-maintained copy of an SDK-internal mapping
Fix it in the SDK, or in commons if it is cross-platform, so every SDK benefits.
That means the fix lands in the runanywhere-sdks monorepo and reaches this app
as a published version bump, not as a patch here.
What legitimately belongs here: the model catalog table (every platform app owns its own, which is what lets two apps ship different model lists against one SDK build), copy strings and prompt suggestions, the local JSON store for conversations and settings, the demo tool implementations, cosine similarity in the embeddings demo, and pure presentation helpers like the segmentation mask painter.
Design parity is a gate
Tokens were transcribed from the Swift design system (the macOS branch of every
#if os(macOS) in runanywhere-ios) into src/renderer/design/tokens.css,
which is the one theme file here. No component invents a value.
Two things are easy to get wrong:
- Use the Swift cool blue-ink neutrals (
#FBFAF8,#0C0E17,#131620,#10182B), which match the shared design guideline (docs/DESIGN_GUIDELINE.md§2 in the monorepo). The Web app ships warm neutrals that appear in neither the guideline nor the Swift app. Do not copy them. - Use the macOS column wherever iOS and macOS differ. Composer radius is 16,
not the iOS 28;
hitTargetis 28, not 44.
Parity is checked two ways. Side-by-side screenshot comparison against the Swift
app is a human review step, done when chrome changes. There is deliberately no
committed pixel baseline: a toHaveScreenshot diff is a function of the machine
that produced it, this suite has no canonical runner, and the app ships on both
macOS and Windows. What test/e2e/screens.spec.ts and test/e2e/shell.spec.ts
assert instead is everything portable: tokens and motion resolved from computed
styles against the Motion table, sidebar scoping per destination, and
prefers-reduced-motion producing a 150 ms crossfade rather than a 0 ms blink,
with ambient loops stopped.
test/e2e/inference.spec.ts is the other half. It runs a real model per modality
through window.runanywhere, and with RA_PACKAGED_EXE it runs against a built
installer instead of the dev tree, which is the only way packaging faults surface
as packaging faults.
Architecture
Three processes. Inference never runs in main or renderer.
MAIN (CJS) forks
├─ window, menu, security, theme, store, .env, native resolution
├─ SDK bootstrap ──────────────────────► UTILITY HOST ── native addon ── C++ commons
└─ brokers a MessagePort main never reads
│
RENDERER (ESM bundle) ── preload (CJS) ──► window.runanywhere / window.appStore
Streaming: an AsyncIterable cannot cross contextBridge. Streams arrive on a
per-request channel and a renderer-side adapter re-exposes an AsyncIterable
whose return() sends a cancel, which keeps iterator.return?.() working as the
Stop button at every call site.
Packaged builds: the paths register() computes point inside app.asar, where
Electron's fs shim makes them look real to JavaScript while the OS loader sees
nothing. src/main/paths.ts rewrites the addon path and every plugin path to
app.asar.unpacked before use.
Non-negotiables
app.setName('RunAnywhere AI')before anyapp.getPath('userData').- The catalog is registered before the SDK preload is required. Registration is per-process.
contextIsolation: true,nodeIntegration: false,sandbox: false(the preload requires SDK modules).- All four security handlers: permission allowlist (
mediaandaudioCaptureonly),setWindowOpenHandler,will-navigate,will-attach-webview. - The menu is replaced, not hidden. Hiding it leaves DevTools accelerators live
in a shipped build. On macOS
{ role: 'appMenu' }must be first, or there is no Cmd-Q, About, or Preferences. - Test IPC channels are registered only under
RA_SELFTEST=1. One of them callsapp.exit. - Store writes stay atomic (temp file,
fsync, rename) and an unparseable file is copied aside rather than discarded. Conversations are capped at 200. - Settings saves merge, never replace. Per-modality model choices live in the same object.
webUtils.getPathForFileis the onlyFileto path route. Electron removedFile.path.- CSP forbids
evalandnew Function, which is why the demo calculator has a hand-written arithmetic parser. - Leaving Voice or Diarization closes the microphone.
- One generation, one RAG query, one VAD window at a time.
- Model residency is the SDK's decision. Never reintroduce an app-side unload policy.
- Errors surfaced to the user come from the SDK's typed
SDKExceptionand itsErrorCode. Do not collapse a native failure into a plain string.
Production release requirements
A production package needs a real org-scoped API key and backend base URL — the same pair
used by runanywhere-ios's RunAnywhereLocalSecrets.plist, runanywhere-android's
local.properties, and runanywhere-web's Vercel production env. Set them in the
gitignored .env (RUNANYWHERE_API_KEY / RUNANYWHERE_BASE_URL); ask a maintainer for
current production credentials. Never hardcode them in any committed file.
A production package must build against the published @runanywhere/electron* npm
packages only — this repo has a local-vs-npm dev switch for iterating against an unreleased
SDK build; before packaging a release, confirm it is set to the npm lane and clear any
local SDK overlay/cache so the build genuinely pulls from the registry, not a local build.
Headless e2e passing is not sufficient. Actually launch the packaged app (not just
npm run build output) on macOS as a real smoke test before calling it release-ready.
Windows must be validated on a real Windows machine/VM — there is no way to run or test
the Windows target on macOS; run the same dev-tree test pass, NPU benchmark, and a
packaged-.exe smoke test there for whatever SDK version is current. mac.identity and
the Windows certificateFile/certificateSubjectName
are currently unset (null) in electron-builder.yml — packages today are unsigned and
unnotarized; Gatekeeper and SmartScreen will warn until real Apple Developer ID +
notarization and a Windows Authenticode cert are supplied and wired in.
Windows
macOS parity is the design target; Windows is a shipping target and must not
regress. Windows engines ship and load at SDK 0.20.22. The .cmd launchers
(which clear ELECTRON_RUN_AS_NODE), %APPDATA% paths, DPAPI secure storage,
the GPU opt-in (--gpu / RA_GPU=1, never the silent default), and the .ico
icon all stay.
Windows has two mutually exclusive lanes: x64 carries llamacpp, onnx and sherpa; arm64 carries QHexRT alone, with no CPU engine behind it. Anything that assumes a CPU fallback is wrong on ARM64.
Platform-specific copy must be platform-conditional. Never show "Windows DPAPI"
or a .cmd filename on macOS.
This app ships for Windows only. Developing and testing on macOS (dev tree,
npm run build, headless e2e) is normal and expected — macOS parity is still the
design target for UI/UX — but there is no macOS production package for this app:
do not sign, notarize, or distribute a .dmg built from this repo. The Mac
desktop release is runanywhere-ios's native macOS target instead (see that
repo's AGENTS.md); package and ship it from there. A real Windows production
build/test pass needs an actual Windows machine — ask a maintainer for access to
the project's Windows test host rather than assuming a local VM.