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
| Permission | Grants |
|---|---|
window | Window create/focus/close operations |
filesystem | File system access from bridge commands |
clipboard | Clipboard read/write |
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 |
filesystem | File system access |
network | Network access |
clipboard | Clipboard 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:
| Code | Cause |
|---|---|
invalid_request | Malformed JSON message |
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.
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 (windows) | Denied unless js_window_api or explicit policy allows the helper and origin/permission checks pass | window permission plus exact allowed origins |
| Builtin bridge (dialogs) | Denied | Explicit builtin_bridge policy 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.