Plugin Permissions
Learn how to configure granular plugin permissions in Tauri v2 to control which commands each window can execute, using capability files and scoped access rules.
Tauri v2 treats every plugin command as a potential security boundary. Without explicit permission, a command cannot be called from the frontend—not even by accident. Plugin permissions define exactly which commands a window is allowed to invoke, and under what constraints.
Think of permissions as a precise access control list for the IPC bridge. A plugin may expose dozens of commands that read, write, or modify system resources. Your job as the application developer is to grant only the ones a particular window actually needs. A settings window, for example, should never be able to spawn a shell process or write to the file system unless you explicitly allow it.
How Plugin Permissions Work
Every Tauri plugin ships a set of permission identifiers. These are defined in the plugin’s permissions/ directory—often autogenerated from the commands the plugin exposes. The identifiers follow a strict naming convention: plugin-name:command-name for individual commands, or plugin-name:default for a curated set of safe defaults.
When you add a permission to a capability file, you are declaring that the associated commands may cross the IPC bridge from the frontend to the Rust backend. If a command is invoked without its required permission, Tauri’s runtime blocks the call and logs an error. The Native APIs permissions chapter covers the same model from the API side.
The permissions you declare are additive (you grant access), but plugins also ship deny-* variants. Adding a deny permission explicitly forbids a command, even if a broader allow rule would otherwise grant it. This lets you, for example, enable a plugin’s default set but then strip away one specific command you consider too permissive.
Where permissions come from:
Permissions are not something you invent. They are defined by the plugin author and live inside the plugin crate. You discover them through the plugin’s documentation, its source code, or—as a last resort—by intentionally triggering a build error with a fake permission name to see the list of valid identifiers.
Allowing Plugin Permissions
The most common starting point is to include a plugin’s default permission set. This grants a reasonable, minimal set of commands that let you use the plugin without opening up everything.
Capability files live in src-tauri/capabilities/. A file named default.json might look like this:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default"
]
}
Here, core:default enables the essential Tauri core APIs (window management, events, app metadata), and fs:default activates the filesystem plugin with read access to the app’s own data directory and nothing else. Every window labeled main receives this capability.
Default is a curated set:
The default permission is designed to be the smallest useful set. It is not an “everything allowed” wildcard. Always check the plugin’s documentation to see exactly which commands it includes.
Restricting Plugin Permissions
Allowing a plugin’s default set is only the first step. Real applications often need to tighten access further. Two mechanisms let you do this: deny lists and scoped permissions.
Using Deny Permissions
If a default set includes a command you do not want, add the corresponding deny-* permission to the capability file. Deny rules take precedence, so you can safely keep the default set and then block the specific command.
Suppose fs:default includes fs:allow-exists, but you want to prevent any code from checking file existence on disk. You would write:
"permissions": [
"fs:default",
"fs:deny-exists"
]
Scoped Permissions
Some plugin commands accept parameters that determine which resources they can touch—for example, a file path. Scoped permissions let you restrict a command to operate only on a specific set of paths, URLs, or other identifiers.
A scope is written as an inline object inside the permissions array. It must include the permission identifier and an allow (or deny) list of constraints.
For the filesystem plugin, a scope that permits writing only to a single file in the user’s home directory looks like this:
{
"identifier": "default",
"windows": ["main"],
"permissions": [
"fs:allow-write-text-file",
{
"identifier": "fs:allow-write-text-file",
"allow": [{ "path": "$HOME/my-app/notes.txt" }]
}
]
}
The first entry enables the command itself. The second entry, with the same identifier, scopes it so that writes are only allowed to the path $HOME/my-app/notes.txt. Any attempt to write to a different path will be denied at runtime.
Scope doesn’t replace the permission:
The scoped object must repeat the exact same permission identifier. Without the plain string entry "fs:allow-write-text-file", the command is still considered unpermitted. The scope only adds a filter on top of an existing allow rule.
Command Permissions
Every command a plugin exposes has two associated permission identifiers: plugin:allow-command-name and plugin:deny-command-name. You can grant permissions at the individual command level if the default set is either too broad or nonexistent.
For a custom plugin named doggy with a command bark, the permission identifiers would be doggy:allow-bark and doggy:deny-bark. Your capability file would include:
"permissions": ["doggy:allow-bark"]
If the plugin has a default set, you can inspect which commands it contains by looking at the plugin’s permissions/default.toml or the rendered documentation. This transparency lets you decide whether to use the default or hand-pick individual commands.
Missing permission means broken frontend calls:
If you forget to add a required command permission, the frontend will receive a “not allowed” error and the command will silently fail. There is no fallback. Always test every plugin API call after changing capabilities.
Window Permissions
A capability is not global to the entire application. It is assigned to specific windows through the windows field. Only windows whose labels appear in that list inherit the capability’s permissions.
This is the mechanism for a principle-of-least-privilege architecture: the main window gets filesystem access, a settings window gets only basic core APIs, and a preview window gets nothing beyond what is strictly needed to display content.
Create multiple capability files for different window roles:
{
"identifier": "main-capability",
"windows": ["main"],
"permissions": ["core:default", "fs:default"]
}
{
"identifier": "settings-capability",
"windows": ["settings"],
"permissions": ["core:default"]
}
A wildcard "windows": ["*"] applies the capability to all windows, including any created later at runtime. Use it sparingly—it removes the isolation that window-scoped permissions provide.
Configuring Plugin Permissions Step by Step
The best way to internalize permission configuration is to walk through a realistic scenario. You will add the official filesystem plugin to a Tauri app, allow writing a text file, and scope that access to a single specific path.
Add the filesystem plugin to the project
Install the Rust crate and the JavaScript bindings. From the project root, run:
pnpm tauri add fs
This command updates Cargo.toml, registers the plugin in the Tauri builder, and adds the npm package @tauri-apps/plugin-fs to your frontend dependencies. If you prefer manual setup, you would run cargo add tauri-plugin-fs and then initialize it in lib.rs.
Initialize the plugin in the Rust backend
Open src-tauri/src/lib.rs and confirm the plugin is registered. The tauri add command does this automatically, but it’s worth verifying:
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Without this line, the plugin’s Rust code is never loaded—permissions are meaningless because the commands don’t exist.
Create a capability file with scoped write access
Add (or modify) the capability file for the main window. You want the filesystem plugin’s default read access, plus the ability to write a single text file in the user’s home directory.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-write-text-file",
{
"identifier": "fs:allow-write-text-file",
"allow": [{ "path": "$HOME/tauri-notes.txt" }]
}
]
}
The fs:default permission already includes read commands scoped to the app’s data directory. The two additional entries enable the write_text_file command and restrict its reach to exactly $HOME/tauri-notes.txt.
Write the frontend code that invokes the command
In your React component, import writeTextFile and BaseDirectory from the plugin’s JavaScript bindings, then call the function with the target path.
import { useState } from "react";
import { writeTextFile, BaseDirectory } from "@tauri-apps/plugin-fs";
function App() {
const [note, setNote] = useState("");
async function saveNote() {
await writeTextFile("tauri-notes.txt", note, {
baseDir: BaseDirectory.Home,
});
setNote("");
}
return (
<div>
<textarea
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Write your note..."
/>
<button onClick={saveNote}>Save to $HOME/tauri-notes.txt</button>
</div>
);
}
export default App;
The baseDir: BaseDirectory.Home maps to $HOME on Unix and macOS, or the user’s profile directory on Windows. Combined with the scoped permission, any attempt to write outside $HOME/tauri-notes.txt will be rejected.
Verification:
Run pnpm tauri dev, type a message, and click Save. Then check the file in your home directory:
cat $HOME/tauri-notes.txt
If you see your text, the permission configuration is working correctly.
Common Mistakes and How to Avoid Them
Plugin permissions are strict by design, but that strictness makes configuration errors both common and silent. Here are the mistakes that trip up most developers.
Forgetting to add the plugin’s default permission. Without it, even basic read commands are blocked. The frontend will throw errors like fs.read_text_file not allowed. Always start with the default set, then layer on additional commands or deny rules.
Mixing up the permission identifier format. Identifiers are case-sensitive and must match exactly what the plugin defines. fs:allow-write-text-file is correct; fs:allow-writetextfile will be silently ignored—or worse, cause a build error.
Adding a scoped object without the plain string entry. The scope object with identifier and allow filters an existing allow rule. If you omit the string "fs:allow-write-text-file" from the permissions array, the command remains blocked entirely.
Assuming all windows get the same permissions. Capability files target specific windows by label. If you create a new window at runtime with a label not listed in any capability file, that window will have no permissions at all—not even the ability to set its own title.
Deny rules are permanent for that capability:
If a capability file includes a deny-* permission for a command, no later allow rule in the same capability can override it. Deny always wins. Plan your capability files so that you don’t inadvertently lock yourself out of a command you need later.
Summary
Plugin permissions are the mechanism that enforces the principle of least privilege in a Tauri v2 application. They are not optional boilerplate—they are the gate that decides whether a frontend call reaches system resources.
The pattern to remember: discover what commands a plugin exposes, start with its default permission set, then adjust by adding specific allow rules, removing unwanted commands with deny rules, and narrowing resource access with scopes. Assign each capability only to the windows that need it.
This configuration lives entirely in your capability files, not in Rust code. That separation keeps security rules easy to audit and change without recompiling the backend.