{"owner":"jgraph","repo":"drawio-desktop","hasSkills":true,"totalSkillsCount":1,"totalTokensCount":5604,"categories":["claude-rule"],"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md"],"skills":{"CLAUDE.md":"# CLAUDE.md - AI Assistant Guide for draw.io Desktop\n\n## Project Overview\n\nDraw.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.\n\n**Repository:** https://github.com/jgraph/drawio-desktop\n**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)\n**Version:** see `drawio/VERSION` (stamped into `package.json` by `npm run sync`)\n\n## Quick Reference\n\n```bash\n# Clone (MUST be recursive for submodule)\ngit clone --recursive https://github.com/jgraph/drawio-desktop.git\n\n# Install dependencies\nnpm install\n\n# Run application\nnpm start\n\n# Run with DevTools enabled\nDRAWIO_ENV=dev npm start\n\n# Sync version before building (required)\nnpm run sync\n\n# Build for specific platforms\nnpm run release-win       # Windows x64\nnpm run release-linux     # Linux (AppImage, deb, rpm)\nnpm run release-appx      # Windows Store\n```\n\n## Project Structure\n\n```\ndrawio-desktop/\n├── src/main/\n│   ├── electron.js           # Main Electron process (3,700+ lines)\n│   ├── electron-preload.js   # IPC bridge with contextBridge\n│   ├── args.js               # CLI argument definitions and parser\n│   ├── export-files.js       # CLI export folder scan and output open (no symlink following)\n│   ├── backup-file.js        # GUI backups written by exclusive temp file and rename\n│   ├── progress-bar.js       # Progress bar for long-running operations\n│   ├── system-path.js        # Absolute paths for Windows system programs (no bare names)\n│   ├── file-watcher.js       # Per-window fs.watchFile registry (change events reach the window that asked)\n│   ├── config-paths.js       # Per-window grants for config-declared local paths (read once at load, user-allowed)\n│   ├── update-channel.js     # Windows arm64 update channel (latest-arm64.yml)\n│   ├── update-mode.js        # Per-install update mode: auto, notify (release page link) or off\n│   ├── window-bounds.js      # Restores saved window bounds against the current displays\n│   ├── print-options.js      # webContents.print() options: printer paper in portrait order + landscape flag\n│   ├── url-params.js         # urlParams.json locations (install dir, userData; cwd only in dev) and merge\n│   └── disableUpdate.js      # Generated by sync script\n├── src/test/\n│   ├── cli-args.test.js      # CLI argument parsing tests (npm test)\n│   ├── export-files.test.js  # CLI export symlink handling tests (npm test)\n│   ├── backup-file.test.js   # GUI path authorisation and backup symlink regression tests\n│   ├── system-path.test.js   # System32 path helper tests (npm test)\n│   ├── config-paths.test.js  # Config-declared path collection and per-window grant tests (npm test)\n│   ├── file-watcher.test.js  # Per-window file watch registry tests (npm test)\n│   ├── update-channel.test.js # Windows arm64 update channel tests (npm test)\n│   ├── update-mode.test.js   # Per-install update mode tests (npm test)\n│   ├── window-bounds.test.js # Window bounds restore tests (npm test)\n│   ├── print-options.test.js # Print paper size, orientation and scale tests (npm test)\n│   ├── url-params.test.js    # urlParams.json location and merge tests (npm test)\n│   └── msi-project-created.test.js # MSI shortcut icon hook tests (npm test)\n├── drawio/                   # Git submodule - core draw.io editor\n│   └── src/main/webapp/      # Web application loaded in Electron\n├── build/                    # Build resources\n│   ├── notarize.mjs          # macOS Quick Look setup, signing + notarization\n│   ├── sign-trusted.mjs      # Windows signing hook (Azure Trusted Signing)\n│   ├── fuses.mjs             # Electron security fuses\n│   ├── msi-project-created.mjs # msiProjectCreated hook: MSI shortcuts use exe icon, not the C:\\Windows\\Installer icon cache\n│   ├── dmg-hidden-files.mjs  # beforePack hook: parks hidden DMG support files outside the installer window\n│   ├── quicklook-preview.html # Quick Look preview page (viewer-static.min.js)\n│   ├── quicklook-entitlements.plist # Sandbox entitlements for .appex\n│   └── entitlements.mac.plist\n├── doc/\n│   ├── RELEASE_PROCESS.md    # Release workflow documentation\n│   └── BUILDING_FOR_PERSONAL_USE.md # Guide for unsigned fork/personal builds\n├── electron-builder-*.json   # Platform-specific build configs\n├── sync.cjs                  # Version sync script\n└── package.json\n```\n\n## Tech Stack\n\n- **Runtime:** Node.js 22.12+ (`engines` in `package.json`; CI builds on Node 24)\n- **Framework:** Electron (version pinned in `package.json`)\n- **Language:** JavaScript (ES6 modules)\n- **Build Tool:** electron-builder\n- **Package Manager:** npm\n\n## Key Files\n\n| File | Purpose |\n|------|---------|\n| `src/main/electron.js` | Main process: window management, IPC handlers, menus, auto-update |\n| `src/main/electron-preload.js` | Secure IPC bridge between renderer and main process |\n| `src/main/args.js` | CLI option definitions and argument parser (used by CLI export) |\n| `sync.cjs` | Pre-build script that syncs version from `drawio/VERSION` |\n| `electron-builder-*.json` | Platform-specific build configurations |\n| `build/sign-trusted.mjs` | electron-builder Windows signing hook (Azure Trusted Signing) |\n\n## Code Style\n\n- **ES6 modules** with `import`/`export`\n- **Tab indentation**\n- **Allman brace style** (opening brace on new line)\n- **camelCase** for variables, **PascalCase** for classes\n- No ESLint/Prettier - manual style consistency\n- Sparse comments; code clarity preferred\n\n## Git Conventions\n\n### Branches\n- `dev` - Main development branch (PR target)\n- `releases/v*.*.*` - One per release, pushed by the `prepare-release` workflow with a PR into `dev`; the `vX.Y.Z` tag goes on its head (`doc/RELEASE_PROCESS.md`). There is no long-lived `release` branch in this repo (retired after v30.4.1); the `ref: release` in the build workflows is drawio-dev's\n\n### Commit Messages\n- Lowercase sentence style without period\n- Issue references: `[jgraph/drawio-desktop#XXXX]`\n- Examples:\n  - `Fixes paste error`\n  - `Adds buffer as dependency [jgraph/drawio-desktop#2301]`\n  - `Prepare release v29.3.0`\n\n### Version Tags\nFormat: `v{MAJOR}.{MINOR}.{PATCH}` (e.g., `v29.3.0`)\nTags trigger CI/CD build workflows.\n\n## Build Process\n\n1. **Sync version:** `npm run sync` reads `drawio/VERSION` and updates `package.json`\n2. **Install:** `npm ci` for clean install\n3. **Build:** `electron-builder` with platform-specific config\n4. **Post-build:** Security fuses applied, Quick Look extension assembled (macOS), notarization (macOS)\n\n### Packaged File Set\n- The `files` arrays in the five `electron-builder-*.json` configs leave out what the packaged app never loads (about 80 MB of `app.asar`): `stencils/**/*.xml` (embedded in `js/stencils.min.js`, which overrides `mxStencilRegistry.loadStencil`), `shapes/**` (compiled into `js/shapes-14-6-5.min.js`; `mxStencilRegistry.allowEval` is false on desktop), the unminified editor sources `js/diagramly/**`, `js/grapheditor/**`, `mxgraph/src/**` and `mxgraph/mxClient.js` (only loaded with `dev=1`), the `cjs`/`dist`/`src`/`ts3.4` trees of `@cantoo/pdf-lib` (the ESM main process resolves the `es` build) and all `*.map` files. `js/diagramly/ElectronApp.js` and `DesktopLibrary.js` stay because `bootstrap.js` loads them uncompiled. Keep the five configs in sync\n- `DRAWIO_ENV=dev` on a packaged build therefore runs the minified bundles: `electron.js` only passes `dev=1` when `js/diagramly/Devel.js` exists. DevTools and main-process logging still work\n\n### Code Signing\n- **Windows:** Azure Trusted Signing via the `signtoolOptions.sign` hook `build/sign-trusted.mjs` (configured in `electron-builder-win*.json`, not CSC_LINK certificates). CI (`electron-builder-win.yml`) downloads the signing dlib, locates `signtool.exe`, and authenticates with `AZURE_TENANT_ID`/`AZURE_CLIENT_ID`/`AZURE_CLIENT_SECRET` secrets. `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 `--dir` run loads no config and ships unsigned, unfused binaries\n- **macOS:** Apple Developer certificate + notarization in `build/notarize.mjs`\n- **Unsigned builds:** `DRAWIO_UNSIGNED=true` skips signing (Windows) and notarization (macOS) for personal/fork builds\n\n### Microsoft Store\n- The `.appx` is never a GitHub release asset — `release-appx` runs `--publish never` and the `microsoft-store` job in `electron-builder-win.yml` submits it to Partner Center with the msstore CLI (`msstore publish <pkg> -id 9MVVSZK43QQW`), which commits the submission into certification\n- The appx is copied to a `.msix` name first: msstore's `MSIXProjectPublisher` only matches `.msix`/`.msixbundle`/`.msixupload`, and pointing it at the project root instead would send it down the Electron configurator path, which runs `npm install`. Same container, but Partner Center holds a `.msix` to the MSIX floor and fails the commit if `MinVersion` is 10.0.17134.0 or lower, so `electron-builder-appx.json` sets `appx.minVersion` to 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`). `maxVersionTested` is 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\n- Needs `MS_STORE_TENANT_ID`/`MS_STORE_SELLER_ID`/`MS_STORE_CLIENT_ID`/`MS_STORE_CLIENT_SECRET` secrets from an Entra app registration holding the Manager role in Partner Center; the client secret expires and must be rotated\n\n### Personal / Fork Builds\n- `doc/BUILDING_FOR_PERSONAL_USE.md` documents building unsigned from a fork (the project is closed to contributions but GPL v3 licensed)\n- Set `DRAWIO_UNSIGNED=true` and run `electron-builder` directly with `--publish never`; use `npm run sync -- disableUpdate` so auto-update doesn't replace the custom build\n- `.github/workflows/personal-build.yml` is a manual (`workflow_dispatch`) workflow that builds unsigned installers on a fork with no secrets and attaches them as run artifacts\n\n> **CI override:** The release build workflows check out the private `jgraph/drawio-dev` repo at its `release` branch, copy the built `*.min.js` **and** `VERSION` into the public `drawio/` submodule tree, then run `npm run sync` as normal. This lets CI ship from an internal release that is ahead of the public `drawio` tag without any change to `sync.cjs`. Out-of-tree builders (who have no access to `drawio-dev`) fall through to the public submodule's `VERSION` as before.\n\n### Platform Build Commands\n| Command | Target |\n|---------|--------|\n| `npm run release-win` | Windows x64 (NSIS + MSI) |\n| `npm run release-win-arm64` | Windows ARM64 |\n| `npm run release-linux` | Linux (AppImage, deb, rpm) |\n| `npm run release-appx` | Windows Store (`--publish never`; the Store job submits it) |\n| `npm run release-snap` | Snap package |\n\n## Architecture Notes\n\n### Security Model\n- **Content Security Policy** prevents remote script execution\n- **contextBridge** exposes only specific APIs to renderer\n- **validateSender()** ensures IPC calls originate from local draw.io\n- **Path authorisation** - `validateSender` alone 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 against `assertWritablePath` (paths blessed through OS chrome - file picker, file association, argv), reads/stat/watch against `assertReadablePath` (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 walker `fs.realpathSync` keeps 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)\n- **Config-declared paths** - the configuration is renderer state (`Editor.config`, from localStorage `.configuration`) that any page script can rewrite, so `src/main/config-paths.js` reads it once per window, at that window's first `app-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-process `dialog.showMessageBox` (persisted as `allowedConfigPaths`, refusals last the session), because a page script can also write `.configuration` and 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 from `createWindow` get any\n- **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 `.csv` link 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, plus `O_NOFOLLOW` where defined; Windows has lstat only). Typed inputs and `-o` paths are followed like any CLI tool\n- **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\n- **Child processes** - Windows system programs are started by absolute path (`getSystem32Path` in `src/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). `NoDefaultCurrentDirectoryInExePath` is set at startup on Windows for dependencies that still start programs by name (electron-updater's signature check)\n- **urlParams.json** - extra editor URL params (e.g. `math-font`, #298) come from `urlParams.json` in the install folder, then userData, and the working directory only with `DRAWIO_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\n- **Navigation** - `will-navigate` cancels every renderer-initiated main-frame navigation (including reloads), but Chromium commits `about:` URLs without a network request, so they never reach it (nor `will-frame-navigate`); `contents.stop()` in `did-start-navigation` crashes Electron synchronously and is too late when deferred. A Navigation API `navigate` listener in the preload's isolated world cancels them instead (`src/main/electron-preload.js`)\n- No external transmission of diagram data\n- **Built-in plugins only** - external/third-party plugins were removed (07/2026). The `isPluginsEnabled` IPC action is retained, hardcoded `false`, so an older bundled webapp degrades to the \"plugins disabled\" dialog rather than failing\n\n### IPC Pattern\nThe preload script uses a request/response pattern with unique IDs:\n```javascript\n// Renderer sends request\nelectron.request({action: 'save', data: ...}, callbackId);\n\n// Main process handles and responds via IPC\nipcMain.on('request', (e, data) => { ... });\n```\n\n### macOS Quick Look Preview\n- Pressing Space in Finder shows a rendered preview of `.drawio` files\n- Uses `quicklookjs` to embed a Quick Look App Extension (`.appex`) in the app bundle\n- The `.appex` loads `viewer-static.min.js` (with embedded shapes) in a WKWebView\n- **Build flow:** `afterPack` (fuses.mjs) applies security fuses, then `afterSign` (notarize.mjs) assembles the `.appex`, signs it with sandbox entitlements, re-signs the outer `.app`, and notarizes\n- The `.appex` is inserted in `afterSign` (not `afterPack`) so it is never present unsigned during electron-builder's signing verification\n- Quick Look extensions require `app-sandbox`, but Electron helpers must not be sandboxed — so the `.appex` gets different entitlements than `entitlementsInherit`\n- The UTI `com.jgraph.drawio` is declared via `extendInfo` in `electron-builder-linux-mac.json`\n- `viewer-static.min.js` is saved to `build/` during CI before the cleanup step removes it from the drawio submodule; for local dev, it's read from the submodule directly\n\n### Auto-Update\n- **Mode per install** (`getUpdateMode` in `src/main/update-mode.js`, decided at runtime, #2569): `auto` for NSIS installs (`Uninstall draw.io.exe` next 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's `resources/package-type`); `off` for the Store (`process.windowsStore`), Snap, Flatpak and `DRAWIO_DISABLE_UPDATE=true` / `--disable-update` / `sync -- disableUpdate`. Only the NSIS installer may replace an install: `latest.yml` lists only the NSIS exe, and electron-updater would otherwise run it over MSI and portable installs, or run `pkexec dpkg -i` at quit on deb\n- **Flow:** checks when the app starts and hourly after that, whenever `updateCheckIntervalHours` (default 24) has passed since `lastUpdateCheck`. `auto` downloads in the background, then the main process sends `updateState` to the editor windows (renderer pulls it with the `getUpdateState` request at load). The editor shows a banner and Help > Restart to Update, which send `installUpdate`; 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)\n- **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-closed` with `quitAndInstall(true, true)`: the assisted NSIS installer only relaunches the app when silent\n- Background check and download errors are logged only; dialogs are for checks the user started\n- The \"Check for Updates Automatically\" setting is `dontCheckUpdates` in the store, toggled from the macOS app menu or Extras > Automatic Updates (`toggleAutoUpdate`, initial state in the `autoUpdate` URL param). `DRAWIO_NO_SILENT_UPDATE=true` / `--no-silent-update` asks before downloading\n- The banner, Help and Extras items live in core (`js/desktop/ElectronApp.js`, `Menus.js`, resource keys `autoUpdate`, `restartToUpdate`, `updateReady`, `updateAvailable`); with an older webapp the update still installs at quit, and Help > Check for Updates offers Restart Now\n- Windows arm64 builds publish and fetch `latest-arm64.yml` (`publish.channel` in `electron-builder-win-arm64.json`, matched by `src/main/update-channel.js`) because electron-builder gives Windows manifests no arch suffix; the x64 build keeps `latest.yml`. `npm test` pins the two names together\n\n### Data Storage\n- **macOS:** `~/Library/Application Support/draw.io`\n- **Windows:** `%APPDATA%\\draw.io\\`\n- Uses `electron-store` for persistent settings\n\n## Testing\n\n`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`:\n- Launch, create diagram, add shapes, save, open\n- Export (PNG, PDF, SVG)\n- Undo/redo functionality\n- About dialog verification\n\n## CI/CD Workflows\n\n| Workflow | Trigger | Purpose |\n|----------|---------|---------|\n| `electron-builder.yml` | Version tag | macOS/Linux builds |\n| `electron-builder-win.yml` | Version tag | Windows builds (Azure Trusted Signing) + Microsoft Store submission |\n| `prepare-release.yml` | Manual | Automated release prep |\n| `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) |\n| `store-credentials-check.yml` | Manual | Read-only check of the Partner Center credentials (`msstore apps get`) |\n| `personal-build.yml` | Manual | Unsigned fork builds, artifacts only (no secrets, no publish) |\n| `stale.yml` | Schedule | Mark stale issues/PRs |\n\n## Important Constraints\n\n1. **Recursive clone required** - drawio submodule must be initialized\n2. **Run `npm run sync` before building** - Updates version from submodule\n3. **Version source of truth** - `drawio/VERSION` for public builds; `drawio-dev/VERSION` is copied over `drawio/VERSION` at CI time so the internal release number wins for packaged builds\n4. **Closed to contributions** - PRs not accepted; maintained by JGraph (forks for personal use are fine, see `doc/BUILDING_FOR_PERSONAL_USE.md`)\n5. **Node 22.12+ required** - see `engines` in `package.json`\n6. **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)\n\n## Development Tips\n\n- Set `DRAWIO_ENV=dev` to auto-open DevTools (the unminified `dev=1` sources only load from a source checkout, packaged builds omit them)\n- Use `npm start --enable-logging` for verbose output\n- If using symlink instead of submodule, also symlink `node_modules`\n- Main process logs to console; check terminal for errors\n\n## Key Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `electron` | Desktop app framework |\n| `electron-builder` | Build/package tool |\n| `electron-updater` | Auto-update mechanism |\n| `electron-store` | Persistent storage |\n| `electron-log` | Logging |\n| `@cantoo/pdf-lib` | PDF export |\n| `quicklookjs` | macOS Quick Look preview extension (dev) |\n\nCLI argument parsing is hand-rolled in `src/main/args.js` (no `commander`).\n"},"files":{"CLAUDE.md":"# CLAUDE.md - AI Assistant Guide for draw.io Desktop\n\n## Project Overview\n\nDraw.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.\n\n**Repository:** https://github.com/jgraph/drawio-desktop\n**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)\n**Version:** see `drawio/VERSION` (stamped into `package.json` by `npm run sync`)\n\n## Quick Reference\n\n```bash\n# Clone (MUST be recursive for submodule)\ngit clone --recursive https://github.com/jgraph/drawio-desktop.git\n\n# Install dependencies\nnpm install\n\n# Run application\nnpm start\n\n# Run with DevTools enabled\nDRAWIO_ENV=dev npm start\n\n# Sync version before building (required)\nnpm run sync\n\n# Build for specific platforms\nnpm run release-win       # Windows x64\nnpm run release-linux     # Linux (AppImage, deb, rpm)\nnpm run release-appx      # Windows Store\n```\n\n## Project Structure\n\n```\ndrawio-desktop/\n├── src/main/\n│   ├── electron.js           # Main Electron process (3,700+ lines)\n│   ├── electron-preload.js   # IPC bridge with contextBridge\n│   ├── args.js               # CLI argument definitions and parser\n│   ├── export-files.js       # CLI export folder scan and output open (no symlink following)\n│   ├── backup-file.js        # GUI backups written by exclusive temp file and rename\n│   ├── progress-bar.js       # Progress bar for long-running operations\n│   ├── system-path.js        # Absolute paths for Windows system programs (no bare names)\n│   ├── file-watcher.js       # Per-window fs.watchFile registry (change events reach the window that asked)\n│   ├── config-paths.js       # Per-window grants for config-declared local paths (read once at load, user-allowed)\n│   ├── update-channel.js     # Windows arm64 update channel (latest-arm64.yml)\n│   ├── update-mode.js        # Per-install update mode: auto, notify (release page link) or off\n│   ├── window-bounds.js      # Restores saved window bounds against the current displays\n│   ├── print-options.js      # webContents.print() options: printer paper in portrait order + landscape flag\n│   ├── url-params.js         # urlParams.json locations (install dir, userData; cwd only in dev) and merge\n│   └── disableUpdate.js      # Generated by sync script\n├── src/test/\n│   ├── cli-args.test.js      # CLI argument parsing tests (npm test)\n│   ├── export-files.test.js  # CLI export symlink handling tests (npm test)\n│   ├── backup-file.test.js   # GUI path authorisation and backup symlink regression tests\n│   ├── system-path.test.js   # System32 path helper tests (npm test)\n│   ├── config-paths.test.js  # Config-declared path collection and per-window grant tests (npm test)\n│   ├── file-watcher.test.js  # Per-window file watch registry tests (npm test)\n│   ├── update-channel.test.js # Windows arm64 update channel tests (npm test)\n│   ├── update-mode.test.js   # Per-install update mode tests (npm test)\n│   ├── window-bounds.test.js # Window bounds restore tests (npm test)\n│   ├── print-options.test.js # Print paper size, orientation and scale tests (npm test)\n│   ├── url-params.test.js    # urlParams.json location and merge tests (npm test)\n│   └── msi-project-created.test.js # MSI shortcut icon hook tests (npm test)\n├── drawio/                   # Git submodule - core draw.io editor\n│   └── src/main/webapp/      # Web application loaded in Electron\n├── build/                    # Build resources\n│   ├── notarize.mjs          # macOS Quick Look setup, signing + notarization\n│   ├── sign-trusted.mjs      # Windows signing hook (Azure Trusted Signing)\n│   ├── fuses.mjs             # Electron security fuses\n│   ├── msi-project-created.mjs # msiProjectCreated hook: MSI shortcuts use exe icon, not the C:\\Windows\\Installer icon cache\n│   ├── dmg-hidden-files.mjs  # beforePack hook: parks hidden DMG support files outside the installer window\n│   ├── quicklook-preview.html # Quick Look preview page (viewer-static.min.js)\n│   ├── quicklook-entitlements.plist # Sandbox entitlements for .appex\n│   └── entitlements.mac.plist\n├── doc/\n│   ├── RELEASE_PROCESS.md    # Release workflow documentation\n│   └── BUILDING_FOR_PERSONAL_USE.md # Guide for unsigned fork/personal builds\n├── electron-builder-*.json   # Platform-specific build configs\n├── sync.cjs                  # Version sync script\n└── package.json\n```\n\n## Tech Stack\n\n- **Runtime:** Node.js 22.12+ (`engines` in `package.json`; CI builds on Node 24)\n- **Framework:** Electron (version pinned in `package.json`)\n- **Language:** JavaScript (ES6 modules)\n- **Build Tool:** electron-builder\n- **Package Manager:** npm\n\n## Key Files\n\n| File | Purpose |\n|------|---------|\n| `src/main/electron.js` | Main process: window management, IPC handlers, menus, auto-update |\n| `src/main/electron-preload.js` | Secure IPC bridge between renderer and main process |\n| `src/main/args.js` | CLI option definitions and argument parser (used by CLI export) |\n| `sync.cjs` | Pre-build script that syncs version from `drawio/VERSION` |\n| `electron-builder-*.json` | Platform-specific build configurations |\n| `build/sign-trusted.mjs` | electron-builder Windows signing hook (Azure Trusted Signing) |\n\n## Code Style\n\n- **ES6 modules** with `import`/`export`\n- **Tab indentation**\n- **Allman brace style** (opening brace on new line)\n- **camelCase** for variables, **PascalCase** for classes\n- No ESLint/Prettier - manual style consistency\n- Sparse comments; code clarity preferred\n\n## Git Conventions\n\n### Branches\n- `dev` - Main development branch (PR target)\n- `releases/v*.*.*` - One per release, pushed by the `prepare-release` workflow with a PR into `dev`; the `vX.Y.Z` tag goes on its head (`doc/RELEASE_PROCESS.md`). There is no long-lived `release` branch in this repo (retired after v30.4.1); the `ref: release` in the build workflows is drawio-dev's\n\n### Commit Messages\n- Lowercase sentence style without period\n- Issue references: `[jgraph/drawio-desktop#XXXX]`\n- Examples:\n  - `Fixes paste error`\n  - `Adds buffer as dependency [jgraph/drawio-desktop#2301]`\n  - `Prepare release v29.3.0`\n\n### Version Tags\nFormat: `v{MAJOR}.{MINOR}.{PATCH}` (e.g., `v29.3.0`)\nTags trigger CI/CD build workflows.\n\n## Build Process\n\n1. **Sync version:** `npm run sync` reads `drawio/VERSION` and updates `package.json`\n2. **Install:** `npm ci` for clean install\n3. **Build:** `electron-builder` with platform-specific config\n4. **Post-build:** Security fuses applied, Quick Look extension assembled (macOS), notarization (macOS)\n\n### Packaged File Set\n- The `files` arrays in the five `electron-builder-*.json` configs leave out what the packaged app never loads (about 80 MB of `app.asar`): `stencils/**/*.xml` (embedded in `js/stencils.min.js`, which overrides `mxStencilRegistry.loadStencil`), `shapes/**` (compiled into `js/shapes-14-6-5.min.js`; `mxStencilRegistry.allowEval` is false on desktop), the unminified editor sources `js/diagramly/**`, `js/grapheditor/**`, `mxgraph/src/**` and `mxgraph/mxClient.js` (only loaded with `dev=1`), the `cjs`/`dist`/`src`/`ts3.4` trees of `@cantoo/pdf-lib` (the ESM main process resolves the `es` build) and all `*.map` files. `js/diagramly/ElectronApp.js` and `DesktopLibrary.js` stay because `bootstrap.js` loads them uncompiled. Keep the five configs in sync\n- `DRAWIO_ENV=dev` on a packaged build therefore runs the minified bundles: `electron.js` only passes `dev=1` when `js/diagramly/Devel.js` exists. DevTools and main-process logging still work\n\n### Code Signing\n- **Windows:** Azure Trusted Signing via the `signtoolOptions.sign` hook `build/sign-trusted.mjs` (configured in `electron-builder-win*.json`, not CSC_LINK certificates). CI (`electron-builder-win.yml`) downloads the signing dlib, locates `signtool.exe`, and authenticates with `AZURE_TENANT_ID`/`AZURE_CLIENT_ID`/`AZURE_CLIENT_SECRET` secrets. `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 `--dir` run loads no config and ships unsigned, unfused binaries\n- **macOS:** Apple Developer certificate + notarization in `build/notarize.mjs`\n- **Unsigned builds:** `DRAWIO_UNSIGNED=true` skips signing (Windows) and notarization (macOS) for personal/fork builds\n\n### Microsoft Store\n- The `.appx` is never a GitHub release asset — `release-appx` runs `--publish never` and the `microsoft-store` job in `electron-builder-win.yml` submits it to Partner Center with the msstore CLI (`msstore publish <pkg> -id 9MVVSZK43QQW`), which commits the submission into certification\n- The appx is copied to a `.msix` name first: msstore's `MSIXProjectPublisher` only matches `.msix`/`.msixbundle`/`.msixupload`, and pointing it at the project root instead would send it down the Electron configurator path, which runs `npm install`. Same container, but Partner Center holds a `.msix` to the MSIX floor and fails the commit if `MinVersion` is 10.0.17134.0 or lower, so `electron-builder-appx.json` sets `appx.minVersion` to 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`). `maxVersionTested` is 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\n- Needs `MS_STORE_TENANT_ID`/`MS_STORE_SELLER_ID`/`MS_STORE_CLIENT_ID`/`MS_STORE_CLIENT_SECRET` secrets from an Entra app registration holding the Manager role in Partner Center; the client secret expires and must be rotated\n\n### Personal / Fork Builds\n- `doc/BUILDING_FOR_PERSONAL_USE.md` documents building unsigned from a fork (the project is closed to contributions but GPL v3 licensed)\n- Set `DRAWIO_UNSIGNED=true` and run `electron-builder` directly with `--publish never`; use `npm run sync -- disableUpdate` so auto-update doesn't replace the custom build\n- `.github/workflows/personal-build.yml` is a manual (`workflow_dispatch`) workflow that builds unsigned installers on a fork with no secrets and attaches them as run artifacts\n\n> **CI override:** The release build workflows check out the private `jgraph/drawio-dev` repo at its `release` branch, copy the built `*.min.js` **and** `VERSION` into the public `drawio/` submodule tree, then run `npm run sync` as normal. This lets CI ship from an internal release that is ahead of the public `drawio` tag without any change to `sync.cjs`. Out-of-tree builders (who have no access to `drawio-dev`) fall through to the public submodule's `VERSION` as before.\n\n### Platform Build Commands\n| Command | Target |\n|---------|--------|\n| `npm run release-win` | Windows x64 (NSIS + MSI) |\n| `npm run release-win-arm64` | Windows ARM64 |\n| `npm run release-linux` | Linux (AppImage, deb, rpm) |\n| `npm run release-appx` | Windows Store (`--publish never`; the Store job submits it) |\n| `npm run release-snap` | Snap package |\n\n## Architecture Notes\n\n### Security Model\n- **Content Security Policy** prevents remote script execution\n- **contextBridge** exposes only specific APIs to renderer\n- **validateSender()** ensures IPC calls originate from local draw.io\n- **Path authorisation** - `validateSender` alone 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 against `assertWritablePath` (paths blessed through OS chrome - file picker, file association, argv), reads/stat/watch against `assertReadablePath` (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 walker `fs.realpathSync` keeps 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)\n- **Config-declared paths** - the configuration is renderer state (`Editor.config`, from localStorage `.configuration`) that any page script can rewrite, so `src/main/config-paths.js` reads it once per window, at that window's first `app-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-process `dialog.showMessageBox` (persisted as `allowedConfigPaths`, refusals last the session), because a page script can also write `.configuration` and 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 from `createWindow` get any\n- **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 `.csv` link 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, plus `O_NOFOLLOW` where defined; Windows has lstat only). Typed inputs and `-o` paths are followed like any CLI tool\n- **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\n- **Child processes** - Windows system programs are started by absolute path (`getSystem32Path` in `src/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). `NoDefaultCurrentDirectoryInExePath` is set at startup on Windows for dependencies that still start programs by name (electron-updater's signature check)\n- **urlParams.json** - extra editor URL params (e.g. `math-font`, #298) come from `urlParams.json` in the install folder, then userData, and the working directory only with `DRAWIO_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\n- **Navigation** - `will-navigate` cancels every renderer-initiated main-frame navigation (including reloads), but Chromium commits `about:` URLs without a network request, so they never reach it (nor `will-frame-navigate`); `contents.stop()` in `did-start-navigation` crashes Electron synchronously and is too late when deferred. A Navigation API `navigate` listener in the preload's isolated world cancels them instead (`src/main/electron-preload.js`)\n- No external transmission of diagram data\n- **Built-in plugins only** - external/third-party plugins were removed (07/2026). The `isPluginsEnabled` IPC action is retained, hardcoded `false`, so an older bundled webapp degrades to the \"plugins disabled\" dialog rather than failing\n\n### IPC Pattern\nThe preload script uses a request/response pattern with unique IDs:\n```javascript\n// Renderer sends request\nelectron.request({action: 'save', data: ...}, callbackId);\n\n// Main process handles and responds via IPC\nipcMain.on('request', (e, data) => { ... });\n```\n\n### macOS Quick Look Preview\n- Pressing Space in Finder shows a rendered preview of `.drawio` files\n- Uses `quicklookjs` to embed a Quick Look App Extension (`.appex`) in the app bundle\n- The `.appex` loads `viewer-static.min.js` (with embedded shapes) in a WKWebView\n- **Build flow:** `afterPack` (fuses.mjs) applies security fuses, then `afterSign` (notarize.mjs) assembles the `.appex`, signs it with sandbox entitlements, re-signs the outer `.app`, and notarizes\n- The `.appex` is inserted in `afterSign` (not `afterPack`) so it is never present unsigned during electron-builder's signing verification\n- Quick Look extensions require `app-sandbox`, but Electron helpers must not be sandboxed — so the `.appex` gets different entitlements than `entitlementsInherit`\n- The UTI `com.jgraph.drawio` is declared via `extendInfo` in `electron-builder-linux-mac.json`\n- `viewer-static.min.js` is saved to `build/` during CI before the cleanup step removes it from the drawio submodule; for local dev, it's read from the submodule directly\n\n### Auto-Update\n- **Mode per install** (`getUpdateMode` in `src/main/update-mode.js`, decided at runtime, #2569): `auto` for NSIS installs (`Uninstall draw.io.exe` next 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's `resources/package-type`); `off` for the Store (`process.windowsStore`), Snap, Flatpak and `DRAWIO_DISABLE_UPDATE=true` / `--disable-update` / `sync -- disableUpdate`. Only the NSIS installer may replace an install: `latest.yml` lists only the NSIS exe, and electron-updater would otherwise run it over MSI and portable installs, or run `pkexec dpkg -i` at quit on deb\n- **Flow:** checks when the app starts and hourly after that, whenever `updateCheckIntervalHours` (default 24) has passed since `lastUpdateCheck`. `auto` downloads in the background, then the main process sends `updateState` to the editor windows (renderer pulls it with the `getUpdateState` request at load). The editor shows a banner and Help > Restart to Update, which send `installUpdate`; 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)\n- **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-closed` with `quitAndInstall(true, true)`: the assisted NSIS installer only relaunches the app when silent\n- Background check and download errors are logged only; dialogs are for checks the user started\n- The \"Check for Updates Automatically\" setting is `dontCheckUpdates` in the store, toggled from the macOS app menu or Extras > Automatic Updates (`toggleAutoUpdate`, initial state in the `autoUpdate` URL param). `DRAWIO_NO_SILENT_UPDATE=true` / `--no-silent-update` asks before downloading\n- The banner, Help and Extras items live in core (`js/desktop/ElectronApp.js`, `Menus.js`, resource keys `autoUpdate`, `restartToUpdate`, `updateReady`, `updateAvailable`); with an older webapp the update still installs at quit, and Help > Check for Updates offers Restart Now\n- Windows arm64 builds publish and fetch `latest-arm64.yml` (`publish.channel` in `electron-builder-win-arm64.json`, matched by `src/main/update-channel.js`) because electron-builder gives Windows manifests no arch suffix; the x64 build keeps `latest.yml`. `npm test` pins the two names together\n\n### Data Storage\n- **macOS:** `~/Library/Application Support/draw.io`\n- **Windows:** `%APPDATA%\\draw.io\\`\n- Uses `electron-store` for persistent settings\n\n## Testing\n\n`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`:\n- Launch, create diagram, add shapes, save, open\n- Export (PNG, PDF, SVG)\n- Undo/redo functionality\n- About dialog verification\n\n## CI/CD Workflows\n\n| Workflow | Trigger | Purpose |\n|----------|---------|---------|\n| `electron-builder.yml` | Version tag | macOS/Linux builds |\n| `electron-builder-win.yml` | Version tag | Windows builds (Azure Trusted Signing) + Microsoft Store submission |\n| `prepare-release.yml` | Manual | Automated release prep |\n| `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) |\n| `store-credentials-check.yml` | Manual | Read-only check of the Partner Center credentials (`msstore apps get`) |\n| `personal-build.yml` | Manual | Unsigned fork builds, artifacts only (no secrets, no publish) |\n| `stale.yml` | Schedule | Mark stale issues/PRs |\n\n## Important Constraints\n\n1. **Recursive clone required** - drawio submodule must be initialized\n2. **Run `npm run sync` before building** - Updates version from submodule\n3. **Version source of truth** - `drawio/VERSION` for public builds; `drawio-dev/VERSION` is copied over `drawio/VERSION` at CI time so the internal release number wins for packaged builds\n4. **Closed to contributions** - PRs not accepted; maintained by JGraph (forks for personal use are fine, see `doc/BUILDING_FOR_PERSONAL_USE.md`)\n5. **Node 22.12+ required** - see `engines` in `package.json`\n6. **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)\n\n## Development Tips\n\n- Set `DRAWIO_ENV=dev` to auto-open DevTools (the unminified `dev=1` sources only load from a source checkout, packaged builds omit them)\n- Use `npm start --enable-logging` for verbose output\n- If using symlink instead of submodule, also symlink `node_modules`\n- Main process logs to console; check terminal for errors\n\n## Key Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `electron` | Desktop app framework |\n| `electron-builder` | Build/package tool |\n| `electron-updater` | Auto-update mechanism |\n| `electron-store` | Persistent storage |\n| `electron-log` | Logging |\n| `@cantoo/pdf-lib` | PDF export |\n| `quicklookjs` | macOS Quick Look preview extension (dev) |\n\nCLI argument parsing is hand-rolled in `src/main/args.js` (no `commander`).\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/jgraph/drawio-desktop/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md - AI Assistant Guide for draw.io Desktop\n\n## Project Overview\n\nDraw.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.\n\n**Repository:** https://github.com/jgraph/drawio-desktop\n**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)\n**Version:** see `drawio/VERSION` (stamped into `package.json` by `npm run sync`)\n\n## Quick Reference\n\n```bash\n# Clone (MUST be recursive for submodule)\ngit clone --recursive https://github.com/jgraph/drawio-desktop.git\n\n# Install dependencies\nnpm install\n\n# Run application\nnpm start\n\n# Run with DevTools enabled\nDRAWIO_ENV=dev npm start\n\n# Sync version before building (required)\nnpm run sync\n\n# Build for specific platforms\nnpm run release-win       # Windows x64\nnpm run release-linux     # Linux (AppImage, deb, rpm)\nnpm run release-appx      # Windows Store\n```\n\n## Project Structure\n\n```\ndrawio-desktop/\n├── src/main/\n│   ├── electron.js           # Main Electron process (3,700+ lines)\n│   ├── electron-preload.js   # IPC bridge with contextBridge\n│   ├── args.js               # CLI argument definitions and parser\n│   ├── export-files.js       # CLI export folder scan and output open (no symlink following)\n│   ├── backup-file.js        # GUI backups written by exclusive temp file and rename\n│   ├── progress-bar.js       # Progress bar for long-running operations\n│   ├── system-path.js        # Absolute paths for Windows system programs (no bare names)\n│   ├── file-watcher.js       # Per-window fs.watchFile registry (change events reach the window that asked)\n│   ├── config-paths.js       # Per-window grants for config-declared local paths (read once at load, user-allowed)\n│   ├── update-channel.js     # Windows arm64 update channel (latest-arm64.yml)\n│   ├── update-mode.js        # Per-install update mode: auto, notify (release page link) or off\n│   ├── window-bounds.js      # Restores saved window bounds against the current displays\n│   ├── print-options.js      # webContents.print() options: printer paper in portrait order + landscape flag\n│   ├── url-params.js         # urlParams.json locations (install dir, userData; cwd only in dev) and merge\n│   └── disableUpdate.js      # Generated by sync script\n├── src/test/\n│   ├── cli-args.test.js      # CLI argument parsing tests (npm test)\n│   ├── export-files.test.js  # CLI export symlink handling tests (npm test)\n│   ├── backup-file.test.js   # GUI path authorisation and backup symlink regression tests\n│   ├── system-path.test.js   # System32 path helper tests (npm test)\n│   ├── config-paths.test.js  # Config-declared path collection and per-window grant tests (npm test)\n│   ├── file-watcher.test.js  # Per-window file watch registry tests (npm test)\n│   ├── update-channel.test.js # Windows arm64 update channel tests (npm test)\n│   ├── update-mode.test.js   # Per-install update mode tests (npm test)\n│   ├── window-bounds.test.js # Window bounds restore tests (npm test)\n│   ├── print-options.test.js # Print paper size, orientation and scale tests (npm test)\n│   ├── url-params.test.js    # urlParams.json location and merge tests (npm test)\n│   └── msi-project-created.test.js # MSI shortcut icon hook tests (npm test)\n├── drawio/                   # Git submodule - core draw.io editor\n│   └── src/main/webapp/      # Web application loaded in Electron\n├── build/                    # Build resources\n│   ├── notarize.mjs          # macOS Quick Look setup, signing + notarization\n│   ├── sign-trusted.mjs      # Windows signing hook (Azure Trusted Signing)\n│   ├── fuses.mjs             # Electron security fuses\n│   ├── msi-project-created.mjs # msiProjectCreated hook: MSI shortcuts use exe icon, not the C:\\Windows\\Installer icon cache\n│   ├── dmg-hidden-files.mjs  # beforePack hook: parks hidden DMG support files outside the installer window\n│   ├── quicklook-preview.html # Quick Look preview page (viewer-static.min.js)\n│   ├── quicklook-entitlements.plist # Sandbox entitlements for .appex\n│   └── entitlements.mac.plist\n├── doc/\n│   ├── RELEASE_PROCESS.md    # Release workflow documentation\n│   └── BUILDING_FOR_PERSONAL_USE.md # Guide for unsigned fork/personal builds\n├── electron-builder-*.json   # Platform-specific build configs\n├── sync.cjs                  # Version sync script\n└── package.json\n```\n\n## Tech Stack\n\n- **Runtime:** Node.js 22.12+ (`engines` in `package.json`; CI builds on Node 24)\n- **Framework:** Electron (version pinned in `package.json`)\n- **Language:** JavaScript (ES6 modules)\n- **Build Tool:** electron-builder\n- **Package Manager:** npm\n\n## Key Files\n\n| File | Purpose |\n|------|---------|\n| `src/main/electron.js` | Main process: window management, IPC handlers, menus, auto-update |\n| `src/main/electron-preload.js` | Secure IPC bridge between renderer and main process |\n| `src/main/args.js` | CLI option definitions and argument parser (used by CLI export) |\n| `sync.cjs` | Pre-build script that syncs version from `drawio/VERSION` |\n| `electron-builder-*.json` | Platform-specific build configurations |\n| `build/sign-trusted.mjs` | electron-builder Windows signing hook (Azure Trusted Signing) |\n\n## Code Style\n\n- **ES6 modules** with `import`/`export`\n- **Tab indentation**\n- **Allman brace style** (opening brace on new line)\n- **camelCase** for variables, **PascalCase** for classes\n- No ESLint/Prettier - manual style consistency\n- Sparse comments; code clarity preferred\n\n## Git Conventions\n\n### Branches\n- `dev` - Main development branch (PR target)\n- `releases/v*.*.*` - One per release, pushed by the `prepare-release` workflow with a PR into `dev`; the `vX.Y.Z` tag goes on its head (`doc/RELEASE_PROCESS.md`). There is no long-lived `release` branch in this repo (retired after v30.4.1); the `ref: release` in the build workflows is drawio-dev's\n\n### Commit Messages\n- Lowercase sentence style without period\n- Issue references: `[jgraph/drawio-desktop#XXXX]`\n- Examples:\n  - `Fixes paste error`\n  - `Adds buffer as dependency [jgraph/drawio-desktop#2301]`\n  - `Prepare release v29.3.0`\n\n### Version Tags\nFormat: `v{MAJOR}.{MINOR}.{PATCH}` (e.g., `v29.3.0`)\nTags trigger CI/CD build workflows.\n\n## Build Process\n\n1. **Sync version:** `npm run sync` reads `drawio/VERSION` and updates `package.json`\n2. **Install:** `npm ci` for clean install\n3. **Build:** `electron-builder` with platform-specific config\n4. **Post-build:** Security fuses applied, Quick Look extension assembled (macOS), notarization (macOS)\n\n### Packaged File Set\n- The `files` arrays in the five `electron-builder-*.json` configs leave out what the packaged app never loads (about 80 MB of `app.asar`): `stencils/**/*.xml` (embedded in `js/stencils.min.js`, which overrides `mxStencilRegistry.loadStencil`), `shapes/**` (compiled into `js/shapes-14-6-5.min.js`; `mxStencilRegistry.allowEval` is false on desktop), the unminified editor sources `js/diagramly/**`, `js/grapheditor/**`, `mxgraph/src/**` and `mxgraph/mxClient.js` (only loaded with `dev=1`), the `cjs`/`dist`/`src`/`ts3.4` trees of `@cantoo/pdf-lib` (the ESM main process resolves the `es` build) and all `*.map` files. `js/diagramly/ElectronApp.js` and `DesktopLibrary.js` stay because `bootstrap.js` loads them uncompiled. Keep the five configs in sync\n- `DRAWIO_ENV=dev` on a packaged build therefore runs the minified bundles: `electron.js` only passes `dev=1` when `js/diagramly/Devel.js` exists. DevTools and main-process logging still work\n\n### Code Signing\n- **Windows:** Azure Trusted Signing via the `signtoolOptions.sign` hook `build/sign-trusted.mjs` (configured in `electron-builder-win*.json`, not CSC_LINK certificates). CI (`electron-builder-win.yml`) downloads the signing dlib, locates `signtool.exe`, and authenticates with `AZURE_TENANT_ID`/`AZURE_CLIENT_ID`/`AZURE_CLIENT_SECRET` secrets. `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 `--dir` run loads no config and ships unsigned, unfused binaries\n- **macOS:** Apple Developer certificate + notarization in `build/notarize.mjs`\n- **Unsigned builds:** `DRAWIO_UNSIGNED=true` skips signing (Windows) and notarization (macOS) for personal/fork builds\n\n### Microsoft Store\n- The `.appx` is never a GitHub release asset — `release-appx` runs `--publish never` and the `microsoft-store` job in `electron-builder-win.yml` submits it to Partner Center with the msstore CLI (`msstore publish <pkg> -id 9MVVSZK43QQW`), which commits the submission into certification\n- The appx is copied to a `.msix` name first: msstore's `MSIXProjectPublisher` only matches `.msix`/`.msixbundle`/`.msixupload`, and pointing it at the project root instead would send it down the Electron configurator path, which runs `npm install`. Same container, but Partner Center holds a `.msix` to the MSIX floor and fails the commit if `MinVersion` is 10.0.17134.0 or lower, so `electron-builder-appx.json` sets `appx.minVersion` to 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`). `maxVersionTested` is 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\n- Needs `MS_STORE_TENANT_ID`/`MS_STORE_SELLER_ID`/`MS_STORE_CLIENT_ID`/`MS_STORE_CLIENT_SECRET` secrets from an Entra app registration holding the Manager role in Partner Center; the client secret expires and must be rotated\n\n### Personal / Fork Builds\n- `doc/BUILDING_FOR_PERSONAL_USE.md` documents building unsigned from a fork (the project is closed to contributions but GPL v3 licensed)\n- Set `DRAWIO_UNSIGNED=true` and run `electron-builder` directly with `--publish never`; use `npm run sync -- disableUpdate` so auto-update doesn't replace the custom build\n- `.github/workflows/personal-build.yml` is a manual (`workflow_dispatch`) workflow that builds unsigned installers on a fork with no secrets and attaches them as run artifacts\n\n> **CI override:** The release build workflows check out the private `jgraph/drawio-dev` repo at its `release` branch, copy the built `*.min.js` **and** `VERSION` into the public `drawio/` submodule tree, then run `npm run sync` as normal. This lets CI ship from an internal release that is ahead of the public `drawio` tag without any change to `sync.cjs`. Out-of-tree builders (who have no access to `drawio-dev`) fall through to the public submodule's `VERSION` as before.\n\n### Platform Build Commands\n| Command | Target |\n|---------|--------|\n| `npm run release-win` | Windows x64 (NSIS + MSI) |\n| `npm run release-win-arm64` | Windows ARM64 |\n| `npm run release-linux` | Linux (AppImage, deb, rpm) |\n| `npm run release-appx` | Windows Store (`--publish never`; the Store job submits it) |\n| `npm run release-snap` | Snap package |\n\n## Architecture Notes\n\n### Security Model\n- **Content Security Policy** prevents remote script execution\n- **contextBridge** exposes only specific APIs to renderer\n- **validateSender()** ensures IPC calls originate from local draw.io\n- **Path authorisation** - `validateSender` alone 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 against `assertWritablePath` (paths blessed through OS chrome - file picker, file association, argv), reads/stat/watch against `assertReadablePath` (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 walker `fs.realpathSync` keeps 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)\n- **Config-declared paths** - the configuration is renderer state (`Editor.config`, from localStorage `.configuration`) that any page script can rewrite, so `src/main/config-paths.js` reads it once per window, at that window's first `app-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-process `dialog.showMessageBox` (persisted as `allowedConfigPaths`, refusals last the session), because a page script can also write `.configuration` and 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 from `createWindow` get any\n- **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 `.csv` link 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, plus `O_NOFOLLOW` where defined; Windows has lstat only). Typed inputs and `-o` paths are followed like any CLI tool\n- **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\n- **Child processes** - Windows system programs are started by absolute path (`getSystem32Path` in `src/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). `NoDefaultCurrentDirectoryInExePath` is set at startup on Windows for dependencies that still start programs by name (electron-updater's signature check)\n- **urlParams.json** - extra editor URL params (e.g. `math-font`, #298) come from `urlParams.json` in the install folder, then userData, and the working directory only with `DRAWIO_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\n- **Navigation** - `will-navigate` cancels every renderer-initiated main-frame navigation (including reloads), but Chromium commits `about:` URLs without a network request, so they never reach it (nor `will-frame-navigate`); `contents.stop()` in `did-start-navigation` crashes Electron synchronously and is too late when deferred. A Navigation API `navigate` listener in the preload's isolated world cancels them instead (`src/main/electron-preload.js`)\n- No external transmission of diagram data\n- **Built-in plugins only** - external/third-party plugins were removed (07/2026). The `isPluginsEnabled` IPC action is retained, hardcoded `false`, so an older bundled webapp degrades to the \"plugins disabled\" dialog rather than failing\n\n### IPC Pattern\nThe preload script uses a request/response pattern with unique IDs:\n```javascript\n// Renderer sends request\nelectron.request({action: 'save', data: ...}, callbackId);\n\n// Main process handles and responds via IPC\nipcMain.on('request', (e, data) => { ... });\n```\n\n### macOS Quick Look Preview\n- Pressing Space in Finder shows a rendered preview of `.drawio` files\n- Uses `quicklookjs` to embed a Quick Look App Extension (`.appex`) in the app bundle\n- The `.appex` loads `viewer-static.min.js` (with embedded shapes) in a WKWebView\n- **Build flow:** `afterPack` (fuses.mjs) applies security fuses, then `afterSign` (notarize.mjs) assembles the `.appex`, signs it with sandbox entitlements, re-signs the outer `.app`, and notarizes\n- The `.appex` is inserted in `afterSign` (not `afterPack`) so it is never present unsigned during electron-builder's signing verification\n- Quick Look extensions require `app-sandbox`, but Electron helpers must not be sandboxed — so the `.appex` gets different entitlements than `entitlementsInherit`\n- The UTI `com.jgraph.drawio` is declared via `extendInfo` in `electron-builder-linux-mac.json`\n- `viewer-static.min.js` is saved to `build/` during CI before the cleanup step removes it from the drawio submodule; for local dev, it's read from the submodule directly\n\n### Auto-Update\n- **Mode per install** (`getUpdateMode` in `src/main/update-mode.js`, decided at runtime, #2569): `auto` for NSIS installs (`Uninstall draw.io.exe` next 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's `resources/package-type`); `off` for the Store (`process.windowsStore`), Snap, Flatpak and `DRAWIO_DISABLE_UPDATE=true` / `--disable-update` / `sync -- disableUpdate`. Only the NSIS installer may replace an install: `latest.yml` lists only the NSIS exe, and electron-updater would otherwise run it over MSI and portable installs, or run `pkexec dpkg -i` at quit on deb\n- **Flow:** checks when the app starts and hourly after that, whenever `updateCheckIntervalHours` (default 24) has passed since `lastUpdateCheck`. `auto` downloads in the background, then the main process sends `updateState` to the editor windows (renderer pulls it with the `getUpdateState` request at load). The editor shows a banner and Help > Restart to Update, which send `installUpdate`; 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)\n- **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-closed` with `quitAndInstall(true, true)`: the assisted NSIS installer only relaunches the app when silent\n- Background check and download errors are logged only; dialogs are for checks the user started\n- The \"Check for Updates Automatically\" setting is `dontCheckUpdates` in the store, toggled from the macOS app menu or Extras > Automatic Updates (`toggleAutoUpdate`, initial state in the `autoUpdate` URL param). `DRAWIO_NO_SILENT_UPDATE=true` / `--no-silent-update` asks before downloading\n- The banner, Help and Extras items live in core (`js/desktop/ElectronApp.js`, `Menus.js`, resource keys `autoUpdate`, `restartToUpdate`, `updateReady`, `updateAvailable`); with an older webapp the update still installs at quit, and Help > Check for Updates offers Restart Now\n- Windows arm64 builds publish and fetch `latest-arm64.yml` (`publish.channel` in `electron-builder-win-arm64.json`, matched by `src/main/update-channel.js`) because electron-builder gives Windows manifests no arch suffix; the x64 build keeps `latest.yml`. `npm test` pins the two names together\n\n### Data Storage\n- **macOS:** `~/Library/Application Support/draw.io`\n- **Windows:** `%APPDATA%\\draw.io\\`\n- Uses `electron-store` for persistent settings\n\n## Testing\n\n`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`:\n- Launch, create diagram, add shapes, save, open\n- Export (PNG, PDF, SVG)\n- Undo/redo functionality\n- About dialog verification\n\n## CI/CD Workflows\n\n| Workflow | Trigger | Purpose |\n|----------|---------|---------|\n| `electron-builder.yml` | Version tag | macOS/Linux builds |\n| `electron-builder-win.yml` | Version tag | Windows builds (Azure Trusted Signing) + Microsoft Store submission |\n| `prepare-release.yml` | Manual | Automated release prep |\n| `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) |\n| `store-credentials-check.yml` | Manual | Read-only check of the Partner Center credentials (`msstore apps get`) |\n| `personal-build.yml` | Manual | Unsigned fork builds, artifacts only (no secrets, no publish) |\n| `stale.yml` | Schedule | Mark stale issues/PRs |\n\n## Important Constraints\n\n1. **Recursive clone required** - drawio submodule must be initialized\n2. **Run `npm run sync` before building** - Updates version from submodule\n3. **Version source of truth** - `drawio/VERSION` for public builds; `drawio-dev/VERSION` is copied over `drawio/VERSION` at CI time so the internal release number wins for packaged builds\n4. **Closed to contributions** - PRs not accepted; maintained by JGraph (forks for personal use are fine, see `doc/BUILDING_FOR_PERSONAL_USE.md`)\n5. **Node 22.12+ required** - see `engines` in `package.json`\n6. **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)\n\n## Development Tips\n\n- Set `DRAWIO_ENV=dev` to auto-open DevTools (the unminified `dev=1` sources only load from a source checkout, packaged builds omit them)\n- Use `npm start --enable-logging` for verbose output\n- If using symlink instead of submodule, also symlink `node_modules`\n- Main process logs to console; check terminal for errors\n\n## Key Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `electron` | Desktop app framework |\n| `electron-builder` | Build/package tool |\n| `electron-updater` | Auto-update mechanism |\n| `electron-store` | Persistent storage |\n| `electron-log` | Logging |\n| `@cantoo/pdf-lib` | PDF export |\n| `quicklookjs` | macOS Quick Look preview extension (dev) |\n\nCLI argument parsing is hand-rolled in `src/main/args.js` (no `commander`).\n","isInternal":false,"tokens":5604,"sizeBytes":23286}],"systemPromptSnippet":"<yakaai_skills repo=\"jgraph/drawio-desktop\">\n<!-- File: CLAUDE.md (Tokens: ~5604 | Category: claude-rule) -->\n# CLAUDE.md - AI Assistant Guide for draw.io Desktop\n\n## Project Overview\n\nDraw.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.\n\n**Repository:** https://github.com/jgraph/drawio-desktop\n**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)\n**Version:** see `drawio/VERSION` (stamped into `package.json` by `npm run sync`)\n\n## Quick Reference\n\n```bash\n# Clone (MUST be recursive for submodule)\ngit clone --recursive https://github.com/jgraph/drawio-desktop.git\n\n# Install dependencies\nnpm install\n\n# Run application\nnpm start\n\n# Run with DevTools enabled\nDRAWIO_ENV=dev npm start\n\n# Sync version before building (required)\nnpm run sync\n\n# Build for specific platforms\nnpm run release-win       # Windows x64\nnpm run release-linux     # Linux (AppImage, deb, rpm)\nnpm run release-appx      # Windows Store\n```\n\n## Project Structure\n\n```\ndrawio-desktop/\n├── src/main/\n│   ├── electron.js           # Main Electron process (3,700+ lines)\n│   ├── electron-preload.js   # IPC bridge with contextBridge\n│   ├── args.js               # CLI argument definitions and parser\n│   ├── export-files.js       # CLI export folder scan and output open (no symlink following)\n│   ├── backup-file.js        # GUI backups written by exclusive temp file and rename\n│   ├── progress-bar.js       # Progress bar for long-running operations\n│   ├── system-path.js        # Absolute paths for Windows system programs (no bare names)\n│   ├── file-watcher.js       # Per-window fs.watchFile registry (change events reach the window that asked)\n│   ├── config-paths.js       # Per-window grants for config-declared local paths (read once at load, user-allowed)\n│   ├── update-channel.js     # Windows arm64 update channel (latest-arm64.yml)\n│   ├── update-mode.js        # Per-install update mode: auto, notify (release page link) or off\n│   ├── window-bounds.js      # Restores saved window bounds against the current displays\n│   ├── print-options.js      # webContents.print() options: printer paper in portrait order + landscape flag\n│   ├── url-params.js         # urlParams.json locations (install dir, userData; cwd only in dev) and merge\n│   └── disableUpdate.js      # Generated by sync script\n├── src/test/\n│   ├── cli-args.test.js      # CLI argument parsing tests (npm test)\n│   ├── export-files.test.js  # CLI export symlink handling tests (npm test)\n│   ├── backup-file.test.js   # GUI path authorisation and backup symlink regression tests\n│   ├── system-path.test.js   # System32 path helper tests (npm test)\n│   ├── config-paths.test.js  # Config-declared path collection and per-window grant tests (npm test)\n│   ├── file-watcher.test.js  # Per-window file watch registry tests (npm test)\n│   ├── update-channel.test.js # Windows arm64 update channel tests (npm test)\n│   ├── update-mode.test.js   # Per-install update mode tests (npm test)\n│   ├── window-bounds.test.js # Window bounds restore tests (npm test)\n│   ├── print-options.test.js # Print paper size, orientation and scale tests (npm test)\n│   ├── url-params.test.js    # urlParams.json location and merge tests (npm test)\n│   └── msi-project-created.test.js # MSI shortcut icon hook tests (npm test)\n├── drawio/                   # Git submodule - core draw.io editor\n│   └── src/main/webapp/      # Web application loaded in Electron\n├── build/                    # Build resources\n│   ├── notarize.mjs          # macOS Quick Look setup, signing + notarization\n│   ├── sign-trusted.mjs      # Windows signing hook (Azure Trusted Signing)\n│   ├── fuses.mjs             # Electron security fuses\n│   ├── msi-project-created.mjs # msiProjectCreated hook: MSI shortcuts use exe icon, not the C:\\Windows\\Installer icon cache\n│   ├── dmg-hidden-files.mjs  # beforePack hook: parks hidden DMG support files outside the installer window\n│   ├── quicklook-preview.html # Quick Look preview page (viewer-static.min.js)\n│   ├── quicklook-entitlements.plist # Sandbox entitlements for .appex\n│   └── entitlements.mac.plist\n├── doc/\n│   ├── RELEASE_PROCESS.md    # Release workflow documentation\n│   └── BUILDING_FOR_PERSONAL_USE.md # Guide for unsigned fork/personal builds\n├── electron-builder-*.json   # Platform-specific build configs\n├── sync.cjs                  # Version sync script\n└── package.json\n```\n\n## Tech Stack\n\n- **Runtime:** Node.js 22.12+ (`engines` in `package.json`; CI builds on Node 24)\n- **Framework:** Electron (version pinned in `package.json`)\n- **Language:** JavaScript (ES6 modules)\n- **Build Tool:** electron-builder\n- **Package Manager:** npm\n\n## Key Files\n\n| File | Purpose |\n|------|---------|\n| `src/main/electron.js` | Main process: window management, IPC handlers, menus, auto-update |\n| `src/main/electron-preload.js` | Secure IPC bridge between renderer and main process |\n| `src/main/args.js` | CLI option definitions and argument parser (used by CLI export) |\n| `sync.cjs` | Pre-build script that syncs version from `drawio/VERSION` |\n| `electron-builder-*.json` | Platform-specific build configurations |\n| `build/sign-trusted.mjs` | electron-builder Windows signing hook (Azure Trusted Signing) |\n\n## Code Style\n\n- **ES6 modules** with `import`/`export`\n- **Tab indentation**\n- **Allman brace style** (opening brace on new line)\n- **camelCase** for variables, **PascalCase** for classes\n- No ESLint/Prettier - manual style consistency\n- Sparse comments; code clarity preferred\n\n## Git Conventions\n\n### Branches\n- `dev` - Main development branch (PR target)\n- `releases/v*.*.*` - One per release, pushed by the `prepare-release` workflow with a PR into `dev`; the `vX.Y.Z` tag goes on its head (`doc/RELEASE_PROCESS.md`). There is no long-lived `release` branch in this repo (retired after v30.4.1); the `ref: release` in the build workflows is drawio-dev's\n\n### Commit Messages\n- Lowercase sentence style without period\n- Issue references: `[jgraph/drawio-desktop#XXXX]`\n- Examples:\n  - `Fixes paste error`\n  - `Adds buffer as dependency [jgraph/drawio-desktop#2301]`\n  - `Prepare release v29.3.0`\n\n### Version Tags\nFormat: `v{MAJOR}.{MINOR}.{PATCH}` (e.g., `v29.3.0`)\nTags trigger CI/CD build workflows.\n\n## Build Process\n\n1. **Sync version:** `npm run sync` reads `drawio/VERSION` and updates `package.json`\n2. **Install:** `npm ci` for clean install\n3. **Build:** `electron-builder` with platform-specific config\n4. **Post-build:** Security fuses applied, Quick Look extension assembled (macOS), notarization (macOS)\n\n### Packaged File Set\n- The `files` arrays in the five `electron-builder-*.json` configs leave out what the packaged app never loads (about 80 MB of `app.asar`): `stencils/**/*.xml` (embedded in `js/stencils.min.js`, which overrides `mxStencilRegistry.loadStencil`), `shapes/**` (compiled into `js/shapes-14-6-5.min.js`; `mxStencilRegistry.allowEval` is false on desktop), the unminified editor sources `js/diagramly/**`, `js/grapheditor/**`, `mxgraph/src/**` and `mxgraph/mxClient.js` (only loaded with `dev=1`), the `cjs`/`dist`/`src`/`ts3.4` trees of `@cantoo/pdf-lib` (the ESM main process resolves the `es` build) and all `*.map` files. `js/diagramly/ElectronApp.js` and `DesktopLibrary.js` stay because `bootstrap.js` loads them uncompiled. Keep the five configs in sync\n- `DRAWIO_ENV=dev` on a packaged build therefore runs the minified bundles: `electron.js` only passes `dev=1` when `js/diagramly/Devel.js` exists. DevTools and main-process logging still work\n\n### Code Signing\n- **Windows:** Azure Trusted Signing via the `signtoolOptions.sign` hook `build/sign-trusted.mjs` (configured in `electron-builder-win*.json`, not CSC_LINK certificates). CI (`electron-builder-win.yml`) downloads the signing dlib, locates `signtool.exe`, and authenticates with `AZURE_TENANT_ID`/`AZURE_CLIENT_ID`/`AZURE_CLIENT_SECRET` secrets. `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 `--dir` run loads no config and ships unsigned, unfused binaries\n- **macOS:** Apple Developer certificate + notarization in `build/notarize.mjs`\n- **Unsigned builds:** `DRAWIO_UNSIGNED=true` skips signing (Windows) and notarization (macOS) for personal/fork builds\n\n### Microsoft Store\n- The `.appx` is never a GitHub release asset — `release-appx` runs `--publish never` and the `microsoft-store` job in `electron-builder-win.yml` submits it to Partner Center with the msstore CLI (`msstore publish <pkg> -id 9MVVSZK43QQW`), which commits the submission into certification\n- The appx is copied to a `.msix` name first: msstore's `MSIXProjectPublisher` only matches `.msix`/`.msixbundle`/`.msixupload`, and pointing it at the project root instead would send it down the Electron configurator path, which runs `npm install`. Same container, but Partner Center holds a `.msix` to the MSIX floor and fails the commit if `MinVersion` is 10.0.17134.0 or lower, so `electron-builder-appx.json` sets `appx.minVersion` to 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`). `maxVersionTested` is 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\n- Needs `MS_STORE_TENANT_ID`/`MS_STORE_SELLER_ID`/`MS_STORE_CLIENT_ID`/`MS_STORE_CLIENT_SECRET` secrets from an Entra app registration holding the Manager role in Partner Center; the client secret expires and must be rotated\n\n### Personal / Fork Builds\n- `doc/BUILDING_FOR_PERSONAL_USE.md` documents building unsigned from a fork (the project is closed to contributions but GPL v3 licensed)\n- Set `DRAWIO_UNSIGNED=true` and run `electron-builder` directly with `--publish never`; use `npm run sync -- disableUpdate` so auto-update doesn't replace the custom build\n- `.github/workflows/personal-build.yml` is a manual (`workflow_dispatch`) workflow that builds unsigned installers on a fork with no secrets and attaches them as run artifacts\n\n> **CI override:** The release build workflows check out the private `jgraph/drawio-dev` repo at its `release` branch, copy the built `*.min.js` **and** `VERSION` into the public `drawio/` submodule tree, then run `npm run sync` as normal. This lets CI ship from an internal release that is ahead of the public `drawio` tag without any change to `sync.cjs`. Out-of-tree builders (who have no access to `drawio-dev`) fall through to the public submodule's `VERSION` as before.\n\n### Platform Build Commands\n| Command | Target |\n|---------|--------|\n| `npm run release-win` | Windows x64 (NSIS + MSI) |\n| `npm run release-win-arm64` | Windows ARM64 |\n| `npm run release-linux` | Linux (AppImage, deb, rpm) |\n| `npm run release-appx` | Windows Store (`--publish never`; the Store job submits it) |\n| `npm run release-snap` | Snap package |\n\n## Architecture Notes\n\n### Security Model\n- **Content Security Policy** prevents remote script execution\n- **contextBridge** exposes only specific APIs to renderer\n- **validateSender()** ensures IPC calls originate from local draw.io\n- **Path authorisation** - `validateSender` alone 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 against `assertWritablePath` (paths blessed through OS chrome - file picker, file association, argv), reads/stat/watch against `assertReadablePath` (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 walker `fs.realpathSync` keeps 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)\n- **Config-declared paths** - the configuration is renderer state (`Editor.config`, from localStorage `.configuration`) that any page script can rewrite, so `src/main/config-paths.js` reads it once per window, at that window's first `app-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-process `dialog.showMessageBox` (persisted as `allowedConfigPaths`, refusals last the session), because a page script can also write `.configuration` and 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 from `createWindow` get any\n- **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 `.csv` link 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, plus `O_NOFOLLOW` where defined; Windows has lstat only). Typed inputs and `-o` paths are followed like any CLI tool\n- **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\n- **Child processes** - Windows system programs are started by absolute path (`getSystem32Path` in `src/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). `NoDefaultCurrentDirectoryInExePath` is set at startup on Windows for dependencies that still start programs by name (electron-updater's signature check)\n- **urlParams.json** - extra editor URL params (e.g. `math-font`, #298) come from `urlParams.json` in the install folder, then userData, and the working directory only with `DRAWIO_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\n- **Navigation** - `will-navigate` cancels every renderer-initiated main-frame navigation (including reloads), but Chromium commits `about:` URLs without a network request, so they never reach it (nor `will-frame-navigate`); `contents.stop()` in `did-start-navigation` crashes Electron synchronously and is too late when deferred. A Navigation API `navigate` listener in the preload's isolated world cancels them instead (`src/main/electron-preload.js`)\n- No external transmission of diagram data\n- **Built-in plugins only** - external/third-party plugins were removed (07/2026). The `isPluginsEnabled` IPC action is retained, hardcoded `false`, so an older bundled webapp degrades to the \"plugins disabled\" dialog rather than failing\n\n### IPC Pattern\nThe preload script uses a request/response pattern with unique IDs:\n```javascript\n// Renderer sends request\nelectron.request({action: 'save', data: ...}, callbackId);\n\n// Main process handles and responds via IPC\nipcMain.on('request', (e, data) => { ... });\n```\n\n### macOS Quick Look Preview\n- Pressing Space in Finder shows a rendered preview of `.drawio` files\n- Uses `quicklookjs` to embed a Quick Look App Extension (`.appex`) in the app bundle\n- The `.appex` loads `viewer-static.min.js` (with embedded shapes) in a WKWebView\n- **Build flow:** `afterPack` (fuses.mjs) applies security fuses, then `afterSign` (notarize.mjs) assembles the `.appex`, signs it with sandbox entitlements, re-signs the outer `.app`, and notarizes\n- The `.appex` is inserted in `afterSign` (not `afterPack`) so it is never present unsigned during electron-builder's signing verification\n- Quick Look extensions require `app-sandbox`, but Electron helpers must not be sandboxed — so the `.appex` gets different entitlements than `entitlementsInherit`\n- The UTI `com.jgraph.drawio` is declared via `extendInfo` in `electron-builder-linux-mac.json`\n- `viewer-static.min.js` is saved to `build/` during CI before the cleanup step removes it from the drawio submodule; for local dev, it's read from the submodule directly\n\n### Auto-Update\n- **Mode per install** (`getUpdateMode` in `src/main/update-mode.js`, decided at runtime, #2569): `auto` for NSIS installs (`Uninstall draw.io.exe` next 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's `resources/package-type`); `off` for the Store (`process.windowsStore`), Snap, Flatpak and `DRAWIO_DISABLE_UPDATE=true` / `--disable-update` / `sync -- disableUpdate`. Only the NSIS installer may replace an install: `latest.yml` lists only the NSIS exe, and electron-updater would otherwise run it over MSI and portable installs, or run `pkexec dpkg -i` at quit on deb\n- **Flow:** checks when the app starts and hourly after that, whenever `updateCheckIntervalHours` (default 24) has passed since `lastUpdateCheck`. `auto` downloads in the background, then the main process sends `updateState` to the editor windows (renderer pulls it with the `getUpdateState` request at load). The editor shows a banner and Help > Restart to Update, which send `installUpdate`; 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)\n- **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-closed` with `quitAndInstall(true, true)`: the assisted NSIS installer only relaunches the app when silent\n- Background check and download errors are logged only; dialogs are for checks the user started\n- The \"Check for Updates Automatically\" setting is `dontCheckUpdates` in the store, toggled from the macOS app menu or Extras > Automatic Updates (`toggleAutoUpdate`, initial state in the `autoUpdate` URL param). `DRAWIO_NO_SILENT_UPDATE=true` / `--no-silent-update` asks before downloading\n- The banner, Help and Extras items live in core (`js/desktop/ElectronApp.js`, `Menus.js`, resource keys `autoUpdate`, `restartToUpdate`, `updateReady`, `updateAvailable`); with an older webapp the update still installs at quit, and Help > Check for Updates offers Restart Now\n- Windows arm64 builds publish and fetch `latest-arm64.yml` (`publish.channel` in `electron-builder-win-arm64.json`, matched by `src/main/update-channel.js`) because electron-builder gives Windows manifests no arch suffix; the x64 build keeps `latest.yml`. `npm test` pins the two names together\n\n### Data Storage\n- **macOS:** `~/Library/Application Support/draw.io`\n- **Windows:** `%APPDATA%\\draw.io\\`\n- Uses `electron-store` for persistent settings\n\n## Testing\n\n`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`:\n- Launch, create diagram, add shapes, save, open\n- Export (PNG, PDF, SVG)\n- Undo/redo functionality\n- About dialog verification\n\n## CI/CD Workflows\n\n| Workflow | Trigger | Purpose |\n|----------|---------|---------|\n| `electron-builder.yml` | Version tag | macOS/Linux builds |\n| `electron-builder-win.yml` | Version tag | Windows builds (Azure Trusted Signing) + Microsoft Store submission |\n| `prepare-release.yml` | Manual | Automated release prep |\n| `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) |\n| `store-credentials-check.yml` | Manual | Read-only check of the Partner Center credentials (`msstore apps get`) |\n| `personal-build.yml` | Manual | Unsigned fork builds, artifacts only (no secrets, no publish) |\n| `stale.yml` | Schedule | Mark stale issues/PRs |\n\n## Important Constraints\n\n1. **Recursive clone required** - drawio submodule must be initialized\n2. **Run `npm run sync` before building** - Updates version from submodule\n3. **Version source of truth** - `drawio/VERSION` for public builds; `drawio-dev/VERSION` is copied over `drawio/VERSION` at CI time so the internal release number wins for packaged builds\n4. **Closed to contributions** - PRs not accepted; maintained by JGraph (forks for personal use are fine, see `doc/BUILDING_FOR_PERSONAL_USE.md`)\n5. **Node 22.12+ required** - see `engines` in `package.json`\n6. **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)\n\n## Development Tips\n\n- Set `DRAWIO_ENV=dev` to auto-open DevTools (the unminified `dev=1` sources only load from a source checkout, packaged builds omit them)\n- Use `npm start --enable-logging` for verbose output\n- If using symlink instead of submodule, also symlink `node_modules`\n- Main process logs to console; check terminal for errors\n\n## Key Dependencies\n\n| Package | Purpose |\n|---------|---------|\n| `electron` | Desktop app framework |\n| `electron-builder` | Build/package tool |\n| `electron-updater` | Auto-update mechanism |\n| `electron-store` | Persistent storage |\n| `electron-log` | Logging |\n| `@cantoo/pdf-lib` | PDF export |\n| `quicklookjs` | macOS Quick Look preview extension (dev) |\n\nCLI argument parsing is hand-rolled in `src/main/args.js` (no `commander`).\n</yakaai_skills>"}