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 = .{ "window", "filesystem" },
.capabilities = .{ "webview", "js_bridge" },

Available permissions

PermissionGrants
windowWindow create/focus/close operations
filesystemFile system access from bridge commands
clipboardClipboard read/write
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
filesystemFile system access
networkNetwork access
clipboardClipboard access

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 windows (zero-native.window.*) and dialogs (zero-native.dialog.*). These are controlled separately from app-defined commands via the builtin_bridge field in RuntimeOptions.

js_window_api exposes the JavaScript window helper, but it does not bypass security. Window commands (zero-native.window.list, create, focus, close) must come from an allowed origin and must have the window permission when runtime permissions are configured. For broader control, use an explicit builtin_bridge policy:

Dialog commands (zero-native.dialog.openFile, saveFile, showMessage) are always default-deny and require an explicit builtin_bridge policy with the command listed:

.builtin_bridge = .{
    .enabled = true,
    .commands = &.{
        .{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "zero-native.dialog.openFile", .origins = .{ "zero://app" } },
        .{ .name = "zero-native.dialog.showMessage", .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 JSON message
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.

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 (windows)Denied unless js_window_api or explicit policy allows the helper and origin/permission checks passwindow permission plus exact allowed origins
Builtin bridge (dialogs)DeniedExplicit builtin_bridge policy 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.