# 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.

- Skill: `stuffbucket/tauri-plugin-dev-permissions-manifest` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add stuffbucket/tauri-plugin-dev-permissions-manifest`
- Raw SKILL.md: https://api.skillmd.com/api/skills/stuffbucket/tauri-plugin-dev-permissions-manifest/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: stuffbucket (https://skillmd.com/u/stuffbucket)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/stuffbucket/tauri-plugin-dev-permissions-manifest

---


# 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.

```rust
// 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:

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

## Layout

```text
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.

```toml
# 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:

```toml
[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.

```toml
# 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:

```toml
"$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):

```rust
// 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:

```toml
[dependencies]
schemars = "0.8"

[build-dependencies]
schemars = "0.8"
```

1. Wire the schema into `build.rs`:

```rust
#[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_server` → `start` 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.rs` — `src/scope.rs` with `JsonSchema` derived.

