The dashboard ships with an optional native desktop app (built with Electron 35) that wraps the existing server + client into a single application you install once and forget — a macOS .app (shipped as a .dmg) and a Windows .exe (an NSIS installer plus a no-install portable build). Everything you see in the browser at localhost:4820 lives inside this window, with native OS lifecycle on top: a menu-bar / notification-area (tray) icon, a native application menu, auto-start at login, and a single quit button that cleans up the server.
The PWA (added in #144) makes the dashboard installable in Chromium-based browsers, which is great for users who already keep the server running. The desktop app solves the orthogonal problem: starting and keeping the server running without a terminal window. Concretely:
| Capability | PWA | Desktop App |
|---|---|---|
| Installs to dock / Applications | ✅ | ✅ |
| Manages the Express server | ❌ — user must npm start separately |
✅ — embedded in-process |
| Auto-starts at login | ❌ | ✅ via macOS Login Items / Windows HKCU\…\Run |
| Menu-bar / notification-area (tray) icon for always-on status | ❌ | ✅ |
| Native application menu (⌘ / Ctrl shortcuts, etc.) | ❌ | ✅ |
| Survives browser restart | ✅ |
The two coexist — install whichever fits your workflow.
Option A — download a pre-built installer (recommended):
-
Open Releases → latest and grab the asset for your platform. Every
mastercommit that bumps the version inpackage.jsoncuts a newvX.Y.Zrelease automatically (CI publishes it), so this link always lands on the current build — no GitHub sign-in required.Platform Asset Notes macOS (Apple Silicon) ClaudeCodeMonitor-<ver>-arm64.dmgdrag into /ApplicationsmacOS (Intel) ClaudeCodeMonitor-<ver>-x64.dmgdrag into /ApplicationsWindows (installer) ClaudeCodeMonitor-Setup-<ver>-x64.exeper-user install, no admin Windows (portable) ClaudeCodeMonitor-<ver>-x64-portable.exerun without installing -
Want a per-commit build instead of waiting for a release? Every green CI run uploads a workflow artifact (sign-in required, 14-day retention) —
ClaudeCodeMonitor-dmgfrom the🍎 macOS Desktop (DMG)job andClaudeCodeMonitor-winfrom the🪟 Windows Desktop (EXE)job:gh run download <run-id> -R hoangsonww/Claude-Code-Agent-Monitor -n ClaudeCodeMonitor-dmg # or ClaudeCodeMonitor-win
-
macOS: double-click the DMG → drag
Claude Code Monitor.appinto yourApplicationsfolder. Open it; macOS may show a Gatekeeper warning the first time — see Gatekeeper & SmartScreen below. -
Windows: run
ClaudeCodeMonitor-Setup-<ver>-x64.exe(per-user, no admin) and follow the wizard, or just run the*-portable.exeto launch without installing. Windows SmartScreen may show "Windows protected your PC" the first time — see Gatekeeper & SmartScreen below.
Option B — build locally:
# In the project root, after `git clone`:
npm run setup # installs root + client + vscode-extension deps
npm run build # builds the React client
npm run desktop:install # installs Electron + electron-builder
# Build for macOS (run ON macOS) — pick one:
npm run desktop:dmg:arm64 # Apple Silicon only — FAST (~1 min); use this for your own Mac
npm run desktop:dmg:x64 # Intel only — FAST
npm run desktop:dmg # universal (x64 + arm64) — SLOW; for distributing one DMG to everyone
# Build for Windows (run ON Windows) — pick one:
npm run desktop:win # NSIS installer → desktop/release/ClaudeCodeMonitor-Setup-<ver>-x64.exe
npm run desktop:win:portable # no-install portable → desktop/release/ClaudeCodeMonitor-<ver>-x64-portable.exe
# electron-builder packages for the HOST OS — you cannot build a Windows .exe
# on macOS or a macOS .dmg on Windows.
# Open the macOS DMG you just built. Each desktop:dmg* build wipes release/ first
# and emits exactly one DMG, so match the suffix to the build above.
open desktop/release/ClaudeCodeMonitor-*-arm64.dmg # …-x64.dmg / …-universal.dmg for the othersThe universal
desktop:dmgbuild is intentionally slow. It builds the app twice (once per architecture), merges both slices with@electron/universal, and code-signs every binary — gigabytes of disk I/O, and the silentpackaging arch=universalstep can run for several minutes. For running on your own Mac, use the arch-specific command (desktop:dmg:arm64/desktop:dmg:x64) — it finishes in about a minute. CI builds the universal DMG for you and uploads it as theClaudeCodeMonitor-dmgartifact, so you rarely need to build it locally.
- The Electron main process picks a free port — preferring 4820, falling back to 4821–4829, then a random high port if all those are taken.
- If something already answers
/api/healthon port 4820 (e.g. you rannpm startin a terminal), the app adopts that server and skips starting a second one. No double-binding, no SQLite contention. - Otherwise it
require()sserver/index.jsdirectly in-process — same Node runtime as the main process, same memory. Boot is typically under two seconds. - On startup the server records its live port to
~/.claude/.agent-dashboard.json. The Claude Code hook handler reads that file, so events still reach the dashboard when the app bound a fallback port instead of 4820. - The dashboard window opens — unless the app was launched at login (on macOS via Login Items; on Windows via the
HKCU\…\Runentry, detected through a--ccam-hiddenlaunch arg since Windows has nowasOpenedAtLogin), in which case it stays tray-only. - A tray icon appears — the macOS menu bar or the Windows notification area. One click opens a dropdown with a live status snapshot (server port, active sessions, working agents, events today — all clickable to jump into the dashboard) plus Open Dashboard, Open in Browser, Restart Server, Show Logs, Open at Login (toggle), and Quit.
- Closing the window hides it. The server keeps running, the tray icon stays, and (on macOS) the dock icon stays too — clicking either re-opens the window. Independent signals that the app is still alive.
- Quitting (⌘Q / Ctrl+Q, Quit in the application menu, or Quit in the tray menu) pops a confirmation dialog — "Quit Claude Code Monitor? Press ⌘Q again to skip this prompt and quit immediately." Press Quit in the dialog, or press ⌘Q / Ctrl+Q a second time to bypass the prompt. Either way the SQLite handle is checkpointed cleanly before the process exits.
- Tray — the macOS menu bar / Windows notification area. macOS uses a black template glyph the OS tints for light/dark menu bars; Windows uses the colored
icon.ico, because a template glyph would vanish on the dark taskbar. A single click (left or right) opens the dropdown, which shows a live status snapshot pulled straight from the embedded SQLite handle each time it opens: server port, active sessions, working agents, and events today. Snapshot rows are clickable — they open the dashboard. The tray's Open Dashboard reliably raises the window even when it is minimized or behind other windows. (The application menu's File ▸ Open Dashboard / ⌘1 is macOS-only — on Windows/Linux a window-attached menu accelerator can't reopen a hidden window, so reopening is the tray's job there.) - Window / taskbar icon — the
BrowserWindowsets itsiconto the colored app logo (icon.icoon Windows,icon.pngelsewhere — the same logo as the macOS Dock, rendered fromassets/icon.svg), so an unpackageddesktop:devrun shows the real app logo in the title bar / taskbar instead of the generic Electron icon. The macOS dev Dock icon is set too; packaged apps already get theirs from the bundle.icns/.exe. - Open-at-login toggle: flip Open at Login in the tray menu (or the app menu). Both platforms go through Electron's first-party
app.*LoginItemSettingsAPI — no third-party deps. On macOS it registers via theSMAppServiceAPI, so the entry appears under → System Settings → General → Login Items. On Windows it writes a per-userHKCU\Software\Microsoft\Windows\CurrentVersion\Runentry, visible under Task Manager → Startup; a login-triggered launch is detected via a--ccam-hiddenarg (Windows has nowasOpenedAtLogin). On Linux the toggle is a no-op (unsupported). - Single-instance: double-launching just focuses the existing window. No second server, no port collision. (Applies on every platform.)
- Logs live at
~/Library/Logs/Claude Code Monitor/desktop.logon macOS and%APPDATA%\Claude Code Monitor\logs\desktop.logon Windows (use Show Logs in the tray menu to open the folder). - Your data (the SQLite database and VAPID keys) lives outside the app bundle / install dir, so it survives app reinstalls and updates —
~/Library/Application Support/Claude Code Monitor/data/on macOS,%APPDATA%\Claude Code Monitor\data\on Windows. The Windows NSIS uninstaller keeps this data by default (deleteAppDataOnUninstall: false), mirroring how dragging the.appto the Trash on macOS never touches your data. - The
claudeCLI on PATH. On macOS the app resolves it using your login-shellPATH, recovered at startup — so "Run Claude" works even though a Finder/Dock-launched app would otherwise only inherit a minimalPATH. On Windows the inherited userPATHalready includes it, so no recovery is needed. - Notifications (including the in-dashboard Send test notification button) are delivered as native OS toasts on both platforms when running inside the app — the embedded server calls Electron's
NotificationAPI directly. On Windows the app sets anAppUserModelId(com.hoangsonww.ccam.desktop, matching the electron-builderappId) so toasts attribute to the app and its taskbar windows group correctly. Web Push doesn't work reliably inside Electron (Chromium-in-Electron ships without Firebase Cloud Messaging credentials, sopushManager.subscribereturns endpoints nothing can deliver to), and this path bypasses it entirely. The web dashboard atnpm startcontinues to use Web Push as before. - Coexists with the web dashboard. You can run the desktop app and
npm run dev(ornpm start) at the same time. Each server writes its{port, pid, startedAt}entry to a shared discovery file at~/.claude/.agent-dashboard.json, and the Claude Code hook handler fan-outs each event to every live entry. Both UIs stay real-time; the two SQLite databases (the per-user data dir'sdashboard.dband the repo'sdata/dashboard.db) each record the same events independently.
desktop/
├── package.json # Electron + electron-builder
├── tsconfig.json
├── electron-builder.yml # macOS (dmg) + Windows (nsis/portable) targets; signing/notarization hooks
├── assets/ # icon.svg + generated icon.icns (macOS) + icon.ico (Windows) + tray PNGs
├── src/
│ ├── main.ts # main process entry, lifecycle; setAppUserModelId on win32
│ ├── server-host.ts # in-process Express boot, port discovery, adopt
│ ├── window.ts # BrowserWindow + persisted state
│ ├── tray.ts # tray icon (platform image: template PNG on macOS, icon.ico on Windows) + context menu
│ ├── menu.ts # native application menu
│ ├── login-item.ts # open-at-login (macOS Login Items + Windows HKCU\…\Run startup)
│ ├── shell-path.ts # recover the user's shell PATH (find `claude`)
│ ├── preload.ts # (empty — kept for future renderer bridges)
│ ├── logger.ts # file logger
│ └── constants.ts # incl. APP_ID (matches electron-builder appId)
├── scripts/
│ ├── install.js # `desktop:install` wrapper: runs npm install, then prints actionable native-dep help + exits non-zero on failure
│ ├── preflight.js # shared native-dep check (hasBetterSqliteBinary) + per-OS prerequisite help (printNativeDepHelp)
│ ├── prebuild.js # ensures root + client are built before tsc; shells npm/npx on Windows (.cmd shims); fails fast with setup help when the better-sqlite3 native binary is missing
│ ├── build-icons.sh # SVG → PNG/ICNS + tray PNGs via qlmanage/sips/iconutil (macOS)
│ ├── build-win-icon.ps1 # icon.png → icon.ico for Windows (PowerShell + .NET)
│ └── notarize.js # electron-builder afterSign hook (opt-in; macOS only)
└── tests/
└── smoke.test.mjs # spawn-and-probe /api/health (resolves the real electron binary via createRequire)
Changes outside desktop/ are deliberately minimal:
server/index.js— a behavior-preserving refactor: the post-listen bootstrap (one-time legacy-session import, update scheduler, Claude Code config watcher, orphaned-run reconciliation) was extracted into an exportedstartBackgroundServices()so the embedded server runs exactly whatnode server/index.jsruns. The standalone server path is functionally unchanged. (The legacy-session import previously sat in the standalone-onlyrequire.mainblock, so the desktop dashboard started empty — moving it intostartBackgroundServices()fixes that.) It also now publishes its live port viaserver/lib/server-info.json startup.server/lib/server-info.js(new) — writes/reads the~/.claude/.agent-dashboard.jsonport discovery file.scripts/hook-handler.js— resolves the dashboard port from the discovery file (falling back toCLAUDE_DASHBOARD_PORT, then 4820), so hook events reach the server even when it bound a fallback port.
client/, mcp/, and vscode-extension/ are untouched. The Electron main process is otherwise just a host for the same code.
The DMG is ad-hoc signed by default — that's all the project can offer without a paid Apple Developer ID. macOS will warn the first time you open it: "Apple could not verify…".
Two ways past it:
# Easiest: strip the quarantine attribute from the DMG before opening.
xattr -cr ~/Downloads/ClaudeCodeMonitor-*.dmgOr open → System Settings → Privacy & Security, scroll to the blocked DMG, click Open Anyway.
The Windows .exe (both the installer and the portable build) is unsigned by default, so Windows SmartScreen may show "Windows protected your PC" the first time you run it. Click More info → Run anyway to launch it.
Authenticode signing is opt-in for the maintainer: provide a code-signing certificate via CSC_LINK (a base64-encoded .p12) and CSC_KEY_PASSWORD and electron-builder signs the .exe automatically — no code change required. A signed build skips the SmartScreen prompt.
When you're ready to make this go away for everyone, add these three repository secrets:
| Secret | Where it comes from |
|---|---|
APPLE_ID |
Your Apple ID email |
APPLE_TEAM_ID |
Your Apple Developer team ID |
APPLE_APP_SPECIFIC_PASSWORD |
An app-specific password created at appleid.apple.com |
Optionally, also CSC_LINK (base64-encoded .p12) and CSC_KEY_PASSWORD to provide an explicit Developer ID certificate from outside the runner keychain. The CI workflow picks them up automatically — no code change required. See desktop/scripts/notarize.js for the hook.
Local builds are always ad-hoc signed: the
packagescript setsCSC_IDENTITY_AUTO_DISCOVERY=false, so a code-signing certificate already in your macOS keychain is never auto-discovered (an Apple Development cert would otherwise be picked up and fail distribution-type signing). Real signing activates only through the explicitCSC_LINKcertificate above — that path is unaffected by the flag.
# Hot-iterate on the main process (rebuilds tsc on save would be next steps;
# v1 ships without watch mode — just re-run desktop:dev after changes):
npm run desktop:dev
# Smoke test (also runs in CI on macOS):
npm run desktop:test
# macOS — single-architecture DMG — fast (~1 min):
npm run desktop:dmg:arm64 # or desktop:dmg:x64 for Intel
# macOS — universal DMG — slow (builds + signs both architectures, then merges):
npm run desktop:dmg
# Windows — NSIS installer / no-install portable (run ON Windows):
npm run desktop:win # NSIS installer .exe
npm run desktop:win:portable # no-install portable .exeelectron-builder packages for the host OS — build DMGs on macOS and the Windows
.exes on Windows. The Windows icon regenerates fromicon.pngwithnpm run build:win-icon(PowerShell + .NET); the macOS icns + tray PNGs come fromnpm run build:icons. On Windows,better-sqlite3is fetched as a prebuilt Electron binary bynpm run desktop:install(its postinstall runselectron-builder install-app-deps), so no Visual Studio C++ toolchain is needed in the common case. If that fetch/rebuild does fail (no C++ toolchain, or a Node version with no prebuilt binary),npm run desktop:install— and anydesktop:*build, gated byprebuild.js— prints the exact per-OS fix plus a no-toolchain alternative and fails loudly rather than crashing at runtime:cd desktop npm install --ignore-scripts node node_modules/electron/install.js npx electron-builder install-app-depsA Node LTS (20/22) ships prebuilt
better-sqlite3binaries and avoids the compile entirely.
After
npm run cleanindesktop/, you mustnpm run buildagain before packaging —cleanremovesout/, andelectron-builderonly packages, it does not compile. Thedesktop:dmg*scripts chain the build for you; a bareelectron-buildercall does not, and fails with "entry file out/main.js does not exist".
The smoke test does not exercise the BrowserWindow (no display on headless CI). It spawns Electron, waits for the embedded server to answer /api/health, then shuts down. Anything that depends on the renderer is part of the manual QA checklist on the PR.
- Bundle size ≈ 80 MB DMG, ≈ 250 MB on disk. The standard Electron tax. The Windows installer is comparable. Tauri would cut this dramatically but at the cost of a sidecar-process model and a Rust toolchain dependency — fair to revisit in a follow-up PR if bundle size becomes a real complaint.
- Native modules:
better-sqlite3is rebuilt against Electron's Node version automatically viaelectron-builder install-app-depsin the desktop workspace'spostinstall. On Windows it is fetched as a prebuilt Electron binary, so no Visual Studio C++ toolchain is needed in the common case. If that build does fail (or the binary is missing afterward),npm run desktop:install— and anydesktop:*build — prints the exact per-OS fix (Windows: Visual Studio Build Tools with the "Desktop development with C++" workload; macOS:xcode-select --install; Linux: build-essential + python3) plus a no-toolchain alternative (npm install --ignore-scripts→node node_modules/electron/install.js→npx electron-builder install-app-deps), and exits non-zero — failing loudly at install/build time rather than crashing at runtime. Even so, if the module is unavailable the server falls back tonode:sqlite(per #37), so the app still boots. - Universal binary:
npm run desktop:dmgproduces a DMG containing both x64 and arm64 slices, which is slow to build.npm run desktop:dmg:arm64andnpm run desktop:dmg:x64build a single-architecture DMG instead — much faster, and roughly half the size. - Auto-update: not wired on either platform. The current update path is re-download the latest installer (DMG on macOS,
.exeon Windows).electron-updater+ GitHub Releases is the natural follow-up.
| Symptom | Cause | Fix |
|---|---|---|
| "Apple could not verify…" on first launch (macOS) | Unnotarized DMG | xattr -cr ~/Downloads/ClaudeCodeMonitor-*.dmg |
| "Windows protected your PC" on first launch (Windows) | The .exe is unsigned by default (SmartScreen) |
Click More info → Run anyway. To remove the prompt for everyone, the maintainer can enable Authenticode signing via CSC_LINK + CSC_KEY_PASSWORD |
| macOS prompts to install Rosetta when opening the app | You installed the x64 build on an Apple Silicon Mac | Check your arch with uname -m (arm64 → Apple Silicon, build with desktop:dmg:arm64). Each desktop:dmg* build now wipes release/ and emits a single DMG whose mounted-volume title states the architecture — e.g. Claude Code Monitor (Apple Silicon) — so there is no ambiguous second window to drag from. If stale DMGs from an older build linger, clear them with rm -rf desktop/release and rebuild |
| Window shows but content is blank (macOS) | Server didn't boot — check ~/Library/Logs/Claude Code Monitor/desktop.log |
Restart from tray → Restart Server |
| Window shows but content is blank (Windows) | Server didn't boot — check %APPDATA%\Claude Code Monitor\logs\desktop.log |
Restart from tray → Restart Server |
| Tray icon missing (macOS) | The OS hides tray icons when the menu bar is full | Move other menu-bar items aside, or look in the overflow chevron |
| Tray icon missing (Windows) | Windows tucked it into the notification-area overflow | Click the ^ overflow chevron in the taskbar; drag the icon out to keep it pinned |
| App didn't auto-start at login (macOS) | Login Items entry got revoked by macOS | Toggle Open at Login off and on again from the tray menu |
| App didn't auto-start at login (Windows) | The HKCU\…\Run startup entry is missing or was disabled |
Toggle Open at Login off and on again from the tray menu, then confirm the entry under Task Manager → Startup is Enabled |
npm run desktop:win / :win:portable fails or produces nothing |
electron-builder packages for the host OS — you ran it on macOS/Linux | Build the Windows .exe on Windows (and DMGs on macOS) |
Desktop build/install fails on better-sqlite3 / native binary missing |
No C++ toolchain, or no prebuilt for your Node version | Run npm run desktop:install and follow the printed help, or use the no-toolchain alternative (npm install --ignore-scripts → node node_modules/electron/install.js → npx electron-builder install-app-deps); or use Node LTS 20/22 |
| Port 4820 already in use, app refuses to start | Something other than the dashboard is on 4820 and it doesn't answer /api/health |
The app will pick a fallback (4821–4829, then a random high port) — check the tray menu's port indicator |
| Dashboard stays empty — 0 sessions, 0 agents, no real-time updates | The app bound a fallback port (4820 was taken), and the Claude Code hooks were posting events to the wrong port | Fixed — the server publishes its live port to ~/.claude/.agent-dashboard.json and the hook handler reads it. After upgrading from a pre-fix build, start a new Claude Code session so the updated hooks take effect |
desktop:dmg seems stuck at packaging arch=universal |
Not stuck — the universal merge is genuinely slow | Wait a few minutes, or build a single architecture with desktop:dmg:arm64 / desktop:dmg:x64 |
Build fails: entry file out/main.js does not exist |
electron-builder was run without compiling TypeScript first |
Build via npm run desktop:dmg* (chains the build); don't invoke electron-builder bare |
Signing fails with Application … could not be found |
A code-signing certificate in your keychain was auto-discovered | Fixed — the package script sets CSC_IDENTITY_AUTO_DISCOVERY=false; build via npm run desktop:dmg* |
"Run Claude" reports the claude CLI isn't on your PATH |
A Finder/Dock-launched app inherits launchd's minimal PATH, not your shell PATH | Fixed — the app recovers your login-shell PATH at startup. If it persists, ensure claude is a real executable (not a shell alias/function) and on your shell PATH |
| Imported history / sessions vanished after updating the app | Older builds stored the database inside the (replaceable) app bundle | Fixed — data now lives in ~/Library/Application Support/Claude Code Monitor/data/ and survives reinstalls. After upgrading from a pre-fix build, re-run Import History → Rescan once |
Signing fails: Application … could not be found after retries |
A keychain code-signing certificate was auto-discovered | Fixed — the package script sets CSC_IDENTITY_AUTO_DISCOVERY=false; build via npm run desktop:dmg* |