Permission Configuration

How to define granular permissions for Tauri v2 commands, control API access, and set scopes for plugins and custom Rust commands

Permissions are the individual building blocks of Tauri’s security model. A capability file decides which windows get access to what; the permission file defines exactly what access that is — down to specific commands and, for some plugins, the data those commands can touch.

In this section we focus on writing and structuring those permission files themselves. If you’re looking for how permissions are wired to windows, that’s covered in Configuring Capability Files.

Where Permission Files Live

Application-level permissions sit inside src-tauri/permissions/. Each file can be JSON (.json) or TOML (.toml). TOML is more readable for hand-written configuration and is the recommended format. Capability files that consume these permissions live in src-tauri/capabilities/.

src-tauri/
├── capabilities/
│   └── default.json
├── permissions/
│   └── home-read.toml
├── src/
├── tauri.conf.json

Plugin permissions are distributed inside the plugin’s own permissions/ directory, but as an application developer you only need to reference them by identifier — you don’t copy them into your project.

TOML or JSON?:

TOML files end with .toml, JSON with .json. Both are supported everywhere a permission file is accepted. The official examples lean toward TOML for clarity.

Anatomy of a Permission

A minimal permission has an identifier and a description. Beyond that, you use arrays like commands.allow and scope.allow to control what’s permitted.

Here’s a permission that allows reading files inside $HOME:

src-tauri/permissions/home-read.toml
[[permission]]
identifier = "home-read"
description = "Allows reading any file inside $HOME, but not subdirectories."
[[scope.allow]]
path = "$HOME/*"

Every [[permission]] block defines one discrete grant. A single file can contain multiple [[permission]] blocks if they logically belong together.

Identifier Rules

The identifier must be unique across all permissions used by your application, including those from plugins. Tauri enforces these constraints:

  • Only lowercase ASCII letters [a-z], hyphens -, and the separator : are allowed.
  • A colon separates a namespace from the permission name, like fs:read-files.
  • When referencing a plugin permission, the tauri-plugin- prefix is added automatically. You write fs:read-files, not tauri-plugin-fs:read-files.
  • Maximum total length is approximately 116 characters due to internal naming limits.

The special suffix :default marks a permission as the default for a plugin or your own app. Plugin defaults are applied automatically when the plugin is installed via the Tauri CLI.

Plugin Permissions

Plugins ship pre-written permissions that cover their commands. The File System plugin, for instance, provides:

  • fs:read-files – enables all read-related commands (like read_file, read, open, read_text_file, etc.)
  • fs:write-files – enables all write-related commands
  • fs:allow-mkdir – enables just the mkdir command
  • fs:scope-home – pre-defines a scope for the $HOME directory

You never need to write these yourself. You reference them in a capability file, or extend them in your own permission files.

As an application developer, you create custom permissions for two reasons:

  1. To define scopes that map to your own Rust commands.
  2. To combine plugin permissions into a set that fits your application’s needs.

Allowing Commands

The commands.allow array lists every Tauri command the permission unlocks. These are the snake_case function names exposed by a plugin or your own Rust backend.

The File System plugin’s read-files permission looks like this internally:

[[permission]]
identifier = "read-files"
description = "Enables all file read commands without any pre-configured accessible paths."
commands.allow = [
  "read_file",
  "read",
  "open",
  "read_text_file",
  "read_text_file_lines",
  "read_text_file_lines_next",
]

When you reference fs:read-files in a capability, all six commands become callable from the frontend. If you want only read_text_file, you would reference fs:allow-read-text-file instead — each command typically has its own granular allow-<command> permission.

For your own Rust commands, you must explicitly allow them. Suppose you register a command called generate_report:

src-tauri/src/lib.rs
#[tauri::command]
fn generate_report(name: String) -> String {
    format!("Report for {}", name)
}
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![generate_report])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

That command won’t be callable from the frontend until you create a permission that includes it:

src-tauri/permissions/report.toml
[[permission]]
identifier = "allow-generate-report"
description = "Enables the generate_report command."
commands.allow = ["generate_report"]

Then reference allow-generate-report in a capability file.

Commands are deny-by-default:

In Tauri v2, a command is invisible to the frontend until a permission explicitly allows it. If you call invoke("generate_report") without the right permission, you’ll get a permission denied error — not a silent failure.

Restricting Commands

Permissions can also deny commands. The commands.deny array removes access that was granted elsewhere. This matters when you combine permissions in a set and need to carve out an exception.

Imagine a permission set that grants broad file system access, but you want to forbid the remove command. You’d write a permission that denies it, then include both in the set:

src-tauri/permissions/fs-no-delete.toml
[[permission]]
identifier = "deny-remove"
description = "Denies the file remove command."
commands.deny = ["remove"]

Deny entries take precedence over allow entries when the same command appears in both. This lets you build broad grants and then explicitly narrow them.

Permission Scopes

Some commands require more than a binary allow/deny — they need to know which data the command can access. The file system plugin is the clearest example: even after allowing read_file, you must also define which paths are readable. That’s what scopes do.

Scopes are defined with [[scope.allow]] and [[scope.deny]] blocks under a permission. Each block declares key-value pairs that constrain a command’s arguments:

