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
| Permission | Grants |
|---|---|
window | Window create/focus/close operations and layered WebView management |
command | App command routing from trusted WebView bridge calls |
view | Generic native view create/list/update/focus/close operations |
dialog | Native file and message dialogs from explicit builtin bridge policies |
filesystem | File system access from bridge commands |
clipboard | Clipboard read/write |
credentials | Credential store read/write/delete operations |
network | Network requests from native code |
camera | Camera access |
microphone | Microphone access |
location | Location services |
notifications | System notifications |
Custom permissions use reverse-DNS names (e.g. com.example.my-permission). Use the smallest set that covers your app.
Available capabilities
| Capability | Description |
|---|---|
webview | WebView rendering |
js_bridge | JavaScript bridge |
native_module | Native Zig extension modules |
native_views | Native shell views, chrome, controls, and utility panels |
menus | Native app and window menus |
shortcuts | Keyboard shortcut registration |
tray | System tray integration |
filesystem | File system access |
network | Network access |
notifications | System notification access |
dialog | Native file and message dialog access |
clipboard | Clipboard access |
credentials | Credential store access |
open_url | Open URLs with the system browser |
reveal_path | Reveal local paths in the platform file manager |
recent_documents | Platform recent document registration |
file_drops | Native file drop events |
app_activation_events | App activate and deactivate lifecycle events |
file_associations | Package metadata for file type registration |
url_schemes | Package 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:
| Code | Cause |
|---|---|
invalid_request | Malformed input, unsupported built-in operation, denied navigation URL, missing window/WebView, duplicate or reserved WebView label, or another caller-fixable request problem |
unknown_command | No handler registered for this command |
permission_denied | Origin or permission check failed |
handler_failed | Handler returned an error |
payload_too_large | Message exceeds 16 KiB limit |
internal_error | Unexpected runtime error |
Handle errors in JavaScript:
try {
const result = await window.zero.invoke("native.ping", {});
} catch (error) {
console.error(error.code, error.message);
}Navigation policy
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
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
| Layer | Default | Opt-in |
|---|---|---|
| App bridge commands | Denied | Per-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 pass | command, view, or window permission plus exact allowed origins |
| Builtin bridge (dialogs and OS capabilities) | Denied | Explicit builtin_bridge policy with matching permissions required |
| Navigation | Blocked | Allowlisted origins |
| External links | Denied | Explicit action + URL prefix list |
| Permissions | None granted | Declared in app.zon, checked at runtime |
| CSP | Not enforced by zero-native | Set 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.