← Catalog

tauri-plugin-dev-permissions-manifest

Use when wiring the `permissions/` directory and autogenerated manifest of a Tauri v2 plugin — writing `build.rs` with `tauri_plugin::Builder::new(COMMANDS).build()`, the per-command `permissions/autogenerated/commands/*.toml` files, hand-written `permissions/default.toml` for the `plugin:default` set, permission sets that compose multiple `allow-*` entries, platform-specific manifest entries, and a `schemars::JsonSchema` scope struct so capability files get IDE autocomplete.

Plugin Permissions and the Autogenerated Manifest

Every command in a Tauri plugin is denied by default. The frontend can only call plugin:<name>|<cmd> when a capability file enables a matching permission. That permission has to come from somewhere — it ships with the plugin crate as a TOML manifest. This skill covers the wiring that produces and publishes that manifest.

Builds on tauri-plugin-dev (scaffolding) and pairs with tauri-security-capabilities-authoring (how consumers reference your permissions).

The build script does the work

In a plugin crate, build.rs runs tauri_plugin::Builder::new(COMMANDS).build(). That call has three effects:

  1. For each name in COMMANDS, it writes permissions/autogenerated/commands/<cmd>.toml containing an allow-<cmd> and deny-<cmd> permission.
  2. It writes permissions/autogenerated/reference.md — a human-readable summary of every permission your plugin exposes.
  3. It collates all .toml under permissions/ (autogenerated and hand-written) into the manifest that ships inside the published crate. Consumers see those identifiers when they edit a capability file.
// build.rs
const COMMANDS: &[&str] = &["start_server", "stop_server", "ping"];

fn main() {
    tauri_plugin::Builder::new(COMMANDS).build();
}

Names must match the snake_case Rust function name in commands.rs exactly. If you rename start_server to start, change COMMANDS too — otherwise the frontend gets “command not allowed” even after granting allow-start-server.

tauri-plugin is a build-dependency, not a runtime dependency:

[build-dependencies]
tauri-plugin = { version = "2", features = ["build"] }

Layout

permissions/
├── autogenerated/
│   ├── commands/
│   │   ├── start_server.toml   # allow-start-server + deny-start-server
│   │   ├── stop_server.toml
│   │   └── ping.toml
│   └── reference.md            # docs, regenerated every build
├── default.toml                # hand-written: the plugin:default set
├── allow-server-control.toml   # hand-written: a permission set
└── scope-allowed-hosts.toml    # hand-written: a global scope permission

The autogenerated/ directory is regenerated on every build — never edit it. Hand-written files live as siblings.

The default permission set

<plugin>:default is what consumers get when they put your plugin in a capability without thinking. Make it the minimum-useful subset — typically the one or two commands most users will need, plus nothing dangerous.

# permissions/default.toml
"$schema" = "schemas/schema.json"

[default]
description = "Allows pinging the server. Server start/stop must be granted explicitly."
permissions = ["allow-ping"]

If you want the default to grant everything (rare; usually a smell), reference the per-command allow permissions:

[default]
permissions = ["allow-start-server", "allow-stop-server", "allow-ping"]

The tauri CLI adds <plugin>:default to a consumer’s default capability automatically when they run cli plugin add. That makes the default permission a public API surface — widening it later is fine, narrowing it is a breaking change.

Permission sets — composing existing permissions

A [[set]] bundles multiple permissions under one identifier. Use these when several commands implement one logical feature.

# permissions/allow-server-control.toml
"$schema" = "schemas/schema.json"

[[set]]
identifier = "allow-server-control"
description = "Full lifecycle control over the embedded server."
permissions = ["allow-start-server", "allow-stop-server"]

A capability that lists my-plugin:allow-server-control ends up with both autogenerated allow permissions.

Platform-specific permissions

A permission entry can carry a platforms array. The Tauri ACL resolver drops the permission on platforms outside the list, so e.g. an Android-only command can ship a permission that simply doesn’t exist on Windows builds:

"$schema" = "schemas/schema.json"

[[permission]]
identifier = "allow-request-notification-permission"
description = "Prompt the user for OS-level notification permission."
commands.allow = ["request_notification_permission"]
platforms = ["android", "iOS"]

Valid values: linux, windows, macOS, android, iOS.

Scope schema with schemars

Plugins that expose scoped permissions (file paths, host allowlists, binary names) should ship a JSON Schema so capability authors get autocomplete and validation in their IDE.

  1. Define the scope struct in a module shared between the crate and the build script (so both can see the type):
// src/scope.rs
use serde::{Deserialize, Serialize};
use schemars::JsonSchema;

#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
pub struct Entry {
    /// Hostname (no scheme, no path). Wildcards: `*.example.com`.
    pub host: String,
}
  1. Add schemars to both dependency tables:
[dependencies]
schemars = "0.8"

[build-dependencies]
schemars = "0.8"
  1. Wire the schema into build.rs:
#[path = "src/scope.rs"]
mod scope;

const COMMANDS: &[&str] = &["fetch"];

fn main() {
    tauri_plugin::Builder::new(COMMANDS)
        .global_scope_schema(schemars::schema_for!(scope::Entry))
        .build();
}

The generated schema lands in the published crate. When a capability writes "scope": [{ "host": "..." }] against your plugin, the IDE knows the field name and that it’s a string.

At runtime, read the scope with tauri::ipc::GlobalScope<'_, Entry> (or CommandScope<'_, Entry> for per-command scopes) inside the command — see tauri-security-scopes for the consumer-side rules and tauri-plugin-dev for the command-handler signature.

Publishing

The manifest is part of the crate, not a separate artifact. cargo publish includes permissions/ automatically because build.rs re-emits the autogenerated files at build time on the consumer’s machine — they don’t have to be in the published tarball if build = "build.rs" is set in Cargo.toml and tauri-plugin is in [build-dependencies]. In practice, commit permissions/autogenerated/ to git anyway so the manifest is auditable on GitHub without a build.

Common traps

  • “command not allowed” after publishing a new command: you added #[tauri::command] fn foo but forgot to add "foo" to COMMANDS in build.rs. The autogenerated allow-foo.toml is never written, so consumers can’t grant it.
  • Renaming a command silently breaks consumers: changing start_serverstart invalidates every capability that referenced allow-start-server. Treat the COMMANDS list as a semver-relevant API.
  • Default set too permissive: if default allows a destructive command, consumers who add your plugin “to try it” get the dangerous bit by accident. Bias default toward read-only / status commands.
  • Scope schema not regenerating: build.rs needs cargo:rerun-if-changed=src/scope.rs if you want edits to the struct to invalidate the build. tauri_plugin::Builder::new(...).build() doesn’t add that automatically when you use global_scope_schema.

Templates

  • templates/build.rs — minimal build script with COMMANDS list and optional scope schema wiring.
  • templates/permissions/default.toml — the [default] set.
  • templates/permissions/autogenerated/commands/start_server.toml — sample autogenerated output (for reference; don’t hand-edit in real plugins).
  • templates/permissions/allow-server-control.toml — a permission set composing multiple allow-* entries.
  • templates/scope-schema.rssrc/scope.rs with JsonSchema derived.