Capabilities Directory

Understand the capabilities directory in Tauri v2 projects and how to control which Rust commands your frontend can call

Every Tauri v2 project includes a src-tauri/capabilities/ directory. It holds capability files that define what your frontend code is allowed to do — which commands it can invoke and under what conditions. Without a matching capability, a webview cannot reach any Tauri IPC layer at all.

Not a general configuration folder:

Capability files are not generic config files. They serve a single, security-critical purpose: granting or denying access to Tauri commands on a per‑window or per‑webview basis.

What a Capability Represents

A capability is a grouping mechanism. It says: “for these specific windows (or webviews), allow these specific permissions.” Permissions are the fine‑grained rules that unlock individual commands — for example, the ability to read a file from the user’s home directory or to show a native dialog.

When your app starts, Tauri checks every window and webview against all active capabilities. If a window label matches a capability’s windows list (or a webview label matches webviews), that window/webview receives the permissions listed inside that capability. If nothing matches, the frontend gets no IPC access — every invoke call will fail silently.

This is a deliberate security boundary. The capabilities system exists so that a vulnerability in one part of your frontend (say, a third‑party widget) cannot easily escalate into full system access.

How Capabilities and Permissions Connect

Capabilities reference permissions by identifier. Those permissions come from:

  • Core plugins that ship with Tauri (like core:window, core:event, core:path)
  • Official or community plugins (like dialog, fs, shell)
  • Your own Rust commands that you register in the app

Permissions are defined inside each plugin’s permissions directory, or in your own src-tauri/permissions/ folder for custom commands. A capability file only points to those identifiers; it does not redefine the commands themselves.

This separation means plugin authors pre‑define sensible permission sets, and you, as the application developer, choose which of them to activate for which windows.

The Default Capability File

When you create a new Tauri v2 project, the capabilities folder already contains a default.json file similar to this:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-capability",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:path:default",
    "core:event:default",
    "core:window:default",
    "core:app:default",
    "core:resources:default",
    "core:menu:default",
    "core:tray:default",
    "core:window:allow-set-title"
  ]
}

The file grants the main window access to the default permission sets of several core plugins, plus the specific allow-set-title command. Without this file, the main window would not even be able to change its own title from the frontend.

Everything is working:

If you can call window.setTitle("Hello") from your React code and see the title bar update, your default.json capability is active and the core:window:default permission set is working.

Capability File Structure

Every capability file — whether JSON or TOML — follows the same schema. The mandatory properties are identifier and permissions. Everything else is optional but often essential for real‑world apps.

The identifier Property

A unique string that names the capability. You use this identifier if you want to reference the capability in tauri.conf.json (see Selectively Enabling Capabilities). Keep it short and descriptive — main-capability, admin-window-access, remote-analytics.

The permissions Array

A list of permission entries. Each entry can be:

  • A simple string identifier like "core:window:default" (references a permission set)
  • An object that references a permission and extends its scope — you need this whenever a permission has configurable allowed values

The following example shows a simple string entry alongside a scope‑extending object:

{
  "identifier": "file-access",
  "description": "Allow the main window to read from the user's Documents folder",
  "windows": ["main"],
  "permissions": [
    "fs:default",
    {
      "identifier": "fs:allow-read-text-file",
      "allow": [{ "path": "$HOME/Documents/**" }]
    }
  ]
}

The allow array here defines which file paths the read_text_file command may touch. Without this scope extension, the fs:allow-read-text-file permission would be present but would have no allowed paths, making the command unusable.

Mixing wildcard and explicit scopes:

