Tauri 2 permissions: capabilities without the runtime denials
How Tauri 2's capability files, plugin crates and builder registrations fit together, why a compiling app still denies its own commands, and how to grant the narrowest permission a desktop feature actually needs.
A Tauri app starts able to do nothing
A Tauri 2 window can render its webview. That is the whole of what it may do until a capability file says otherwise. There is no ambient filesystem, no shell, no HTTP to anywhere but its own origin — the access control list denies by default, and every ability an app has is one someone wrote down.
That is a better default than the desktop frameworks it replaces, and it is also the source of the most common confusion in a Tauri port: the code compiles, the plugin is installed, the frontend calls it, and the call fails at runtime with a denied-permission error.
The free Tauri permissions generator builds the capability file alongside the two other files that have to agree with it, which is the fastest way to see the shape of the problem.
Three files, one decision
Adding a plugin to a Tauri 2 app is not one edit. It is three, and skipping any of them fails differently:
src-tauri/Cargo.toml— the crate, e.g.tauri-plugin-fs = "2". Missing, and the project does not compile.src-tauri/src/lib.rs— the registration,.plugin(tauri_plugin_fs::init())on the builder. Missing, and the plugin's commands do not exist at runtime, however healthy the dependency graph looks.src-tauri/capabilities/default.json— the permission,"fs:default"in thepermissionsarray. Missing, and everything compiles, the command exists, and the invocation is refused by the ACL.
Only the third failure happens after a successful build, which is why it is the one that reaches users. Treat the trio as a single unit of work: a crate with no registration is dead weight in the binary, and a registration with no permission is a feature that works on nobody's machine.
If the frontend calls the plugin, there is a fourth piece — the guest bindings on npm. Every official plugin ships as a crate (tauri-plugin-fs) and a matching package (@tauri-apps/plugin-fs). The crate is the implementation in the native host; the npm package is the typed wrapper the webview imports to reach it.
What a capability file actually says
A capability is a named grant applied to named windows:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capabilities granted to the main window.",
"windows": ["main"],
"permissions": ["core:default", "dialog:default"]
}
Three parts are worth reading carefully. The identifier is the file's name, and a project can have several capability files — which is the mechanism for giving a settings window less than the main one. The windows array is the scope: a permission granted here reaches those window labels and no others. And permissions is additive, so the array is the complete, auditable answer to what this window may do.
core:default is the baseline every window needs. Everything after it is a decision.
Not all grants cost the same
Ranking the common plugins by what they hand over is more useful than ranking them by how often they are used:
shellis the widest grant available. It can launch other programs. Ship the default permission and the app can run whatever an injected script asks it to; scope it to an allow-list of exact commands instead.fsreads and writes the user's disk. The plugin's own scope configuration is where you say which paths —fs:defaultis broader than most features need.httpmakes requests to hosts outside the app's origin, which is precisely what the webview's content security policy exists to prevent. Restrict the URLs.clipboard-managerreads whatever was last copied, which is frequently a password from a password manager.updaterdownloads and installs new code. It is exactly as safe as the signing key behind it, and no safer.os,notificationanddialogare the mild end.dialogin particular is the safest way to touch files at all, because the user picks the path in a native picker and the app never needed a filesystem grant to begin with.
The point of the ranking is not to avoid the top of it. Plenty of good desktop apps need the shell. The point is that the capability file is a document a reviewer, a security-conscious user or a future maintainer will read, and each line should have a feature behind it.
Prefer the narrow permission to the default one
Most plugins expose more than a :default permission. fs distinguishes reading from writing and scopes paths; shell distinguishes opening a URL in the system browser from executing a command. Reaching for :default because it is the documented one-liner is how an app ends up with an ACL that describes the plugin rather than the product.
The same applies to windows. A capability that lists ["main"] is a narrower statement than one that omits the field, and a second capability file for a second window is cheap.
One case earns its own note: a custom title bar. An undecorated window has no OS chrome, so the buttons the app draws instead need the specific core:window:* permissions for dragging, minimising, maximising and closing. That is a real functional requirement, and it is also a good example of scoping — core:window:allow-start-dragging and its four siblings, not core:window:default.
Verify by removal
The check that catches an over-broad capability file is mechanical, and worth running before a release:
- The app builds and every feature works with exactly the declared permissions.
- Removing any one permission breaks exactly one feature — the one that justified it.
- Every crate in
Cargo.tomlis registered inlib.rs, and every registration has a permission. - No permission was added to make an error message go away without knowing which call produced it.
That last one is where capability files rot. A denied-permission error is easy to silence by widening the array, and the widening outlives the debugging session. Read the error, find the command, grant the permission that command needs.
Where this fits in a project
Permissions are not a packaging step to be sorted out at the end. What a desktop app is allowed to do is a product decision, and it is easier to make while the features are still being planned than after they are built.
In the Nodlume workspace, a Desktop project's capabilities live beside the canvas the app is designed on, and the generated src-tauri host, the Cargo.toml and the capability file are emitted from that one selection — so the three files cannot drift apart in the first place. If you want to work out the grants before committing to any of that, the Tauri permissions generator is the same picker on its own, free and with no account. Building for phones as well? The React Native permissions generator is its mobile counterpart, and browser extensions have their own manifest generator.
Target-specific scaffolding
Continue this learning path
Permissions, manifests, capabilities, agent projects and MCP servers for browser extensions, desktop, mobile, and backend agents.
Design your application
Design Web, Desktop, Mobile, Terminal, or Extension projects with a target-aware canvas and exporter.
Open projects