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:
- For each name in
COMMANDS, it writespermissions/autogenerated/commands/<cmd>.tomlcontaining anallow-<cmd>anddeny-<cmd>permission. - It writes
permissions/autogenerated/reference.md— a human-readable summary of every permission your plugin exposes. - It collates all
.tomlunderpermissions/(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.
- 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,
}
- Add
schemarsto both dependency tables:
[dependencies]
schemars = "0.8"
[build-dependencies]
schemars = "0.8"
- 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 foobut forgot to add"foo"toCOMMANDSinbuild.rs. The autogeneratedallow-foo.tomlis never written, so consumers can’t grant it. - Renaming a command silently breaks consumers: changing
start_server→startinvalidates every capability that referencedallow-start-server. Treat theCOMMANDSlist as a semver-relevant API. - Default set too permissive: if
defaultallows a destructive command, consumers who add your plugin “to try it” get the dangerous bit by accident. Biasdefaulttoward read-only / status commands. - Scope schema not regenerating:
build.rsneedscargo:rerun-if-changed=src/scope.rsif you want edits to the struct to invalidate the build.tauri_plugin::Builder::new(...).build()doesn’t add that automatically when you useglobal_scope_schema.
Templates
templates/build.rs— minimal build script withCOMMANDSlist 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 multipleallow-*entries.templates/scope-schema.rs—src/scope.rswithJsonSchemaderived.