src-tauri/permissions/home-docs-read.toml
[[permission]]
identifier = "home-docs-read"
description = "Read access to the Documents folder only."
commands.allow = ["read_text_file"]
[[scope.allow]]
path = "$HOME/Documents/**"
[[scope.deny]]
path = "$HOME/Documents/secrets/**"

The path key corresponds to a parameter the command expects. The plugin’s Rust implementation checks incoming paths against the allowed patterns. A path that doesn’t match any scope.allow entry — or matches a scope.deny entry — causes the command to fail with a scope error.

Scopes are enforced inside the command:

Tauri’s permission system only knows about the scope configuration. The actual check happens inside the Rust command implementation using tauri::Scope. If you write your own command that takes a path argument but never validates it against the scope, the permission scope does nothing. Always call scope.check(&path) in custom commands that handle user-supplied paths.

Scope Patterns

The path scope supports glob-style patterns:

  • $HOME/* — any direct child of the home directory, non-recursive.
  • $HOME/** — everything inside home, recursively.
  • **/* — the entire filesystem (use with extreme caution).
  • $HOME/Documents/** — everything inside the Documents folder and all subdirectories.

System variables like $HOME, $APPDATA, $CACHE, and other Tauri base directory variables are expanded at runtime.

Global Scope vs. Permission Scope

Scopes can also be defined at the plugin or app level independently of a specific permission identifier — these are global scopes. They are referenced separately. However, the most common pattern is to bundle the scope directly inside the permission, as shown above, so everything the command needs is in one place.

Grouping Permissions into Sets

A permission set combines multiple permissions under a single identifier. This is how you create re-usable bundles.

src-tauri/permissions/home-extended.toml
[[set]]
identifier = "allow-home-read-extended"
description = "Read and create directories in $HOME, non-recursively."
permissions = [
  "fs:read-files",
  "fs:scope-home",
  "fs:allow-mkdir",
]

Now allow-home-read-extended can be used in a capability file exactly like any single permission. The set automatically resolves to the three underlying permissions.

Sets reduce capability bloat:

Without sets, a capability file that grants read, write, and dialog access might list fifteen individual permission strings. A set collapses that into one meaningful name, making the capability file readable at a glance.

Practical Example: Reading Files from the Frontend

Let’s walk through a complete end-to-end configuration. The goal: a React component that lets the user click a button to read and display the contents of $HOME/notes.txt.

1. Rust Backend (the command already exists via the plugin)

The File System plugin registers the read_text_file command. No custom Rust code is needed beyond adding the plugin.

2. Create the Permission

src-tauri/permissions/home-notes-read.toml
[[permission]]
identifier = "home-notes-read"
description = "Allows reading text files directly inside $HOME."
commands.allow = ["read_text_file"]
[[scope.allow]]
path = "$HOME/*"

3. Reference the Permission in a Capability

src-tauri/capabilities/default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-capability",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "home-notes-read"
  ]
}

4. Call It from React

src/App.tsx
import { useState } from "react";
import { readTextFile } from "@tauri-apps/plugin-fs";
function App() {
  const [content, setContent] = useState("");
  const loadNotes = async () => {
    const text = await readTextFile("notes.txt", { baseDir: "home" });
    setContent(text);
  };
  return (
    <div>
      <button onClick={loadNotes}>Load Notes</button>
      <pre>{content}</pre>
    </div>
  );
}
export default App;

When the user clicks the button, the plugin resolves notes.txt inside $HOME and returns its contents. If notes.txt is actually $HOME/subdir/notes.txt, the call fails because the scope only permits $HOME/* — not recursive subdirectories. That’s the scope constraint working exactly as configured.

The wildcard `**/*` is a common footgun:

Setting path = "**/*" grants access to the entire filesystem. It’s the quickest way to get rid of scope errors during development, but it eliminates the entire point of the security model. Use the narrowest scope that satisfies your application’s actual needs.

Common Mistakes

Forgetting to add the permission to a capability. Writing a permission file is only half the job. Until a capability references that permission and assigns it to a window, the frontend sees nothing.

Mixing up scope and command allow. A scope entry like path = "$HOME/*" does not implicitly allow any command. You still need commands.allow = ["read_text_file"]. They are independent; the scope just constrains the command’s arguments once the command itself is permitted.

Assuming scopes work across all plugins uniformly. Each plugin defines its own scope keys. The file system plugin uses path. A hypothetical database plugin might use table or query. Read the plugin’s documentation to know which keys to use.

Using the wrong identifier format. Plugin permission identifiers must include the plugin prefix when referenced in capability files, but that prefix is the short form — fs:scope-home, not tauri-plugin-fs:scope-home. For your own permissions, just use the exact identifier from the permission file.

Duplicate identifiers. If two permission files define the same identifier, Tauri will error at build time. Identifiers are global, so name them carefully.

Summary

Permission configuration in Tauri v2 is about writing small, focused TOML (or JSON) files that declare which commands are available and what data those commands can access. Plugin authors provide pre-built permissions; application developers extend them and create their own.

The three levels of control — command allow/deny, scoped access, and permission sets — give you precise control over what your frontend can do. The most common real-world pattern is a permission that bundles one or two command allows with a tightly scoped path, then gets pulled into a capability that applies to the main window.