23 KiB
CLAUDE.md - AI Assistant Guide for draw.io Desktop
Project Overview
Draw.io Desktop is an Electron-based desktop application that wraps the core draw.io diagramming editor (included as a git submodule). It enables creating flowcharts, UML diagrams, and more, with a security-first design that isolates diagram data from the internet.
Repository: https://github.com/jgraph/drawio-desktop
License: GPL v3 only since v31.5.2, Apache 2.0 before it (GPL-3.0-only in package.json, which electron-builder writes into the deb/rpm License: field; the drawio core submodule stays Apache 2.0)
Version: see drawio/VERSION (stamped into package.json by npm run sync)
Quick Reference
# Clone (MUST be recursive for submodule)
git clone --recursive https://github.com/jgraph/drawio-desktop.git
# Install dependencies
npm install
# Run application
npm start
# Run with DevTools enabled
DRAWIO_ENV=dev npm start
# Sync version before building (required)
npm run sync
# Build for specific platforms
npm run release-win # Windows x64
npm run release-linux # Linux (AppImage, deb, rpm)
npm run release-appx # Windows Store
Project Structure
drawio-desktop/
├── src/main/
│ ├── electron.js # Main Electron process (3,700+ lines)
│ ├── electron-preload.js # IPC bridge with contextBridge
│ ├── args.js # CLI argument definitions and parser
│ ├── export-files.js # CLI export folder scan and output open (no symlink following)
│ ├── backup-file.js # GUI backups written by exclusive temp file and rename
│ ├── progress-bar.js # Progress bar for long-running operations
│ ├── system-path.js # Absolute paths for Windows system programs (no bare names)
│ ├── file-watcher.js # Per-window fs.watchFile registry (change events reach the window that asked)
│ ├── config-paths.js # Per-window grants for config-declared local paths (read once at load, user-allowed)
│ ├── update-channel.js # Windows arm64 update channel (latest-arm64.yml)
│ ├── update-mode.js # Per-install update mode: auto, notify (release page link) or off
│ ├── window-bounds.js # Restores saved window bounds against the current displays
│ ├── print-options.js # webContents.print() options: printer paper in portrait order + landscape flag
│ ├── url-params.js # urlParams.json locations (install dir, userData; cwd only in dev) and merge
│ └── disableUpdate.js # Generated by sync script
├── src/test/
│ ├── cli-args.test.js # CLI argument parsing tests (npm test)
│ ├── export-files.test.js # CLI export symlink handling tests (npm test)
│ ├── backup-file.test.js # GUI path authorisation and backup symlink regression tests
│ ├── system-path.test.js # System32 path helper tests (npm test)
│ ├── config-paths.test.js # Config-declared path collection and per-window grant tests (npm test)
│ ├── file-watcher.test.js # Per-window file watch registry tests (npm test)
│ ├── update-channel.test.js # Windows arm64 update channel tests (npm test)
│ ├── update-mode.test.js # Per-install update mode tests (npm test)
│ ├── window-bounds.test.js # Window bounds restore tests (npm test)
│ ├── print-options.test.js # Print paper size, orientation and scale tests (npm test)
│ ├── url-params.test.js # urlParams.json location and merge tests (npm test)
│ └── msi-project-created.test.js # MSI shortcut icon hook tests (npm test)
├── drawio/ # Git submodule - core draw.io editor
│ └── src/main/webapp/ # Web application loaded in Electron
├── build/ # Build resources
│ ├── notarize.mjs # macOS Quick Look setup, signing + notarization
│ ├── sign-trusted.mjs # Windows signing hook (Azure Trusted Signing)
│ ├── fuses.mjs # Electron security fuses
│ ├── msi-project-created.mjs # msiProjectCreated hook: MSI shortcuts use exe icon, not the C:\Windows\Installer icon cache
│ ├── dmg-hidden-files.mjs # beforePack hook: parks hidden DMG support files outside the installer window
│ ├── quicklook-preview.html # Quick Look preview page (viewer-static.min.js)
│ ├── quicklook-entitlements.plist # Sandbox entitlements for .appex
│ └── entitlements.mac.plist
├── doc/
│ ├── RELEASE_PROCESS.md # Release workflow documentation
│ └── BUILDING_FOR_PERSONAL_USE.md # Guide for unsigned fork/personal builds
├── electron-builder-*.json # Platform-specific build configs
├── sync.cjs # Version sync script
└── package.json
Tech Stack
- Runtime: Node.js 22.12+ (
enginesinpackage.json; CI builds on Node 24) - Framework: Electron (version pinned in
package.json) - Language: JavaScript (ES6 modules)
- Build Tool: electron-builder
- Package Manager: npm
Key Files
| File | Purpose |
|---|---|
src/main/electron.js |
Main process: window management, IPC handlers, menus, auto-update |
src/main/electron-preload.js |
Secure IPC bridge between renderer and main process |
src/main/args.js |
CLI option definitions and argument parser (used by CLI export) |
sync.cjs |
Pre-build script that syncs version from drawio/VERSION |
electron-builder-*.json |
Platform-specific build configurations |
build/sign-trusted.mjs |
electron-builder Windows signing hook (Azure Trusted Signing) |
Code Style
- ES6 modules with
import/export - Tab indentation
- Allman brace style (opening brace on new line)
- camelCase for variables, PascalCase for classes
- No ESLint/Prettier - manual style consistency
- Sparse comments; code clarity preferred
Git Conventions
Branches
dev- Main development branch (PR target)releases/v*.*.*- One per release, pushed by theprepare-releaseworkflow with a PR intodev; thevX.Y.Ztag goes on its head (doc/RELEASE_PROCESS.md). There is no long-livedreleasebranch in this repo (retired after v30.4.1); theref: releasein the build workflows is drawio-dev's
Commit Messages
- Lowercase sentence style without period
- Issue references:
[jgraph/drawio-desktop#XXXX] - Examples:
Fixes paste errorAdds buffer as dependency [jgraph/drawio-desktop#2301]Prepare release v29.3.0
Version Tags
Format: v{MAJOR}.{MINOR}.{PATCH} (e.g., v29.3.0)
Tags trigger CI/CD build workflows.
Build Process
- Sync version:
npm run syncreadsdrawio/VERSIONand updatespackage.json - Install:
npm cifor clean install - Build:
electron-builderwith platform-specific config - Post-build: Security fuses applied, Quick Look extension assembled (macOS), notarization (macOS)
Packaged File Set
- The
filesarrays in the fiveelectron-builder-*.jsonconfigs leave out what the packaged app never loads (about 80 MB ofapp.asar):stencils/**/*.xml(embedded injs/stencils.min.js, which overridesmxStencilRegistry.loadStencil),shapes/**(compiled intojs/shapes-14-6-5.min.js;mxStencilRegistry.allowEvalis false on desktop), the unminified editor sourcesjs/diagramly/**,js/grapheditor/**,mxgraph/src/**andmxgraph/mxClient.js(only loaded withdev=1), thecjs/dist/src/ts3.4trees of@cantoo/pdf-lib(the ESM main process resolves theesbuild) and all*.mapfiles.js/diagramly/ElectronApp.jsandDesktopLibrary.jsstay becausebootstrap.jsloads them uncompiled. Keep the five configs in sync DRAWIO_ENV=devon a packaged build therefore runs the minified bundles:electron.jsonly passesdev=1whenjs/diagramly/Devel.jsexists. DevTools and main-process logging still work
Code Signing
- Windows: Azure Trusted Signing via the
signtoolOptions.signhookbuild/sign-trusted.mjs(configured inelectron-builder-win*.json, not CSC_LINK certificates). CI (electron-builder-win.yml) downloads the signing dlib, locatessigntool.exe, and authenticates withAZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRETsecrets.win.signExts: [".dll"]signs the bundled Electron DLLs too — unsigned ffmpeg.dll etc. trip Defender's ASR ransomware rule after each release (#2509). The portable-zip step must pass--config electron-builder-win.json; a bare--dirrun loads no config and ships unsigned, unfused binaries - macOS: Apple Developer certificate + notarization in
build/notarize.mjs - Unsigned builds:
DRAWIO_UNSIGNED=trueskips signing (Windows) and notarization (macOS) for personal/fork builds
Microsoft Store
- The
.appxis never a GitHub release asset —release-appxruns--publish neverand themicrosoft-storejob inelectron-builder-win.ymlsubmits it to Partner Center with the msstore CLI (msstore publish <pkg> -id 9MVVSZK43QQW), which commits the submission into certification - The appx is copied to a
.msixname first: msstore'sMSIXProjectPublisheronly matches.msix/.msixbundle/.msixupload, and pointing it at the project root instead would send it down the Electron configurator path, which runsnpm install. Same container, but Partner Center holds a.msixto the MSIX floor and fails the commit ifMinVersionis 10.0.17134.0 or lower, soelectron-builder-appx.jsonsetsappx.minVersionto 10.0.17763.0 (Windows 10 1809, the first build the Store installs MSIX on; electron-builder's x64 default 10.0.14316.0 only passed as a hand-uploaded.appx).maxVersionTestedis kept equal to it on purpose: it sets which OS behaviours the packaged app runs with, and a raise cannot be tested before the job commits into certification - Needs
MS_STORE_TENANT_ID/MS_STORE_SELLER_ID/MS_STORE_CLIENT_ID/MS_STORE_CLIENT_SECRETsecrets from an Entra app registration holding the Manager role in Partner Center; the client secret expires and must be rotated
Personal / Fork Builds
doc/BUILDING_FOR_PERSONAL_USE.mddocuments building unsigned from a fork (the project is closed to contributions but GPL v3 licensed)- Set
DRAWIO_UNSIGNED=trueand runelectron-builderdirectly with--publish never; usenpm run sync -- disableUpdateso auto-update doesn't replace the custom build .github/workflows/personal-build.ymlis a manual (workflow_dispatch) workflow that builds unsigned installers on a fork with no secrets and attaches them as run artifacts
CI override: The release build workflows check out the private
jgraph/drawio-devrepo at itsreleasebranch, copy the built*.min.jsandVERSIONinto the publicdrawio/submodule tree, then runnpm run syncas normal. This lets CI ship from an internal release that is ahead of the publicdrawiotag without any change tosync.cjs. Out-of-tree builders (who have no access todrawio-dev) fall through to the public submodule'sVERSIONas before.
Platform Build Commands
| Command | Target |
|---|---|
npm run release-win |
Windows x64 (NSIS + MSI) |
npm run release-win-arm64 |
Windows ARM64 |
npm run release-linux |
Linux (AppImage, deb, rpm) |
npm run release-appx |
Windows Store (--publish never; the Store job submits it) |
npm run release-snap |
Snap package |
Architecture Notes
Security Model
- Content Security Policy prevents remote script execution
- contextBridge exposes only specific APIs to renderer
- validateSender() ensures IPC calls originate from local draw.io
- Path authorisation -
validateSenderalone is not enough (a renderer XSS passes it, and diagram content reaches some IPC handlers on its own), so file IPC also checks the path: writes againstassertWritablePath(paths blessed through OS chrome - file picker, file association, argv), reads/stat/watch againstassertReadablePath(the same set plus local paths declared in the requesting window's configuration, for #1278 libraries/templates/fonts). Both realpath-canonicalise first, so symlinks cannot escape. Blessing and checking must use the same native realpath (fs.realpathSync.native,fs.promises.realpath): Node's JS walkerfs.realpathSynckeeps mapped and subst drive letters and the case as given, and mixing the two refused every file on a mapped network drive in 31.5.2 (#2559) - Config-declared paths - the configuration is renderer state (
Editor.config, from localStorage.configuration) that any page script can rewrite, sosrc/main/config-paths.jsreads it once per window, at that window's firstapp-load-finished, and never again; read replies to a window wait for that read, so no file content is in the page when it happens. A path in it is readable only once allowed in a main-processdialog.showMessageBox(persisted asallowedConfigPaths, refusals last the session), because a page script can also write.configurationand open a window that boots from it (newfile). The first launch with this adopts the existing configuration without asking (allowedConfigPathsMigrated), like the recents/libraries migrations. Only this window's configured paths count, and only windows fromcreateWindowget any - CLI export symlinks - paths the user did not type are not followed (GHSA-2w35-fgjm-2vvh,
src/main/export-files.js): folder scans skip every symbolic link, even ones inside the folder, and never stat their targets (a.csvlink renders any text file into the export); output names made up from an input name refuse a link at that name, dangling or not (lstat, plusO_NOFOLLOWwhere defined; Windows has lstat only). Typed inputs and-opaths are followed like any CLI tool - GUI backup symlinks - lexical filenames cannot override an unauthorised canonical target, and unresolved final symlinks are rejected (GHSA-36x5-vw5q-29rv). Backups use an exclusively created temporary file followed by rename (
src/main/backup-file.js), so a link planted after authorisation cannot redirect the backup write. An unsafe or failed backup is skipped while the diagram save continues - Child processes - Windows system programs are started by absolute path (
getSystem32Pathinsrc/main/system-path.js) with no shell, never by bare name: Windows searches the working directory first, and opening a diagram from Explorer while draw.io is closed makes the diagram's folder the working directory (GHSA-qg46-52fx-h7p8).NoDefaultCurrentDirectoryInExePathis set at startup on Windows for dependencies that still start programs by name (electron-updater's signature check) - urlParams.json - extra editor URL params (e.g.
math-font, #298) come fromurlParams.jsonin the install folder, then userData, and the working directory only withDRAWIO_ENV=dev(src/main/url-params.js): opening a diagram from Explorer makes its folder the working directory, so a file shipped next to it reconfigured every window (embed=1,dev=1,chrome=0). The file never overrides the params that mirror main-process settings (disableUpdate,enableSpellCheck,enableStoreBkp,isGoogleFontsEnabled,appLang,autoUpdate): the Extras checkmarks read them and main flips its own copy, so a mismatch stored the opposite setting - Navigation -
will-navigatecancels every renderer-initiated main-frame navigation (including reloads), but Chromium commitsabout:URLs without a network request, so they never reach it (norwill-frame-navigate);contents.stop()indid-start-navigationcrashes Electron synchronously and is too late when deferred. A Navigation APInavigatelistener in the preload's isolated world cancels them instead (src/main/electron-preload.js) - No external transmission of diagram data
- Built-in plugins only - external/third-party plugins were removed (07/2026). The
isPluginsEnabledIPC action is retained, hardcodedfalse, so an older bundled webapp degrades to the "plugins disabled" dialog rather than failing
IPC Pattern
The preload script uses a request/response pattern with unique IDs:
// Renderer sends request
electron.request({action: 'save', data: ...}, callbackId);
// Main process handles and responds via IPC
ipcMain.on('request', (e, data) => { ... });
macOS Quick Look Preview
- Pressing Space in Finder shows a rendered preview of
.drawiofiles - Uses
quicklookjsto embed a Quick Look App Extension (.appex) in the app bundle - The
.appexloadsviewer-static.min.js(with embedded shapes) in a WKWebView - Build flow:
afterPack(fuses.mjs) applies security fuses, thenafterSign(notarize.mjs) assembles the.appex, signs it with sandbox entitlements, re-signs the outer.app, and notarizes - The
.appexis inserted inafterSign(notafterPack) so it is never present unsigned during electron-builder's signing verification - Quick Look extensions require
app-sandbox, but Electron helpers must not be sandboxed — so the.appexgets different entitlements thanentitlementsInherit - The UTI
com.jgraph.drawiois declared viaextendInfoinelectron-builder-linux-mac.json viewer-static.min.jsis saved tobuild/during CI before the cleanup step removes it from the drawio submodule; for local dev, it's read from the submodule directly
Auto-Update
- Mode per install (
getUpdateModeinsrc/main/update-mode.js, decided at runtime, #2569):autofor NSIS installs (Uninstall draw.io.exenext to the exe), macOS and AppImage;notify(a message and a link to the release page, no in-app install) for MSI, the zip, the arm64 portable exe (PORTABLE_EXECUTABLE_FILE) and deb/rpm (electron-builder'sresources/package-type);offfor the Store (process.windowsStore), Snap, Flatpak andDRAWIO_DISABLE_UPDATE=true/--disable-update/sync -- disableUpdate. Only the NSIS installer may replace an install:latest.ymllists only the NSIS exe, and electron-updater would otherwise run it over MSI and portable installs, or runpkexec dpkg -iat quit on deb - Flow: checks when the app starts and hourly after that, whenever
updateCheckIntervalHours(default 24) has passed sincelastUpdateCheck.autodownloads in the background, then the main process sendsupdateStateto the editor windows (renderer pulls it with thegetUpdateStaterequest at load). The editor shows a banner and Help > Restart to Update, which sendinstallUpdate; the macOS app menu swaps in its own Restart to Update item. A downloaded update otherwise installs at quit (autoInstallOnAppQuit, silent, UAC prompt on the per-machine NSIS install) - Restart to Update quits through the normal window close (unsaved changes prompt; cancelling keeps the update for the next quit) and installs from
window-all-closedwithquitAndInstall(true, true): the assisted NSIS installer only relaunches the app when silent - Background check and download errors are logged only; dialogs are for checks the user started
- The "Check for Updates Automatically" setting is
dontCheckUpdatesin the store, toggled from the macOS app menu or Extras > Automatic Updates (toggleAutoUpdate, initial state in theautoUpdateURL param).DRAWIO_NO_SILENT_UPDATE=true/--no-silent-updateasks before downloading - The banner, Help and Extras items live in core (
js/desktop/ElectronApp.js,Menus.js, resource keysautoUpdate,restartToUpdate,updateReady,updateAvailable); with an older webapp the update still installs at quit, and Help > Check for Updates offers Restart Now - Windows arm64 builds publish and fetch
latest-arm64.yml(publish.channelinelectron-builder-win-arm64.json, matched bysrc/main/update-channel.js) because electron-builder gives Windows manifests no arch suffix; the x64 build keepslatest.yml.npm testpins the two names together
Data Storage
- macOS:
~/Library/Application Support/draw.io - Windows:
%APPDATA%\draw.io\ - Uses
electron-storefor persistent settings
Testing
npm test runs the unit tests in src/test/ (CLI argument parsing, CLI export symlink handling, GUI backups and path authorisation, config-declared readable paths, per-window file watching, window bounds restore, print paper size and orientation, urlParams.json locations, Windows arm64 update channel, per-install update mode, MSI shortcut icon hook, System32 path helper; Node's built-in test runner). The test script in package.json names each file, so a new test file must be added there too. Everything else is manual testing, documented in doc/RELEASE_PROCESS.md:
- Launch, create diagram, add shapes, save, open
- Export (PNG, PDF, SVG)
- Undo/redo functionality
- About dialog verification
CI/CD Workflows
| Workflow | Trigger | Purpose |
|---|---|---|
electron-builder.yml |
Version tag | macOS/Linux builds |
electron-builder-win.yml |
Version tag | Windows builds (Azure Trusted Signing) + Microsoft Store submission |
prepare-release.yml |
Manual | Automated release prep |
hash-gen.yml |
Release publish, daily cron, manual | Upload checksums to releases missing them (release events are dropped by GitHub since Dec 2024, hence the cron) |
store-credentials-check.yml |
Manual | Read-only check of the Partner Center credentials (msstore apps get) |
personal-build.yml |
Manual | Unsigned fork builds, artifacts only (no secrets, no publish) |
stale.yml |
Schedule | Mark stale issues/PRs |
Important Constraints
- Recursive clone required - drawio submodule must be initialized
- Run
npm run syncbefore building - Updates version from submodule - Version source of truth -
drawio/VERSIONfor public builds;drawio-dev/VERSIONis copied overdrawio/VERSIONat CI time so the internal release number wins for packaged builds - Closed to contributions - PRs not accepted; maintained by JGraph (forks for personal use are fine, see
doc/BUILDING_FOR_PERSONAL_USE.md) - Node 22.12+ required - see
enginesinpackage.json - Windows x64/arm64 and macOS 13+ only - Electron 44 dropped ia32 binaries and macOS 12, so the 32-bit Windows build was removed (09/2026)
Development Tips
- Set
DRAWIO_ENV=devto auto-open DevTools (the unminifieddev=1sources only load from a source checkout, packaged builds omit them) - Use
npm start --enable-loggingfor verbose output - If using symlink instead of submodule, also symlink
node_modules - Main process logs to console; check terminal for errors
Key Dependencies
| Package | Purpose |
|---|---|
electron |
Desktop app framework |
electron-builder |
Build/package tool |
electron-updater |
Auto-update mechanism |
electron-store |
Persistent storage |
electron-log |
Logging |
@cantoo/pdf-lib |
PDF export |
quicklookjs |
macOS Quick Look preview extension (dev) |
CLI argument parsing is hand-rolled in src/main/args.js (no commander).