Murasaki

Windows & permissions

Declare multiple native windows and give each renderer only the commands it needs.

Murasaki creates application windows from murasaki.config.ts. The primary window is the existing window field and always has the label main. Additional entries live in windows, keyed by a stable label:

murasaki.config.ts
import { defineConfig } from 'murasaki'

export default defineConfig({
  appId: 'com.example.notes',
  productName: 'Notes',

  capabilities: ['clipboard:readText'], // fallback for main only
  window: {
    route: '/',
    width: 1100,
    height: 760,
    capabilities: ['window:getLabel', 'window:open', 'window:list', 'window:manage'],
  },

  windows: {
    settings: {
      route: '/settings',
      title: 'Settings',
      width: 720,
      height: 560,
      // visible defaults to false for secondary windows
      capabilities: [],
    },
    preview: {
      route: '/preview',
      visible: false,
      capabilities: ['clipboard:readText'],
    },
  },
})

All declared windows are created by the host. Secondary windows start hidden unless visible: true is set. A secondary window with no capabilities is deny-all; it never inherits the top-level list. For backwards compatibility, the primary window resolves permissions as window.capabilities ?? capabilities ?? [].

Open a hidden secondary from a visible renderer, normally main. WebKit and WebView2 may either run that renderer before it is shown or defer its JavaScript until after it becomes visible, so do not depend on either timing and do not make the hidden renderer responsible for calling windows.open() on itself. Native visibility is applied by the OS event loop; when code must observe the post-open state through windows.list(), wait for visible to become true instead of assuming the first read is synchronous with the compositor.

The Windows backend console option is primary-only because it controls the application backend console, not an individual native window. Putting console on a secondary declaration is rejected during configuration resolution.

Labels must be 1–64 characters, start with a letter or number, and contain only letters, numbers, ., _, or -. main is reserved. A route must be a same-origin path beginning with /; full URLs, protocol-relative URLs, and backslashes are rejected during configuration resolution.

Identify and manage windows

Use appWindow for the renderer's own native window and windows for a declared window by label:

'use client'

import { appWindow, windows } from 'murasaki/native'

const current = await appWindow.getLabel()
await windows.open('settings')
await windows.focus('settings')

const all = await windows.list()
const settings = all.find((item) => item.label === 'settings')
console.log({ current, settings })

await windows.hide('settings')

windows.list() returns WindowInfo[]:

interface WindowInfo {
  label: string
  primary: boolean
  visible: boolean
  focused: boolean
  minimized: boolean
  maximized: boolean
}
APICapabilityBehavior
appWindow.getLabel()window:getLabelReturns the current renderer's label.
windows.open(label)window:openShows, restores, and focuses a live declared window.
windows.list()window:listReturns state for all live declared windows.
windows.show(label) / hide(label) / focus(label) / close(label)window:manageManages another declared window.

Only labels declared in configuration are available. These APIs do not create an arbitrary route or a new runtime window.

The OS close control and appWindow.close() hide a secondary window, so windows.open(label) can show that same declared window again. In contrast, windows.close(label) explicitly destroys a secondary target; a destroyed secondary cannot be recreated in the current release. Closing main remains an application quit request.

Primary close and application lifecycle

Using the OS close control or appWindow.close() on a secondary hides that window and affects no other window. Closing main is an application quit request: it runs beforeQuit(), can be cancelled there, and then runs bounded shutdown() before the native host exits. Murasaki does not currently expose a separate per-window close hook. Use windows.close(label) only when the secondary should be destroyed for the rest of the process lifetime.

Per-window least privilege

Permissions belong to the renderer that makes the call. A settings window that only edits React state can use capabilities: []; a preview that reads the clipboard can receive only clipboard:readText. Management permissions should normally live on the trusted primary renderer:

murasaki.config.ts
export default defineConfig({
  appId: 'com.example.notes',
  productName: 'Notes',
  window: {
    route: '/',
    capabilities: ['window:open', 'window:list', 'window:manage'],
  },
  windows: {
    settings: { route: '/settings', capabilities: [] },
    importer: { route: '/import', capabilities: ['dialog:openFile'] },
  },
})

Per-window capability lists limit which native command names a compromised renderer can call. They do not scope arguments: window:manage is broad, and dialog:openFile cannot yet be limited to one directory. Do not load remote or user-authored executable content into any privileged renderer, and validate app-level intent before broad operations.

This is a native-command boundary, not a complete renderer sandbox. Declared windows currently share the same application origin and authenticated local Node session, so Server Actions, 'use main', API routes, and updater HTTP endpoints are not separated by these lists. Authenticate and authorize application operations inside those handlers; do not treat a deny-all window as safe for untrusted executable content.

Next

On this page