Loom Desktop: Native Apps on the Claude Code Runtime
You're helping someone build a desktop application where Claude Code is the runtime — not a chat wrapper, but a native tool where Claude reads files, runs commands, and streams structured output to a local UI.
This is the desktop counterpart to web Loom. Where web Loom puts an Express
server between the browser and Claude, desktop Loom removes that layer entirely.
The Bun process spawns claude -p directly and pushes output to the webview via
typed RPC. No HTTP. No SSE formatting. No proxy timeouts. No auth.
Desktop means one user on their own machine. That simplifies everything: no sessions to manage, no credentials to pass per-request, no rate limiting across users. The architecture collapses to a thin bridge between subprocess and UI.
Why Desktop
The question to help users explore is: what would you build if Claude had full access to your local filesystem, ran as a native app on your dock, and could stream its reasoning to a custom interface — with zero server infrastructure?
A desktop Loom app has things web apps don't:
- Full local file access — Claude reads and writes files on the user's machine directly. No upload step.
- Native UX — System tray, native menus, file dialogs, drag-and-drop. Feels like a real tool, not a browser tab.
- Offline-first — The app launches instantly. Claude needs network for inference, but the interface itself has no server dependency.
- No infrastructure — No VM to provision, no server to keep running, no auth to maintain. Ship a binary.
- Privacy — Files never leave the machine. Claude processes them locally via the CLI.
Prerequisites
Before building, you need:
- Claude CLI installed and on PATH —
claude --versionshould return a version string - Bun v1.1+ — bun.sh
- ElectroBun —
bun add electrobun— github.com/blackboardsh/electrobun - Platform: macOS 14+, Windows 11+, or Ubuntu 22.04+ (other Linux with gtk3 & webkit2gtk-4.1)
ElectroBun is currently at v1.15.1 stable. It's actively maintained but still evolving — check the issue tracker for platform-specific concerns.
The Architecture: Thin Bridge
Every Loom desktop app has this shape:
┌─────────────────────────────────────────────────────┐
│ ElectroBun App │
│ │
│ ┌──────────────┐ Typed RPC ┌──────────────┐ │
│ │ Webview │◄───messages────►│ Bun Process │ │
│ │ (React/HTML) │ │ │ │
│ │ │ rpc.request │ Claude │ │
│ │ Renders UI │──startTask()──►│ Manager │ │
│ │ Shows tokens │ rpc.request │ │ │
│ │ Tool status │──abort()──────►│ Spawns │ │
│ │ │ │ claude -p │ │
│ │ │◄─rpc.sendProxy │ │ │
│ │ │ .token() │ Parses │ │
│ │ │ .toolUse() │ stdout │ │
│ │ │ .done() │ stream │ │
│ │ │ .error() │ │ │
│ │ │ .status() │ │ │
│ └──────────────┘ └──────┬───────┘ │
│ │ │
│ stdio │ │
│ ▼ │
│ ┌──────────┐ │
│ │ claude -p │ │
│ │ subprocess│ │
│ └──────────┘ │
└─────────────────────────────────────────────────────┘
Webview: The custom UI. React, vanilla HTML, whatever fits the app. Runs in a system webview (WKWebView on macOS, WebView2 on Windows, WebKit2GTK on Linux).
Bun Process: The bridge. Receives requests from the webview via typed RPC,
spawns claude -p subprocesses, parses their NDJSON stdout, and pushes results
back as typed RPC messages. This is the Claude Manager — typically 80-100 lines
of TypeScript.
Claude Subprocess: The intelligence. claude -p with the right flags. This
is where the agentic work happens — reading files, running commands, generating
structured output.
Why RPC Instead of HTTP
Web Loom uses Express + SSE because browsers need HTTP. Desktop Loom skips that:
| Concern | Web Loom (HTTP/SSE) | Desktop Loom (RPC) |
|---|---|---|
| Transport | HTTP + SSE streams | WebSocket (encrypted) |
| Type safety | Manual JSON parsing | Compile-time typed schema |
| Proxy timeouts | Heartbeat hacks needed | Not applicable |
| Auth | OAuth per-request | Not needed (single user) |
| Complexity | ~200 lines of plumbing | ~20 lines of plumbing |
The typed RPC contract is defined once and shared between Bun and webview. See
references/rpc-schema-reference.md for the full schema.
Project Setup
Scaffold an ElectroBun project:
bunx electrobun init hello-world
cd hello-world
bun install
bunx electrobun dev
This gives you a running app with a Bun process and webview. From here, you'll add the Claude Manager to the Bun side and the task UI to the webview.
For the full setup guide — project structure, configuration, dev workflow — see
references/electrobun-setup.md.
For Claude CLI flags and output formats, see references/cli-runtime-reference.md.
The Conversation
When someone comes to you with a desktop app idea, walk through these design questions. Don't dump them all at once — have a natural conversation. But cover this ground before you start building.
1. What does this app do?
Get concrete about the interface. What does someone see when they open it? What do they click? What appears when Claude is working? What does the output look like?
Desktop apps have a stronger "tool" identity than web apps. A web app might be a dashboard someone visits. A desktop app is something someone installs and reaches for. What's the tool? What problem does it solve?
Ask: "Walk me through opening this app. What do you see? What do you do first?"
2. What Claude capabilities does it need?
Map the app's features to what Claude does behind the scenes:
| Need | Claude Configuration |
|---|---|
| Read and analyze files | --tools "Read,Glob,Grep" |
| Modify code or files | --tools "Read,Edit,Write,Glob,Grep,Bash" |
| Pure reasoning, no file access | Omit --tools and use --permission-mode bypassPermissions |
| Web research | --tools "WebSearch,WebFetch" |
| Structured data extraction | --json-schema '{...}' |
| Custom persona | --system-prompt "You are..." |
The tools list determines what Claude can do. Tighter is safer — only grant what the app actually needs.
Note: --tools "" (empty string) is fragile and causes ambiguous CLI
behavior. For pure reasoning tasks, prefer omitting --tools entirely and
relying on bypassPermissions with no tool invocations, or pass file content
directly in the prompt so Claude doesn't need tools.
3. What's the interaction model?
How does the person's action translate to a Claude invocation?
| Interaction | Pattern |
|---|---|
| Click a button, get a result | Synchronous — spawn, collect, return |
| Watch progress in real-time | Streaming — tokens flow as RPC messages |
| Multi-step conversation with context | Conversational — --session-id first turn, --resume <id> after |
| Long task, minimize to tray | Background — task registry + tray status |
Most desktop apps start with Streaming. Add Conversational if the user needs follow-up questions. Add Background for tasks that take minutes.
4. What desktop features matter?
This is where desktop Loom diverges from web Loom. Ask which native features the app needs:
- File drag-and-drop — Drop files onto the window to feed them to Claude
- Native file dialogs — "Open File..." to select input, save results to disk
- System tray — Minimize during long tasks, show progress in tooltip
- Native menus — App menu bar with New Task, Abort, Model Selection, Settings
- Notifications — Native OS notification when a background task completes
Not every app needs all of these. A simple analysis tool might only need drag-and-drop. A code assistant might need the full set.
5. What are the cost and safety boundaries?
Desktop simplifies auth (no OAuth needed) but cost and permissions still matter:
- Budget:
--max-budget-usdcaps spending per task (print mode only). What's appropriate? $0.50 for quick analysis, $5 for deep codebase review. Pair with--max-turnsto bound cost and loop count. - Model:
haikufor fast/cheap,sonnetfor balanced,opusfor best quality;fableis the most capable alias — enable it for the hardest agentic work once it's available on your plan. Aliases resolve to the latest generation of each tier.--fallback-model sonnet,haiku(comma-separated, tried in order) for reliability. - Reasoning depth:
--effort low|medium|high|xhigh|maxis a separate axis from the model.xhighis Claude Code's own default for coding/agentic work; uselow/mediumfor quick extraction. - Permissions: Use
--permission-mode bypassPermissionswith explicit--toolslist.dontAskauto-denies anything not whitelisted, but also blocks reads outside the project directory — meaning dropped files from ~/Desktop won't be accessible.bypassPermissionsis safer for desktop apps where you control the prompt. - Turn limit:
--max-turnsprevents runaway agent loops. - Filesystem scope: Consider constraining Claude to specific directories via
--allowedTools "Read(/path/**)"patterns.
Building It
Default to TypeScript for both the Bun process and the webview. Use React for the UI unless the person prefers vanilla HTML.
Shared Utilities
Four helpers used by every pattern: resolveClaudePath() (find claude binary),
cleanEnv() (remove nesting guards), createStreamParser() (buffer stdout
into JSON lines), and deriveAndSendRPC() (map stream events to RPC messages).
See
references/desktop-patterns.md#shared-utilitiesfor the full implementations with explanatory prose.
Event Type Mapping
claude -p --output-format stream-json --verbose emits newline-delimited JSON.
Each event maps to an RPC message:
| Stream Event | RPC Message | Notes |
|---|---|---|
system (subtype: init) |
status (state: "running") |
Session started |
assistant (text content block) |
token |
Complete text for thinking models |
stream_event (content_block_delta) |
token |
Incremental for non-thinking models |
assistant (tool_use content block) |
toolUse |
Tool invocation |
user (tool_result block) |
toolResult |
Tool output |
result (subtype: success) |
done |
Session complete with cost |
| parse error / process crash | error |
Process failure |
Patterns at a Glance
Most desktop apps start with Streaming — it's the primary pattern. Tokens flow from Claude's stdout through Bun to the webview as typed RPC messages, with status heartbeats every 2 seconds.
| Pattern | When to Use | Reference |
|---|---|---|
| Synchronous | Quick extraction, classification | references/desktop-patterns.md#pattern-1-synchronous |
| Streaming | Primary pattern — tokens flow to UI | references/desktop-patterns.md#pattern-2-streaming |
| Conversational | Multi-turn with context retention | references/desktop-patterns.md#pattern-3-conversational |
| Background | Long tasks, tray minimization | references/desktop-patterns.md#pattern-4-background |
Read
references/desktop-patterns.mdwhen implementing a specific interaction pattern — it has the complete Bun-side and webview-side code.
Desktop Features
| Feature | Description | Reference |
|---|---|---|
| File Drag-and-Drop | Drop files for Claude to analyze (use FileReader, not File.path) | references/desktop-features.md#file-drag-and-drop |
| Native File Dialogs | Open/save files via Utils.openFileDialog() | references/desktop-features.md#native-file-dialogs |
| System Tray | Minimize during long tasks, show progress | references/desktop-features.md#system-tray |
| Native Menus | App menu bar with keyboard shortcuts | references/desktop-features.md#native-menus |
| File Access Config | Permission modes and tools lists for desktop | references/desktop-features.md#local-file-access-configuration |
Read
references/desktop-features.mdwhen adding native platform features to your app.
Distribution
Build with bunx electrobun build --env=stable. Always distribute the DMG,
never a ZIP (zipping strips executable permissions). Claude CLI is an external
dependency — don't bundle it. For clean distribution without Gatekeeper warnings,
set up code signing with an Apple Developer account.
Read
references/distribution.mdfor the complete build, signing, DMG icon, and auto-update setup.
Patterns at a Glance (extended)
Two additional patterns beyond the four core ones:
| Pattern | When to Use | Reference |
|---|---|---|
| Interrupt, done right | Stop button that also kills tool grandchildren | references/desktop-patterns.md#interrupt-done-right |
| Surviving a webview reload | Dev reload or crash-recovery re-attaches to running process | references/desktop-patterns.md#surviving-a-webview-reload |
The duplex bridge pattern (one long-lived claude -p --input-format stream-json process fed turns on stdin) is covered in
references/desktop-patterns.md#persistent-duplex-bridge-for-multi-turn as a lower-latency alternative to per-turn --resume.
Gotchas
Critical issues from real-world development. These 4 bite everyone:
- #1 Env cleaning — Remove
CLAUDECODEandCLAUDE_CODE_ENTRYPOINT, but neverCLAUDE_CODE_OAUTH_TOKEN. UsecleanEnv(). - #2 Stream buffering — TCP chunks split JSON lines. Use
createStreamParser, never rawsplit("\n"). - #13 macOS PATH — GUI apps don't inherit shell PATH. Use
resolveClaudePath()at startup. - #12 File.path — Doesn't exist in system webviews. Use
FileReader.readAsText()via RPC instead. - Bare kill orphans grandchildren —
proc.kill()only signals the top-level process. Spawn withdetached: trueand signal the process group to kill Bash subtrees too. Seereferences/desktop-patterns.md#interrupt-done-right.
The remaining 12 gotchas (numbers match references/gotchas.md):
- #3 TextDecoder with
{ stream: true }— prevents multi-byte UTF-8 corruption - #4
dontAskblocks reads outside project dir — usebypassPermissionsfor desktop - #5 Extended thinking models — handle both
assistanttext blocks andstream_eventdeltas - #6 RPC request timeout — return
taskIdimmediately, before Claude finishes - #7 ElectroBun version status — v1.15.1, check known platform issues
- #8 System webview differences — WKWebView/WebView2/WebKit2GTK behavior varies
- #9 Spawned Claude inherits user hooks — use
--setting-sources ""to skip settings - #10 Missing index.html — ElectroBun doesn't auto-generate it, create manually
- #11 stderr is a ReadableStream — read it once into a buffer, don't consume twice
- #14 ZIP strips executable permissions — always use DMG
- #15 No cross-compilation — build on the target platform
- #16 Process crash recovery — retry transient failures with backoff
Read
references/gotchas.mdfor full explanations, code samples, and fixes for all 16 issues.
What to Generate
When you build the app, produce:
-
electrobun.config.ts— App configuration (name, identifier, version, build options) -
src/bun/index.ts— Main process entry: startup CLI check, window creation, menu setup -
src/bun/claude-manager.ts—resolveClaudePath(),cleanEnv(),createStreamParser(),spawnClaude(),abort(), heartbeat -
src/bun/rpc.ts— RPC schema type definition (LoomRPC) andBrowserView.defineRPC<LoomRPC>()with handlers -
src/mainview/index.html— HTML shell (ElectroBun does NOT auto-generate this) -
src/mainview/index.ts— Webview entry point withElectroview.defineRPC<LoomRPC>()+new Electroview({ rpc })setup -
src/mainview/App.tsx— React UI (or vanilla HTML) withhandlers.messagescallbacks and task controls - Startup CLI check — verify
claude --versionsucceeds, show setup screen if missing - Error handling for process crashes —
proc.exitedcheck, stderr capture,errorRPC message
For simple apps, the Bun-side code fits in two files (index.ts + claude-manager.ts).
For complex apps, split RPC definitions, session management, and tray logic into
separate modules.
After generating, run bunx electrobun dev and verify the app launches. Then iterate
based on what the person sees.
After the app is working, ask the user if they'd like help building the
executable for distribution. If yes, run bunx electrobun build --env=stable
and explain:
- The build artifacts land in
artifacts/ - Recipients need Claude CLI installed and authenticated on their machine
- Without an Apple Developer certificate, macOS Gatekeeper will block the app —
the recipient can bypass with
xattr -cr /path/to/app.appor right-click > Open - For proper distribution without warnings, configure code signing and
notarization in
electrobun.config.ts
The Possibility Space
The most interesting desktop Loom apps are not chat interfaces in a window. They're tools that couldn't exist without an agentic runtime on the local machine — applications where Claude has direct access to the filesystem, runs as a native process, and streams its reasoning to a purpose-built interface.
A code review tool that you drag a project folder onto. A document processor that watches a directory and summarizes new files. A research assistant that reads your notes and finds connections. A local AI agent with a system tray icon that's always available.
Help people think about what they actually want to make. The question isn't "how do I put a chat box in a native window?" but "what would this tool look like if there were an intelligence behind it — one that could read any file on your machine and stream its thinking to a custom interface?"