Security

zero-native treats the WebView as untrusted by default. App authors opt into native power with explicit permissions, command policies, and navigation rules.

Permissions and capabilities

capabilities describe broad features an app uses. permissions are the runtime grants checked before native commands run.

.permissions = .{ "command", "view", "dialog", "window", "filesystem" },
.capabilities = .{ "webview", "js_bridge", "native_views", "dialog", "filesystem" },

Available permissions

PermissionGrants
windowWindow create/focus/close operations and layered WebView management
commandApp command routing from trusted WebView bridge calls
viewGeneric native view create/list/update/focus/close operations
dialogNative file and message dialogs from explicit builtin bridge policies
filesystemFile system access from bridge commands
clipboardClipboard read/write
credentialsCredential store read/write/delete operations
networkNetwork requests from native code
cameraCamera access
microphoneMicrophone access
locationLocation services
notificationsSystem notifications

Custom permissions use reverse-DNS names (e.g. com.example.my-permission). Use the smallest set that covers your app.

Available capabilities

CapabilityDescription
webviewWebView rendering
js_bridgeJavaScript bridge
native_moduleNative Zig extension modules
native_viewsNative shell views, chrome, controls, and utility panels
menusNative app and window menus
shortcutsKeyboard shortcut registration
traySystem tray integration
filesystemFile system access
networkNetwork access
notificationsSystem notification access
dialogNative file and message dialog access
clipboardClipboard access
credentialsCredential store access
open_urlOpen URLs with the system browser
reveal_pathReveal local paths in the platform file manager
recent_documentsPlatform recent document registration
file_dropsNative file drop events
app_activation_eventsApp activate and deactivate lifecycle events
file_associationsPackage metadata for file type registration
url_schemesPackage metadata for URL scheme registration

Native commands

Native bridge commands are default-deny. A command must be registered by native code and allowed by policy before the runtime invokes it.

.bridge = .{
    .commands = .{
        .{
            .name = "native.ping",
            .origins = .{ "zero://app" },
        },
        .{
            .name = "zero-native.window.create",
            .permissions = .{ "window" },
            .origins = .{ "zero://app" },
        },
    },
},

Prefer exact origins over "*". Use "*" only for local development or commands that do not expose native state.

Builtin bridge policy

zero-native provides built-in commands for app command routing (zero-native.command.*), windows (zero-native.window.*), generic native views (zero-native.view.*), layered WebViews (zero-native.webview.*), platform support queries (zero-native.platform.*), dialogs (zero-native.dialog.*), selected OS capabilities (zero-native.os.*), clipboard access (zero-native.clipboard.*), and credential storage (zero-native.credentials.*). These are controlled separately from app-defined commands via the builtin_bridge field in RuntimeOptions.

js_window_api exposes the JavaScript command, window, view, WebView, and platform support helpers, but it does not bypass security. Command routing (zero-native.command.invoke) must come from an allowed origin and have the command permission when runtime permissions are configured. Generic native view commands (zero-native.view.create, list, update, setFrame, setVisible, focus, focusNext, focusPrevious, close) require the view permission. Platform support queries, window commands (zero-native.window.list, create, focus, close), and WebView commands (zero-native.webview.create, list, setFrame, navigate, setZoom, setLayer, close) require the window permission. The legacy window grant is still accepted for command and view helpers for compatibility. View and WebView commands can only target the window that sent the bridge message. WebView URLs must also be allowed by security.navigation.allowed_origins, and child WebViews receive window.zero only when created with bridge: true.

For broader control, use an explicit builtin_bridge policy. When you choose this path, list every built-in command your app calls. Dialog commands (zero-native.dialog.openFile, saveFile, showMessage), OS commands (zero-native.os.openUrl, showNotification, revealPath, addRecentDocument, clearRecentDocuments), clipboard commands (zero-native.clipboard.readText, writeText, read, write), and credential commands (zero-native.credentials.set, get, delete) are always default-deny and require an explicit builtin_bridge policy with the command listed. zero-native.os.openUrl also requires security.navigation.external_links to allow the target URL:

.builtin_bridge = .{
    .enabled = true,
    .commands = &.{
        .{ .name = "zero-native.command.invoke", .permissions = .{ "command" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.platform.supports", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.view.create", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.view.list", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.view.update", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.view.setFrame", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.view.setVisible", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.view.focus", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.view.focusNext", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.view.focusPrevious", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.view.close", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.list", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.setFrame", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.navigate", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.setZoom", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.setLayer", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.webview.close", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.dialog.openFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.dialog.saveFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.dialog.showMessage", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.os.openUrl", .permissions = .{ "network" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.os.showNotification", .permissions = .{ "notifications" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.os.revealPath", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.os.addRecentDocument", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.os.clearRecentDocuments", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.clipboard.readText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.clipboard.writeText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.clipboard.read", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.clipboard.write", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.credentials.set", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.credentials.get", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.credentials.delete", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
    },
},

Bridge error codes

When a bridge call fails, the JavaScript promise rejects with an error containing a code field:

CodeCause
invalid_requestMalformed input, unsupported built-in operation, denied navigation URL, missing window/WebView, duplicate or reserved WebView label, or another caller-fixable request problem
unknown_commandNo handler registered for this command
permission_deniedOrigin or permission check failed
handler_failedHandler returned an error
payload_too_largeMessage exceeds 16 KiB limit
internal_errorUnexpected runtime error

Handle errors in JavaScript:

try {
  const result = await window.zero.invoke("native.ping", {});
} catch (error) {
  console.error(error.code, error.message);
}

Main-frame navigation is allowlisted. Packaged assets normally use zero://app, inline examples use zero://inline, and dev servers should list their exact local origin.

.security = .{
    .navigation = .{
        .allowed_origins = .{
            "zero://app",
            "zero://inline",
            "http://127.0.0.1:5173",
        },
    },
},

Unknown main-frame navigations are blocked unless the external-link policy explicitly handles them.

External links are denied by default. To open links in the system browser, opt in and list URL prefixes:

.security = .{
    .navigation = .{
        .external_links = .{
            .action = "open_system_browser",
            .allowed_urls = .{ "https://example.com/docs/*" },
        },
    },
},

Do not allow broad external patterns for pages that can be influenced by remote content.

The same policy gates runtime.openExternalUrl(...) and window.zero.os.openUrl(...). A bridge grant for zero-native.os.openUrl is not enough by itself; the URL must also match external_links.allowed_urls.

CSP guidance

For packaged assets, start with a strict Content Security Policy:

<meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'">

For inline Zig examples that embed scripts or styles, add only the minimum inline allowances:

<meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'">

For dev servers, extend connect-src only to the local dev origin and WebSocket endpoint required by the framework. Keep production CSP separate from development CSP.

Security model summary

LayerDefaultOpt-in
App bridge commandsDeniedPer-command policy with origin and permission checks
Builtin bridge (commands, windows, views, and WebViews)Denied unless js_window_api or explicit policy allows the helper and origin/permission checks passcommand, view, or window permission plus exact allowed origins
Builtin bridge (dialogs and OS capabilities)DeniedExplicit builtin_bridge policy with matching permissions required
NavigationBlockedAllowlisted origins
External linksDeniedExplicit action + URL prefix list
PermissionsNone grantedDeclared in app.zon, checked at runtime
CSPNot enforced by zero-nativeSet in your HTML <meta> tag

The goal is defense in depth: even if a command is registered in Zig, it won't execute unless the policy allows it from the requesting origin with the required permissions.