Configuring Capability Files
Learn how to create, configure, and manage Tauri v2 capability files to control which permissions and commands your frontend can access
A capability file is the bridge between a window in your Tauri app and the permissions it needs. It says: this window, with this label, can use these commands and access these resources. All capability files live under src-tauri/capabilities and are written as JSON or TOML.
Tauri v2 ships with a deny-by-default model for plugins and specific commands. The frontend receives no access to any plugin API until you grant it through a capability. This page covers how to author those capability files, how to wire them into your app, and how to avoid the most common mistakes that cause permission-denied errors at runtime.
Prerequisites:
This document assumes you are already familiar with what capabilities and permissions are in Tauri v2. If you need a refresher, read Understanding Capabilities and Permission Configuration first.
Where Capability Files Live
All capability files must sit inside the src-tauri/capabilities directory. Tauri scans this folder automatically at build time.
tauri-app/
└── src-tauri/
├── capabilities/
│ └── default.json # your capability definitions
├── src/
│ └── main.rs
└── tauri.conf.json
You can name the files however you like — default.json, media.json, admin.toml — but the name does not affect behavior. Only the identifier field inside the file matters.
Automatic Discovery vs Explicit Enablement
By default, every capability file in the directory is active. You do not need to list it anywhere for it to work.
If you explicitly list capabilities in tauri.conf.json under app.security.capabilities, Tauri switches to an explicit mode and only those capabilities are used. Any file not referenced is ignored.
{
"app": {
"security": {
"capabilities": ["main-capability", "media-capability"]
}
}
}
All-or-nothing switch:
As soon as you write app.security.capabilities as an array, every capability file that is not in that list becomes inactive. If you later add a new file and forget to add its identifier to the array, the window will lose those permissions silently. Start without the array, let auto-discovery work during development, and only lock down the list when you are ready.
Creating a New Capability File
The following steps walk through adding a capability that grants the main window access to the dialog plugin and the window.setTitle command. Every new capability follows the same sequence.
Step 1: Create the file in src-tauri/capabilities
Pick a descriptive filename like main.json. The extension tells Tauri whether the content is JSON or TOML.
mkdir -p src-tauri/capabilities
touch src-tauri/capabilities/main.json
Step 2: Define the identifier and window targets
Every capability needs a unique identifier. The windows array contains the labels of the webview windows that receive these permissions. Use "*" to match all windows.
{
"identifier": "main-capability",
"description": "Permissions for the primary application window",
"windows": ["main"]
}
Step 3: Add permissions
Permissions are strings that map to pre-defined privilege sets. Core permissions use the core: prefix, plugin permissions use the plugin name as a prefix (e.g., dialog:).
{
"identifier": "main-capability",
"description": "Permissions for the primary application window",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"core:window:allow-set-title"
]
}
Step 4: (Optional) Reference in tauri.conf.json
If you later decide to switch to explicit capability listing, add the identifier to your tauri.conf.json.
{
"app": {
"security": {
"capabilities": ["main-capability"]
}
}
}
Until then, the file is auto-discovered and works immediately.
Verifying the setup works:
After adding the capability, call a dialog from your React frontend. If dialog:default is missing, the call will throw a permission error. With the capability above, a simple import { open } from '@tauri-apps/plugin-dialog'; await open(); succeeds.
Choosing Between JSON and TOML
Tauri accepts capability files in either format. The choice is purely about which syntax you prefer to write and read. The data structure is identical.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Permissions for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"core:window:allow-set-title"
]
}
The $schema field is optional but strongly recommended for JSON files — it enables autocompletion and validation in editors that support JSON Schema. You can reference the desktop-specific or mobile-specific schema depending on your target.
Adding Permissions to a Capability
The permissions array contains a list of permission identifiers. Each identifier follows one of these patterns:
core:default— the default set of core Tauri commandscore:<command>— an individual core command, e.g.,core:window:allow-set-title<plugin>:default— the default set for a plugin, e.g.,dialog:default<plugin>:allow-<command>— a single plugin command, e.g.,fs:allow-read-file- a custom identifier you define in
src-tauri/permissions/
The capability file only references permissions. The actual definition of what a permission includes (which commands it enables, which scopes it grants) lives in the permission files themselves. For plugin permissions, those definitions ship with the plugin crate.
Adding Default Plugin Permissions
Most plugins expose a default permission that bundles their most common commands. To enable a plugin's standard functionality, add plugin-name:default:
{
"identifier": "main-capability",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"shell:default",
"notification:default"
]
}
Missing plugin permission causes silent failure:
If you register a plugin in your Rust code via .plugin() but do not add its permission to any capability, the frontend calls to that plugin's API will throw a permission error at runtime. There is no compile-time warning. Always add the matching permission when you add a plugin.
Adding Individual Command Permissions
Instead of granting an entire default set, you can allow a single command. This is useful when you need a specific operation from a plugin but want to limit what the frontend can do.
{
"identifier": "window-control-capability",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-set-title",
"core:window:allow-minimize",
"core:window:allow-close"
]
}
Here the window can change its title, minimize, and close, but cannot maximize, unminimize, or toggle fullscreen because those individual permissions are not listed.
Allowing Custom Commands
Commands you register in your Rust backend with tauri::Builder::invoke_handler are, by default, accessible to all windows. This is because core:default includes the IPC invocation path for commands registered via generate_handler!.
If you want to restrict which custom commands are callable, you need to:
- Restrict the command list at build time with
AppManifest::commands. - Define a permission that explicitly allows your command.
- Reference that permission in a capability.
Restricting Custom Commands
In build.rs, limit which commands are exposed:
fn main() {
tauri_build::try_build(
tauri_build::Attributes::new()
.app_manifest(
tauri_build::AppManifest::new()
.commands(&["read_user_data", "save_user_data"])
),
)
.unwrap();
}
Without this, all commands registered via generate_handler! would be callable even if you don't explicitly grant them — because core:default carries a blanket allowance. After adding the restriction above, a command not listed in commands is completely unreachable from the frontend, and commands that are listed still need a permission grant.
Granting Access to a Custom Command
Create a permission file that enables your command:
[[permission]]
identifier = "allow-read-user-data"
description = "Enables the read_user_data command"
commands.allow = ["read_user_data"]
Then reference that permission in a capability:
{
"identifier": "main-capability",
"windows": ["main"],
"permissions": [
"core:default",
"allow-read-user-data"
]
}
Now the frontend can call read_user_data but no other custom commands unless they are similarly permitted.
custom permissions require the commands to be listed in build.rs:
Defining a permission with commands.allow does not override the restriction set by AppManifest::commands. If the command is not in the manifest's command list, the permission has no effect. Both layers must agree.
The React side demonstrates that the command is reachable:
import { invoke } from "@tauri-apps/api/core";
function App() {
const fetchData = async () => {
const data = await invoke("read_user_data");
console.log(data);
};
return <button onClick={fetchData}>Load User Data</button>;
}
export default App;
With the capability in place, clicking the button succeeds. Without it, invoke throws a PermissionDenied error.
Configuring File System Scopes
Some permissions, like those in the fs plugin, allow you to narrow down which paths the frontend can access. You add a scope directly inside the capability file's permissions array as an object with an identifier and allow/deny arrays.
{
"identifier": "main-capability",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "fs:scope",
"allow": [{ "path": "$HOME/Documents/**" }],
"deny": [{ "path": "$HOME/Documents/secret/**" }]
},
"fs:allow-read-file"
]
}
This grants read access to everything under $HOME/Documents, except the secret subdirectory. The ** glob matches any depth.
Using **/* for unrestricted access:
A scope like { "path": "**/*" } lets the frontend read any file the operating system permits. This is sometimes necessary for file-picker-based workflows, but it removes the directory boundary that the capability system exists to enforce. Use it only if your app genuinely needs to read arbitrary user-chosen files.
Enabling Remote API Access
During development, the Vite dev server runs on http://localhost:1420 (or any port). By default, Tauri only allows IPC communication from bundled code loaded from tauri:// URLs. Without explicit configuration, the dev server cannot call Tauri commands.
Add a remote block to the capability to whitelist the dev server origin:
{
"identifier": "main-capability",
"windows": ["main"],
"remote": {
"urls": ["http://localhost:1420"]
},
"permissions": [
"core:default"
]
}
You can use wildcards: http://localhost:*/** matches any port and any path, which is convenient when the port might change.
remote urls in production:
The remote.urls field exists primarily for development. In production, the frontend is served from tauri:// and does not need remote access. Leaving remote.urls open to broad patterns like https://* after shipping is a security risk — it allows any remote page loaded in that window to invoke Tauri commands.
Platform-Specific Capabilities
Some permissions only make sense on certain operating systems. The platforms array restricts a capability to a subset of linux, macOS, windows, iOS, and android.
{
"identifier": "desktop-capability",
"windows": ["main"],
"platforms": ["linux", "macOS", "windows"],
"permissions": [
"core:default",
"global-shortcut:allow-register"
]
}
A mobile counterpart might look like this:
{
"identifier": "mobile-capability",
"windows": ["main"],
"platforms": ["iOS", "android"],
"permissions": [
"core:default",
"barcode-scanner:allow-scan"
]
}
At build time, Tauri includes only the capabilities that match the target platform. A desktop build will ignore mobile.json, and a mobile build will ignore desktop.json. This keeps the binary clean and prevents permission references to plugins that don't even exist on that platform.
Common Mistakes That Break Capabilities
Several errors show up repeatedly when developers configure capabilities for the first time.
Forgetting to add a plugin permission after adding the plugin. The Rust side compiles, the frontend imports the API, and the call fails with a permission error. The fix is to add the corresponding plugin-name:default entry to the capability.
Typoing a permission identifier. Permission strings are case-sensitive and must match exactly what the plugin or core defines. fs:allow-read-file works; fs:allow-readFile does not. Look at the plugin's autogenerated permissions or its documentation.
Removing core:default without understanding what it covers. core:default includes the ability to invoke any command, emit events, and manage basic window operations. Stripping it out breaks nearly everything unless you replace each piece individually.
Mixing explicit and auto-discovered capabilities incorrectly. Once you add the app.security.capabilities array to tauri.conf.json, any file not listed is inactive. If you later add a new capability file and forget to add its identifier to the array, the permissions in that file are silently missing.
Broad file system scopes without a clear need. Scopes like **/* or $HOME/** expose far more of the user's disk than most applications require. Start with the narrowest scope that supports your feature, and expand only if necessary.
A capability file is a security boundary — treat it like one:
Every permission you grant is a path that a compromised frontend could exploit. A capability that allows shell:default on a window that loads remote content from an unverified origin is a serious risk. Always apply the principle of least privilege: give each window only the permissions it absolutely needs.
Summary
Configuring a capability file means answering three questions: which window needs access, which permissions does it need, and which paths or origins should it be allowed to touch. The file format is either JSON or TOML, the directory is src-tauri/capabilities, and the fields — identifier, windows, permissions, remote, platforms — work the same way in both formats.
The most impactful habit is to keep capabilities granular. A separate file for different security contexts (main window vs. admin panel vs. settings window) reduces the blast radius if one window is compromised. Platform-specific files keep mobile-only permissions out of desktop builds and vice versa.