A scope like "path": "**/*" grants access to the entire filesystem. Use it only during development or for tools that genuinely need it. Prefer narrow scopes (e.g. $HOME/Documents/**) in production.

The windows and webviews Properties

Both take arrays of strings. They control which application windows and webviews receive the capability. You can use exact labels ("main") or glob patterns ("admin-*").

  • windows: Applies the capability to all webviews inside matching windows.
  • webviews: Applies the capability only to specific webviews whose label matches, regardless of the parent window.

For multi‑webview windows, prefer specifying webviews for fine‑grained control.

The platforms Property

An array of target operating systems: "linux", "macOS", "windows", "iOS", "android". If omitted, the capability applies everywhere.

This is useful when a plugin or command only exists on certain platforms. For example, the global-shortcut plugin is desktop‑only. A capability that enables it should set "platforms": ["linux", "macOS", "windows"] so that it is silently ignored on mobile builds.

The remote Property

By default, Tauri’s API is only accessible from content loaded from the local app bundle (the tauri://localhost scheme). The remote object lets you allow specific external URLs to use the capability’s permissions.

{
  "identifier": "remote-analytics",
  "description": "Allow a remote analytics dashboard to emit events",
  "windows": ["main"],
  "remote": {
    "urls": ["https://*.myanalytics.example.com"]
  },
  "permissions": ["core:event:default"]
}

The urls field uses the URLPattern standard. "https://*.myanalytics.example.com" covers all subdomains; "https://myanalytics.example.com/api/*" covers any path under /api/.

Never open to arbitrary origins:

Avoid patterns like "*" or "https://*" that would give any website access to Tauri’s IPC. Restrict remote access to domains you control and only to the permissions absolutely required.

The local Property

A boolean that defaults to true. When true, the capability applies to local app URLs. Set it to false if you intend the capability to be exclusively for remote URLs. This is rarely changed.

The description Property

A human‑readable string explaining the capability’s purpose. It does not affect behaviour but is valuable for anyone reading the configuration later. Write it as if you were explaining the capability to a teammate who just joined the project.

Defining Capabilities: Inline vs. Dedicated Files

Tauri gives you two ways to organise capabilities, and you can mix both.

Place .json or .toml files inside src-tauri/capabilities/. By default, all capability files in this directory are automatically enabled. You do not need to list them anywhere else.

This is the preferred approach because it keeps permissions isolated in self‑contained files, making audits easier.

Inline in tauri.conf.json

You can also define capabilities directly inside the tauri.conf.json under app.security.capabilities. You can mix inline capabilities with references to files by identifier:

{
  "app": {
    "security": {
      "capabilities": [
        {
          "identifier": "my-inline-capability",
          "description": "An inline capability for all windows",
          "windows": ["*"],
          "permissions": ["fs:default"]
        },
        "main-capability"
      ]
    }
  }
}

Selective enabling disables auto‑detection:

Once you set app.security.capabilities in tauri.conf.json, only the capabilities listed there are active. Any files in the capabilities/ directory that are not referenced will be ignored. If you want to keep the automatic behaviour, do not add the capabilities array to your config at all.

Using TOML for Capability Files

While JSON is the default, you can write capability files in TOML. The structure is identical, just a different syntax. TOML can be easier to read when you have many scope entries.

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "fs-access",
  "description": "Read and write access to the app's data directory",
  "windows": ["main"],
  "permissions": [
    {
      "identifier": "fs:allow-read-text-file",
      "allow": [{ "path": "$APPDATA/**" }]
    },
    {
      "identifier": "fs:allow-write-text-file",
      "allow": [{ "path": "$APPDATA/**" }]
    }
  ]
}

In TOML, the capability is defined under a [capability] table, and each permission entry is a [[capability.permissions]] array of tables. The semantics are exactly the same.

Creating a Custom Permission for Your Own Commands

Capabilities are not limited to plugin permissions. You can define permissions for your own Rust commands and then reference them in a capability.

1

Step 1: Register your command in Rust

Define a Tauri command and register it with the app:

#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
2

Step 2: Create a permission file for the command

Write a TOML permission file that enables the command:

[[permission]]
identifier = "allow-greet"
description = "Allows the greet command to be called from the frontend"
commands.allow = ["greet"]
3

Step 3: Reference the permission in a capability

In your capability file, add the new permission identifier (no plugin prefix needed for app‑own commands):

{
  "identifier": "main-capability",
  "windows": ["main"],
  "permissions": [
    "core:window:default",
    "allow-greet"
  ]
}
4

Step 4: Call the command from the frontend

Now the frontend can invoke the greet command:

import { invoke } from "@tauri-apps/api/core";
async function greetUser() {
  const greeting = await invoke("greet", { name: "Aria" });
  console.log(greeting);
}

This workflow shows the full chain: Rust command → permission file → capability → frontend invocation. The capability acts as the gate that connects the permission to a specific window.

Schema Support for Autocompletion

When you build your Tauri project, the build script (tauri_build) generates JSON schemas inside src-tauri/gen/schemas/. There are separate schemas for desktop and mobile targets.

By setting the $schema property at the top of your capability file, you get autocompletion and validation in editors that support JSON Schema (VS Code, JetBrains IDEs):

"$schema": "../gen/schemas/desktop-schema.json"

If you are writing a capability for iOS or Android, use ../gen/schemas/mobile-schema.json. The schema ensures you don’t misspell a permission identifier or use an invalid structure.

Schema validation saves time:

Run cargo tauri build (or cargo tauri dev) once to generate the schemas. Then, your editor will underline mistakes before you even compile.

Common Mistakes

Forgetting to Add a Permission for a Plugin

Adding a plugin with cargo tauri add <plugin> usually adds the plugin’s default permissions to your capabilities automatically. But if you manually install a plugin (by editing Cargo.toml), you must also add the corresponding permission identifier to a capability file. Without it, the plugin’s commands are denied.

Using the Wrong Permission Identifier Format

Plugin permissions must be prefixed with the plugin name followed by a colon. For the file system plugin, the identifier is fs:allow-read-text-file, not just allow-read-text-file. For core plugins, it is core:window:default. App‑own permissions have no prefix.

Scope Denial Not Working as Expected

The deny array in a scope extension takes priority over allow. If you allow $HOME/** and deny $HOME/secret, the denied path is blocked. A mistake is to assume that deny scopes are inherited or combined across capabilities — they are not. A deny in one capability does not affect another capability; each capability is self‑contained.

Enabling Too Many Permissions for Every Window

Assigning every capability to "*" (all windows) defeats the security model. If a secondary window only needs to display static content, it should not have file system or shell access. Create separate capability files for different privilege levels.

Summary

The capabilities/ directory is the bridge between your frontend and the Rust backend. It translates high‑level “what this window needs” decisions into precise lists of allowed commands and scopes. Once you understand that capabilities collect permissions and attach them to specific windows, you can design your app’s security boundary with intention rather than treating it as a checkbox to bypass.