desktop-security-tauri
Tauri 2.x deny-by-default security model, capabilities, permissions, scopes, ACL
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/desktop-security-tauri/skills/desktop-security-tauri
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Tauri Capabilities & ACL
Quick Guide: Tauri 2 uses a deny-by-default security model. Nothing is accessible unless explicitly granted in a capability file (
src-tauri/capabilities/*.json). Capabilities bind permissions to specific windows. Permissions follow theplugin:commandidentifier pattern. Scopes restrict operations to specific paths or URLs with allow/deny lists (deny always wins). Every plugin and custom command needs a permission grant -- missing permissions cause runtime errors, not compile errors.Current version: Tauri 2.x (stable). Tauri 1.x used a boolean allowlist which is completely removed in v2.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST create at least one capability file in src-tauri/capabilities/ -- without it, ALL plugin and core API calls fail at runtime)
(You MUST include core:default in every capability -- without it, basic app lifecycle commands fail)
(You MUST scope permissions to specific windows using the windows array -- a window not listed in any capability has zero IPC access)
(You MUST use deny scopes to restrict sensitive paths -- deny ALWAYS takes precedence over allow)
(You MUST use plugin:permission-name format for plugin permissions and plain permission-name for app commands)
</critical_requirements>
Auto-detection: Tauri capabilities, src-tauri/capabilities, capability file, permissions, ACL, allow-scope, deny-scope, core:default, fs:allow, shell:allow, http:allow, permission set, remote domain, CapabilityRemote, desktop-schema.json, mobile-schema.json, scope allow deny, Tauri security, tauri permission denied, capability identifier
When to use:
- Creating or modifying capability files for a Tauri 2 app
- Granting permissions to specific windows or webviews
- Restricting filesystem, HTTP, or shell access with scoped permissions
- Writing custom permissions for your own Tauri commands
- Grouping permissions into permission sets
- Enabling remote domain access to Tauri APIs
- Debugging "permission denied" or "not allowed" runtime errors
- Migrating from the Tauri v1 allowlist to v2 capabilities
When NOT to use:
- Writing Tauri commands or IPC bridge logic (use desktop-framework-tauri)
- Plugin installation and registration (use desktop-framework-tauri)
- Window management, system tray, or menus (use desktop-framework-tauri)
- General Rust programming unrelated to Tauri ACL
- Frontend framework patterns
Key patterns covered:
- Capability file structure and fields (examples/core.md)
- Window-specific and platform-specific capabilities (examples/core.md)
- Scoped permissions with allow/deny lists (examples/core.md)
- Custom permission definitions in TOML (examples/custom-permissions.md)
- Permission sets for grouping related permissions (examples/custom-permissions.md)
- Remote domain access (examples/core.md)
- Debugging permission errors (examples/core.md)
- Migration from v1 allowlist (reference.md)
Detailed resources:
- examples/core.md - Capability files, scoped permissions, window/platform targeting, remote access, debugging
- examples/custom-permissions.md - Custom permission definitions, permission sets, app-level permissions
- reference.md - Permission identifier patterns, path variables, core permissions, v1 migration checklist
<decision_framework>
Decision Framework
What Permissions Does My App Need?
What does the app do?
|-- Reads/writes files?
| +-- tauri-plugin-fs permissions with path scopes
|-- Makes HTTP requests from Rust backend?
| +-- tauri-plugin-http permissions with URL scopes
|-- Opens file/folder dialogs?
| +-- tauri-plugin-dialog permissions
|-- Runs external processes?
| +-- tauri-plugin-shell permissions (desktop only)
|-- Shows system notifications?
| +-- tauri-plugin-notification permissions
|-- Uses persistent key-value storage?
| +-- tauri-plugin-store permissions
+-- Basic app + window lifecycle only?
+-- core:default is sufficient
How Granular Should Capabilities Be?
How many windows does the app have?
|-- Single window?
| +-- One capability file with all permissions is fine
|-- Multiple windows with SAME needs?
| +-- One capability file listing all windows in the array
+-- Multiple windows with DIFFERENT needs?
+-- Separate capability files per window (principle of least privilege)
Does the app need platform-specific features?
|-- Same features on all platforms?
| +-- Omit the platforms field
+-- Different features per platform?
+-- Separate capability files with platforms field
Where to Put the Scope?
Is the scope for a built-in plugin?
|-- YES -> Inline in the capability file permissions array
| { "identifier": "fs:allow-read-text-file", "allow": [...] }
+-- NO -> Is it for your custom commands?
|-- YES -> Define in src-tauri/permissions/*.toml
+-- NO -> It might not need a scope -- simple allow/deny suffices
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Missing
core:defaultin a capability -- basic app lifecycle commands fail silently - No capability file at all in
src-tauri/capabilities/-- ALL IPC calls fail at runtime - Using v1
tauri.allowlistin config -- completely removed in v2, does nothing - Granting
fs:allow-read-text-filewithout path scope -- allows reading ANY file on the system - Missing
windowsarray in capability -- permissions apply to no windows (useless capability)
Medium Priority Issues:
- Using wildcard
"windows": ["*"]in production -- every window gets these permissions, including dynamic ones - Not splitting capabilities by platform -- desktop-only plugins (shell, autostart, global-shortcut) cause errors on mobile
- Overly broad HTTP scopes (
http:allow-fetchwithout URL scope) -- allows requests to any URL - Granting write permissions when only read is needed -- violates least privilege
Common Mistakes:
- Forgetting that deny always wins -- adding a deny scope and wondering why the allow scope "doesn't work"
- Using OS paths (
/home/user/) instead of Tauri path variables ($HOME/) in scopes - Expecting compile-time errors for missing permissions -- they are runtime errors only
- Using
app.security.capabilitiesintauri.conf.jsonto list some capabilities, then wondering why unlisted capability files are ignored (explicit list overrides auto-discovery) - Capability identifier containing digits after first character (identifiers are restricted to
[a-z]plus hyphens)
Gotchas & Edge Cases:
- Stale ACL in builds: If you add a new permission but the build does not regenerate, the old ACL is embedded. Clean build (
cargo clean) or ensurebuild.rshascargo:rerun-if-changedfor the capabilities directory - Schema path: The
$schemafield must point to the correct generated schema (../gen/schemas/desktop-schema.jsonormobile-schema.json). Runcargo tauri devonce to generate schemas - Merged capabilities: A window listed in multiple capabilities gets ALL permissions from ALL matching capabilities merged together -- there is no "override" mechanism
- Remote access on Linux/Android: Tauri cannot distinguish iframe requests from window requests on these platforms, making remote domain restrictions less effective
- Permission identifier format: Plugin permissions use
plugin:permission-name, app-defined permissions use justpermission-name(no prefix). Using the wrong format causes "permission not found" errors - TOML vs JSON for permissions: Capability files accept JSON or TOML. Custom permission definitions (in
src-tauri/permissions/) must be TOML only
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST create at least one capability file in src-tauri/capabilities/ -- without it, ALL plugin and core API calls fail at runtime)
(You MUST include core:default in every capability -- without it, basic app lifecycle commands fail)
(You MUST scope permissions to specific windows using the windows array -- a window not listed in any capability has zero IPC access)
(You MUST use deny scopes to restrict sensitive paths -- deny ALWAYS takes precedence over allow)
(You MUST use plugin:permission-name format for plugin permissions and plain permission-name for app commands)
Failure to follow these rules will cause silent runtime permission denials that do not surface at compile time.
</critical_reminders>
Files (skills)
-
examples
-
core.md 13.9 KB
# Tauri Capabilities & ACL - Core Patterns > Capability file structure, scoped permissions, window/platform targeting, remote access, debugging. See [SKILL.md](../SKILL.md) for decision frameworks and red flags. See [custom-permissions.md](custom-permissions.md) for app-defined permissions and permission sets. --- ## Pattern 1: Minimal Capability File Every Tauri 2 app starts with at least one capability file. This is the minimum viable capability. ```json // src-tauri/capabilities/default.json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Default permissions for all windows", "windows": ["main"], "permissions": ["core:default"] } ``` **Why this works:** `core:default` includes `core:app:default`, `core:event:default`, `core:window:default`, `core:path:default`, `core:image:default`, `core:menu:default`, `core:tray:default`, `core:webview:default`, and `core:resources:default`. This is enough for basic app lifecycle, window management, and event handling. **Without this file:** Every `invoke()` call from the frontend will fail with a permission denied error at runtime. --- ## Pattern 2: Complete Capability File (All Fields) A capability file with every available field documented. ```json // src-tauri/capabilities/main.json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "main-capability", "description": "Full permissions for the main application window", "local": true, "windows": ["main"], "webviews": [], "platforms": ["linux", "macOS", "windows"], "permissions": [ "core:default", "event:default", "window:default", "path:default", "shell:allow-open", "dialog:default", { "identifier": "fs:allow-read-text-file", "allow": [{ "path": "$APPDATA/**" }, { "path": "$RESOURCE/**" }] }, { "identifier": "fs:allow-write-text-file", "allow": [{ "path": "$APPDATA/**" }] } ], "remote": { "urls": ["https://*.example.com"] } } ``` **Field reference:** | Field | Type | Required | Purpose | | ------------- | -------------------- | -------- | ------------------------------------------------------------------------------ | | `$schema` | string | No | IDE autocompletion (generated on first `cargo tauri dev`) | | `identifier` | string | Yes | Unique name for this capability (`[a-z]` and hyphens only) | | `description` | string | Yes | Explains what this capability grants | | `local` | boolean | No | Enable for local app URLs (default: `true`) | | `windows` | string[] | No | Window labels this capability applies to (supports `"*"` wildcard) | | `webviews` | string[] | No | Webview labels this capability applies to | | `platforms` | string[] | No | Restrict to platforms: `"linux"`, `"macOS"`, `"windows"`, `"iOS"`, `"android"` | | `permissions` | (string \| object)[] | Yes | Permission identifiers or scoped permission objects | | `remote` | object | No | Remote domain access via `urls` array using URLPattern standard | --- ## Pattern 3: Scoped Filesystem Permissions Restrict file operations to specific directories using path scopes. Deny always wins. ```json // src-tauri/capabilities/file-access.json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "file-access", "description": "Scoped file system access for main window", "windows": ["main"], "permissions": [ "core:default", "fs:default", { "identifier": "fs:allow-read-text-file", "allow": [ { "path": "$APPDATA/**" }, { "path": "$RESOURCE/**" }, { "path": "$DOCUMENT/**" } ] }, { "identifier": "fs:allow-write-text-file", "allow": [{ "path": "$APPDATA/**" }] }, { "identifier": "fs:deny-read-text-file", "deny": [ { "path": "$APPDATA/secrets/**" }, { "path": "$APPDATA/.credentials/**" } ] } ] } ``` **Why good:** Reads from app data, resources, and documents. Writes only to app data. Explicitly denies access to secrets subdirectory. Even if another capability allows reading `$APPDATA/**`, the deny scope blocks the secrets path. ```json // BAD: No path scope -- allows reading ANY file on the system { "permissions": ["fs:allow-read-text-file"] } ``` **Why bad:** Without a scope, the permission grants access to the entire filesystem. Always scope filesystem permissions to specific directories. **Path variables and glob patterns:** See [reference.md](../reference.md) for the full path variable table (`$APPDATA`, `$HOME`, `$RESOURCE`, etc.) and glob pattern syntax. --- ## Pattern 4: Scoped HTTP Permissions Restrict HTTP requests to specific domains using URL scopes. ```json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "api-access", "description": "Restricted HTTP access", "windows": ["main"], "permissions": [ "core:default", { "identifier": "http:default", "allow": [ { "url": "https://api.example.com/**" }, { "url": "https://cdn.example.com/**" } ], "deny": [{ "url": "https://api.example.com/admin/**" }] } ] } ``` **Why good:** HTTP requests are restricted to two specific domains. Admin endpoints are explicitly denied even though the base domain is allowed. --- ## Pattern 5: Multi-Window Capabilities Different windows get different permission levels based on their role. ```json // src-tauri/capabilities/editor.json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "editor-capability", "description": "Full access for the editor window", "windows": ["editor"], "permissions": [ "core:default", "fs:default", "dialog:default", { "identifier": "fs:allow-read-text-file", "allow": [{ "path": "$DOCUMENT/**" }, { "path": "$APPDATA/**" }] }, { "identifier": "fs:allow-write-text-file", "allow": [{ "path": "$DOCUMENT/**" }, { "path": "$APPDATA/**" }] } ] } ``` ```json // src-tauri/capabilities/viewer.json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "viewer-capability", "description": "Read-only access for the viewer window", "windows": ["viewer"], "permissions": [ "core:default", { "identifier": "fs:allow-read-text-file", "allow": [{ "path": "$RESOURCE/**" }] } ] } ``` **Why good:** Principle of least privilege per window. The editor can read/write documents and app data. The viewer can only read bundled resources. If the viewer window is compromised, it cannot modify files or access user documents. **Wildcard window matching:** ```json { "windows": ["*"], "permissions": ["core:default"] } ``` The `"*"` wildcard matches all windows, including dynamically created ones. Use sparingly in production -- prefer explicit window labels. --- ## Pattern 6: Platform-Specific Capabilities Some plugins are desktop-only or mobile-only. Split capabilities by platform to prevent runtime errors. ```json // src-tauri/capabilities/desktop.json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "desktop-features", "description": "Desktop-only features", "windows": ["main"], "platforms": ["linux", "macOS", "windows"], "permissions": [ "core:default", "shell:allow-open", "global-shortcut:default", "autostart:default" ] } ``` ```json // src-tauri/capabilities/mobile.json { "$schema": "../gen/schemas/mobile-schema.json", "identifier": "mobile-features", "description": "Mobile-only features", "windows": ["main"], "platforms": ["iOS", "android"], "permissions": [ "core:default", "barcode-scanner:default", "biometric:default" ] } ``` **Why good:** Shell, autostart, and global-shortcut are desktop-only plugins. Barcode scanner and biometric are mobile-only. Splitting prevents "plugin not available" errors on the wrong platform. **Cross-platform capability (shared features):** ```json // src-tauri/capabilities/shared.json { "identifier": "shared-features", "description": "Features available on all platforms", "windows": ["main"], "permissions": [ "core:default", "dialog:default", "notification:default", "store:default" ] } ``` --- ## Pattern 7: Remote Domain Access Allow remote web content to access Tauri APIs. Use for apps that load external content in webviews. ```json // src-tauri/capabilities/remote-access.json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "remote-partner-access", "description": "Allow partner domain to use notifications", "windows": ["main"], "remote": { "urls": ["https://*.partner.com", "https://dashboard.internal.dev"] }, "permissions": ["notification:default"] } ``` **Why good:** Only the specified remote domains can call the notification API. The `urls` array uses the URLPattern standard -- `*` matches any subdomain. **Security warnings:** - Remote URLs gain access to the specified Tauri APIs -- this is a significant security surface - On Linux and Android, Tauri cannot distinguish between `<iframe>` requests and window requests - Only grant the minimum permissions needed to remote content - Never grant filesystem or shell permissions to remote domains --- ## Pattern 8: Referencing Capabilities in tauri.conf.json By default, all capability files in `src-tauri/capabilities/` are auto-enabled. To explicitly control which capabilities are active: ```json // tauri.conf.json { "app": { "security": { "capabilities": ["shared-features", "editor-capability"] } } } ``` **Key rule:** Once you add any entries to this array, ONLY the listed capabilities are used. Unlisted capability files are ignored. This is useful for excluding development-only capabilities from production builds. **Development vs production pattern:** Create a `dev-tools.json` capability with broad permissions for development. In production builds, omit it from the `capabilities` array in `tauri.conf.json`. In development, leave the array empty (or omit it entirely) so all capabilities are auto-discovered. --- ## Pattern 9: Inline Capabilities in tauri.conf.json For simple apps, capabilities can be defined directly in `tauri.conf.json` instead of separate files. ```json { "app": { "security": { "capabilities": [ { "identifier": "main-capability", "description": "Main window permissions", "windows": ["main"], "permissions": ["core:default", "shell:allow-open", "dialog:default"] } ] } } } ``` **When to inline:** Single-window apps with few permissions where a separate file feels like overkill. **When NOT to inline:** Multi-window apps, apps with scoped permissions, or when you want IDE autocompletion from the `$schema` field. --- ## Pattern 10: Debugging Permission Errors When a Tauri API call fails with "not allowed" or "permission denied": **Step 1: Check the error message.** It usually names the specific permission that is missing, e.g., `fs.write_text_file not allowed`. **Step 2: Verify the capability file.** Ensure the correct `plugin:permission-name` is listed in the `permissions` array. ```json // The permission identifier MUST match the plugin and command "permissions": [ "fs:allow-write-text-file" ] // NOT "fs:write-text-file" (missing allow/deny prefix) // NOT "allow-write-text-file" (missing plugin prefix) ``` **Step 3: Check the `windows` array.** Ensure the window making the API call is listed. A common mistake is creating a new window programmatically but forgetting to add its label to a capability. **Step 4: Check scopes.** If the permission has a scope, verify the path or URL matches. Remember deny always wins. **Step 5: Check for stale ACL.** If you recently added a permission but it still fails: ```sh # Clean the build to force ACL regeneration cargo clean cargo tauri dev ``` The build system embeds the ACL at compile time. If `build.rs` does not have `cargo:rerun-if-changed` for the capabilities directory, incremental builds may use a stale ACL. **Step 6: Verify schema generation.** Run `cargo tauri dev` once to generate the schema files in `src-tauri/gen/schemas/`. The `$schema` field in capability files only works after schemas are generated. --- ## Pattern 11: Content Security Policy Configure CSP in `tauri.conf.json` to control what the webview can load. This is separate from capabilities -- CSP restricts web content sources, capabilities restrict Tauri API access. ```json { "app": { "security": { "csp": "default-src 'self'; img-src 'self' asset: http://asset.localhost; style-src 'self' 'unsafe-inline'; connect-src ipc: http://ipc.localhost" } } } ``` **Key CSP directives for Tauri:** | Directive | Value | Purpose | | ------------- | ------------------------------- | ----------------------------------------------- | | `default-src` | `'self'` | Only allow loading from the app's own origin | | `connect-src` | `ipc: http://ipc.localhost` | Required for Tauri command invocation | | `img-src` | `asset: http://asset.localhost` | Required for loading bundled assets as images | | `style-src` | `'self' 'unsafe-inline'` | Allow inline styles (many frameworks need this) | **Key rules:** - Avoid `'unsafe-eval'` for scripts -- it allows arbitrary code execution - The `ipc:` and `http://ipc.localhost` schemes are Tauri-specific for command invocation - The `asset:` and `http://asset.localhost` schemes are for accessing bundled files - If your frontend framework requires `'unsafe-eval'` in development, restrict it to dev-only configuration -
custom-permissions.md 6.7 KB
# Tauri Capabilities & ACL - Custom Permissions > Custom permission definitions, permission sets, app-level permissions. See [SKILL.md](../SKILL.md) for decision frameworks and red flags. See [core.md](core.md) for capability files and scoped permissions. --- ## Pattern 1: Auto-Generated Permissions Tauri automatically generates `allow-*` and `deny-*` permissions for every command registered in `generate_handler![]`. You do not need to define these manually. For a command registered as: ```rust #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}!", name) } #[tauri::command] fn reset_database(state: tauri::State<AppState>) -> Result<(), String> { // ... Ok(()) } tauri::Builder::default() .invoke_handler(tauri::generate_handler![greet, reset_database]) ``` Tauri auto-generates these permissions (visible in `src-tauri/permissions/autogenerated/`): - `allow-greet` / `deny-greet` - `allow-reset-database` / `deny-reset-database` **Key point:** The permission name uses the kebab-case version of the Rust function name. `reset_database` becomes `allow-reset-database`. **Key point:** Unlike plugin permissions, app-defined permissions do NOT use a prefix. In a capability file, reference them as `"allow-greet"`, not `"app:allow-greet"`. --- ## Pattern 2: Default Permission Set Define a default permission set for your app in `src-tauri/permissions/default.toml`. This groups auto-generated permissions into a single `default` identifier. ```toml # src-tauri/permissions/default.toml $schema = "schemas/schema.json" [default] description = "Default permissions for the application" permissions = [ "allow-greet", "allow-get-settings", "allow-save-settings" ] ``` Now reference it in a capability file: ```json { "identifier": "main-capability", "windows": ["main"], "permissions": ["core:default", "default"] } ``` **Why good:** Groups safe, commonly-needed commands under one identifier. The capability file stays clean. Sensitive commands (like `allow-reset-database`) are excluded from the default set, requiring explicit opt-in. --- ## Pattern 3: Named Permission Sets Create named permission sets to group related permissions. Useful for role-based or feature-based access control. ```toml # src-tauri/permissions/editor.toml [[set]] identifier = "editor-commands" description = "Permissions for document editing features" permissions = [ "allow-open-document", "allow-save-document", "allow-export-pdf", "allow-get-recent-files" ] ``` ```toml # src-tauri/permissions/admin.toml [[set]] identifier = "admin-commands" description = "Permissions for administrative operations" permissions = [ "allow-reset-database", "allow-export-all-data", "allow-manage-users", "allow-view-audit-log" ] ``` Reference in capability files: ```json // src-tauri/capabilities/editor-window.json { "identifier": "editor-window", "windows": ["editor"], "permissions": ["core:default", "editor-commands"] } ``` ```json // src-tauri/capabilities/admin-window.json { "identifier": "admin-window", "windows": ["admin"], "permissions": ["core:default", "editor-commands", "admin-commands"] } ``` **Why good:** The editor window gets document editing permissions but NOT admin operations. The admin window gets both. Permission sets make this composition clean and maintainable. --- ## Pattern 4: Custom Permissions with Scopes Define custom permissions with scopes for fine-grained access control over your own commands. ```toml # src-tauri/permissions/file-ops.toml [[permission]] identifier = "allow-read-project-files" description = "Allow reading files within the project directory" commands.allow = ["read_file"] [[scope.allow]] path = "$DOCUMENT/projects/**" [[permission]] identifier = "deny-read-dotfiles" description = "Deny reading hidden configuration files" commands.deny = ["read_file"] [[scope.deny]] path = "$DOCUMENT/projects/**/.*" ``` **Key point:** The scope is enforced by the command implementation. Tauri passes the resolved scope to the command -- the command must check that the requested path falls within the allowed scope. For built-in plugins, this enforcement is automatic. For custom commands, you implement the check. --- ## Pattern 5: Permission Set Combining Plugin and App Permissions Permission sets can combine both plugin permissions and app-defined permissions. ```toml # src-tauri/permissions/project-management.toml [[set]] identifier = "project-management" description = "All permissions needed for project management features" permissions = [ "allow-create-project", "allow-delete-project", "allow-list-projects", "fs:allow-read-text-file", "fs:allow-write-text-file", "dialog:allow-open", "dialog:allow-save" ] ``` ```json // src-tauri/capabilities/main.json { "identifier": "main-capability", "windows": ["main"], "permissions": ["core:default", "project-management"] } ``` **Why good:** One permission set bundles everything a feature needs -- app commands AND plugin permissions. Adding or removing features is a single-line change in the capability file. --- ## Pattern 6: Platform-Specific Permission Sets Permission sets can target specific platforms, useful when a feature uses platform-specific plugins. ```toml # src-tauri/permissions/desktop-features.toml [[set]] identifier = "desktop-file-management" description = "Desktop file management with shell integration" permissions = [ "allow-open-in-explorer", "allow-open-in-terminal", "shell:allow-open" ] ``` Reference with platform restriction in the capability: ```json { "identifier": "desktop-file-ops", "windows": ["main"], "platforms": ["linux", "macOS", "windows"], "permissions": ["core:default", "desktop-file-management"] } ``` **Key point:** The platform restriction is on the capability, not the permission set. Permission sets are platform-agnostic. The capability decides which platforms get the set. --- ## Permission File Location Summary | Location | Format | Purpose | | -------------------------------------- | --------- | ----------------------------------------------------- | | `src-tauri/permissions/default.toml` | TOML | Default permission set for the app | | `src-tauri/permissions/<name>.toml` | TOML | Custom permission sets and individual permissions | | `src-tauri/permissions/autogenerated/` | Auto | Tauri-generated `allow-*` / `deny-*` for each command | | `src-tauri/capabilities/<name>.json` | JSON/TOML | Capability files binding permissions to windows | **Key rule:** Permission definitions are always TOML. Capability files can be JSON or TOML. The auto-generated permissions folder is managed by Tauri -- do not edit files in it.
-
-
reference.md 11.2 KB
# Tauri Capabilities & ACL Reference > Quick-lookup tables, permission identifier patterns, path variables, core permissions, migration checklist. See [SKILL.md](SKILL.md) for decision frameworks and red flags. See [examples/core.md](examples/core.md) for full code examples. --- ## Permission Identifier Patterns | Pattern | Example | Meaning | | -------------------------- | ------------------------- | ------------------------------------------------------------- | | `core:default` | `core:default` | All default core permissions (app, event, window, path, etc.) | | `<plugin>:default` | `fs:default` | Plugin's safe default permission set | | `<plugin>:allow-<command>` | `fs:allow-read-text-file` | Allow a specific plugin command | | `<plugin>:deny-<command>` | `fs:deny-write-text-file` | Deny a specific plugin command | | `allow-<command>` | `allow-greet` | Allow an app-defined command (no prefix) | | `deny-<command>` | `deny-reset-database` | Deny an app-defined command (no prefix) | | `<set-name>` | `editor-commands` | Reference a custom permission set | **Key rule:** Plugin permissions always use the `plugin:` prefix. App-defined permissions never use a prefix. **Identifier restrictions:** Lowercase ASCII letters `[a-z]`, hyphens `-`, and colons `:` only. Max length: 116 characters. No digits after the first character. --- ## Core Default Permissions `core:default` bundles these permission groups: | Group | Includes | Purpose | | ------------------------ | ---------------------------------------------------------------- | ------------------------------------- | | `core:app:default` | `allow-version`, `allow-name`, `allow-tauri-version` | App metadata | | `core:event:default` | `allow-listen`, `allow-unlisten`, `allow-emit`, `allow-emit-to` | Event system | | `core:window:default` | Position, size, focus, state queries | Window info (read-only safe defaults) | | `core:path:default` | `allow-resolve-directory`, `allow-join`, `allow-normalize`, etc. | Path utilities | | `core:image:default` | Image operations | Image handling | | `core:menu:default` | Menu operations | App/context menus | | `core:tray:default` | Tray operations | System tray | | `core:webview:default` | Webview operations | Webview management | | `core:resources:default` | Resource operations | Bundled resource access | **Always include `core:default`** in every capability. Without it, basic operations like event listening and window title queries fail. --- ## Tauri Path Variables Use in capability file scopes. These are Tauri-specific, not environment variables. | Variable | Resolves to | Typical use | | --------------- | -------------------------------------------------------------------------------------- | ---------------------------------- | | `$APPDATA` | App data dir (`~/.local/share/<id>` Linux, `~/Library/Application Support/<id>` macOS) | User data, databases | | `$APPCONFIG` | App config dir (`~/.config/<id>` Linux) | Settings, preferences | | `$APPCACHE` | App cache dir | Temporary cached data | | `$APPLOG` | App log dir | Log files | | `$APPLOCALDATA` | App local data dir | Platform-specific local data | | `$HOME` | User home directory | Broad user access (use cautiously) | | `$RESOURCE` | App resource dir (bundled assets) | Read-only bundled files | | `$TEMP` | System temp directory | Temporary files | | `$DESKTOP` | User desktop dir | Desktop shortcuts, exports | | `$DOCUMENT` | User documents dir | User documents | | `$DOWNLOAD` | User downloads dir | Downloaded files | **Glob patterns:** `$APPDATA/*` = direct children. `$APPDATA/**` = all descendants recursively. --- ## Capability File Quick Reference ### Minimal capability ```json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Default permissions", "windows": ["main"], "permissions": ["core:default"] } ``` ### Scoped permission (inline) ```json { "identifier": "fs:allow-read-text-file", "allow": [{ "path": "$APPDATA/**" }] } ``` ### Deny scope (inline) ```json { "identifier": "fs:deny-read-text-file", "deny": [{ "path": "$HOME/.ssh/**" }] } ``` ### Platform restriction ```json { "platforms": ["linux", "macOS", "windows"] } ``` ### Remote domain access ```json { "remote": { "urls": ["https://*.example.com"] } } ``` --- ## Common Plugin Permission Identifiers | Plugin | Common Permissions | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **fs** | `fs:default`, `fs:allow-read-text-file`, `fs:allow-write-text-file`, `fs:allow-exists`, `fs:allow-mkdir`, `fs:allow-remove`, `fs:allow-rename`, `fs:allow-copy-file` | | **dialog** | `dialog:default`, `dialog:allow-open`, `dialog:allow-save`, `dialog:allow-message`, `dialog:allow-ask`, `dialog:allow-confirm` | | **http** | `http:default`, `http:allow-fetch` | | **shell** | `shell:allow-open`, `shell:allow-execute`, `shell:default` | | **notification** | `notification:default`, `notification:allow-notify`, `notification:allow-request-permission` | | **store** | `store:default`, `store:allow-get`, `store:allow-set`, `store:allow-delete`, `store:allow-keys`, `store:allow-clear` | | **clipboard** | `clipboard-manager:default`, `clipboard-manager:allow-read-text`, `clipboard-manager:allow-write-text` | | **process** | `process:default`, `process:allow-exit`, `process:allow-restart` | | **updater** | `updater:default` | | **os** | `os:default`, `os:allow-platform`, `os:allow-arch`, `os:allow-version` | **Finding available permissions:** Use the `$schema` field in your capability file pointing to the generated schema (`../gen/schemas/desktop-schema.json`). Your IDE will autocomplete all available permission identifiers. --- ## Tauri v1 to v2 Security Migration | v1 (allowlist in tauri.conf.json) | v2 (capability files) | | --------------------------------------------- | --------------------------------------------------- | | `"allowlist": { "fs": { "all": true } }` | `"fs:default"` + scoped `fs:allow-*` permissions | | `"allowlist": { "fs": { "readFile": true } }` | `"fs:allow-read-text-file"` with path scope | | `"allowlist": { "shell": { "open": true } }` | `"shell:allow-open"` | | `"allowlist": { "dialog": { "all": true } }` | `"dialog:default"` | | `"allowlist": { "http": { "all": true } }` | `"http:default"` with URL scope | | Scope defined in tauri.conf.json | Scope defined inline in capability file permissions | | Global boolean per API | Per-window, per-platform, per-path granular control | ### Migration steps 1. Run `npm run tauri migrate` (or `cargo tauri migrate`) -- auto-generates capability files from v1 allowlist 2. Review generated capabilities -- the migration tool may grant overly broad permissions 3. Add path scopes to filesystem permissions 4. Add URL scopes to HTTP permissions 5. Split capabilities by window if you have multiple windows 6. Remove `"allowlist"` from `tauri.conf.json` (it is ignored in v2) 7. Test every feature -- missing permissions only surface at runtime **Key differences:** - v1 was global on/off. v2 is per-window, per-platform, per-path. - v1 had no deny mechanism. v2 has deny scopes that always win over allow. - v1 scopes were in config. v2 scopes are inline in capability files. - v2 permissions are runtime-checked, not compile-time-checked. --- ## Debugging Checklist When a Tauri API call fails: 1. [ ] Error message names the missing permission -- add it to the capability file 2. [ ] Permission uses correct format: `plugin:permission-name` for plugins, `permission-name` for app commands 3. [ ] The calling window's label is in the capability's `windows` array 4. [ ] Path/URL scopes match the actual paths/URLs being accessed 5. [ ] No deny scope is blocking the operation (deny always wins) 6. [ ] Build is not using stale ACL -- run `cargo clean && cargo tauri dev` 7. [ ] Schema files exist in `src-tauri/gen/schemas/` (run `cargo tauri dev` once to generate) 8. [ ] If using `app.security.capabilities` in config, the capability is listed there 9. [ ] Platform field (if set) includes the current platform 10. [ ] Plugin is registered in Rust with `.plugin(tauri_plugin_<name>::init())` or `.plugin(tauri_plugin_<name>::Builder::new().build())` -
SKILL.md 15.1 KB
--- name: desktop-security-tauri description: Tauri 2.x deny-by-default security model, capabilities, permissions, scopes, ACL --- # Tauri Capabilities & ACL > **Quick Guide:** Tauri 2 uses a deny-by-default security model. Nothing is accessible unless explicitly granted in a capability file (`src-tauri/capabilities/*.json`). Capabilities bind permissions to specific windows. Permissions follow the `plugin:command` identifier pattern. Scopes restrict operations to specific paths or URLs with allow/deny lists (deny always wins). Every plugin and custom command needs a permission grant -- missing permissions cause runtime errors, not compile errors. > > **Current version:** Tauri 2.x (stable). Tauri 1.x used a boolean allowlist which is completely removed in v2. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST create at least one capability file in `src-tauri/capabilities/` -- without it, ALL plugin and core API calls fail at runtime)** **(You MUST include `core:default` in every capability -- without it, basic app lifecycle commands fail)** **(You MUST scope permissions to specific windows using the `windows` array -- a window not listed in any capability has zero IPC access)** **(You MUST use deny scopes to restrict sensitive paths -- deny ALWAYS takes precedence over allow)** **(You MUST use `plugin:permission-name` format for plugin permissions and plain `permission-name` for app commands)** </critical_requirements> --- **Auto-detection:** Tauri capabilities, src-tauri/capabilities, capability file, permissions, ACL, allow-scope, deny-scope, core:default, fs:allow, shell:allow, http:allow, permission set, remote domain, CapabilityRemote, desktop-schema.json, mobile-schema.json, scope allow deny, Tauri security, tauri permission denied, capability identifier **When to use:** - Creating or modifying capability files for a Tauri 2 app - Granting permissions to specific windows or webviews - Restricting filesystem, HTTP, or shell access with scoped permissions - Writing custom permissions for your own Tauri commands - Grouping permissions into permission sets - Enabling remote domain access to Tauri APIs - Debugging "permission denied" or "not allowed" runtime errors - Migrating from the Tauri v1 allowlist to v2 capabilities **When NOT to use:** - Writing Tauri commands or IPC bridge logic (use desktop-framework-tauri) - Plugin installation and registration (use desktop-framework-tauri) - Window management, system tray, or menus (use desktop-framework-tauri) - General Rust programming unrelated to Tauri ACL - Frontend framework patterns **Key patterns covered:** - Capability file structure and fields ([examples/core.md](examples/core.md)) - Window-specific and platform-specific capabilities ([examples/core.md](examples/core.md)) - Scoped permissions with allow/deny lists ([examples/core.md](examples/core.md)) - Custom permission definitions in TOML ([examples/custom-permissions.md](examples/custom-permissions.md)) - Permission sets for grouping related permissions ([examples/custom-permissions.md](examples/custom-permissions.md)) - Remote domain access ([examples/core.md](examples/core.md)) - Debugging permission errors ([examples/core.md](examples/core.md)) - Migration from v1 allowlist ([reference.md](reference.md)) **Detailed resources:** - [examples/core.md](examples/core.md) - Capability files, scoped permissions, window/platform targeting, remote access, debugging - [examples/custom-permissions.md](examples/custom-permissions.md) - Custom permission definitions, permission sets, app-level permissions - [reference.md](reference.md) - Permission identifier patterns, path variables, core permissions, v1 migration checklist --- <philosophy> ## Philosophy Tauri 2 implements a **deny-by-default** access control model. Every potentially dangerous operation (filesystem, network, shell, clipboard) is blocked until explicitly granted in a capability file. This is a fundamental shift from v1's boolean allowlist -- instead of toggling features on/off globally, you define granular permissions scoped to specific windows, platforms, and paths. **The security hierarchy:** 1. **Capabilities** - Bind permissions to windows/webviews. A window not listed in any capability has zero IPC access. 2. **Permissions** - Define what operations are allowed or denied. Follow the `plugin:command` identifier pattern. 3. **Scopes** - Restrict WHERE operations can act (paths, URLs). Deny always supersedes allow. **Key design decisions:** - **Granular, not global** -- permissions are per-window, per-platform, per-path - **Deny wins** -- if a path is denied by any scope, it is blocked even if allowed by another - **Runtime, not compile-time** -- missing permissions cause runtime errors, not build errors. This makes debugging harder but allows dynamic permission resolution. - **Schema-driven** -- capability files reference auto-generated schemas (`desktop-schema.json`, `mobile-schema.json`) for IDE autocompletion **When to invest in fine-grained capabilities:** - Multi-window apps where windows need different permission levels - Apps handling sensitive data (credentials, financial records) - Apps distributed publicly where security posture matters - Apps with remote content that needs limited API access **When simple capabilities suffice:** - Single-window apps with straightforward needs - Internal tools where the security boundary is less critical - Prototypes and MVPs where iteration speed matters more </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Capability File Structure Every Tauri 2 app needs at least one capability file in `src-tauri/capabilities/`. The file grants permissions to specific windows. ```json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "main-capability", "description": "Permissions for the main application window", "windows": ["main"], "permissions": ["core:default", "event:default", "window:default"] } ``` **Key fields:** `identifier` (unique name), `windows` (which windows get these permissions), `permissions` (what operations are allowed). The `$schema` field enables IDE autocompletion for available permissions. **Key rule:** All capabilities in `src-tauri/capabilities/` are auto-enabled unless you explicitly list capabilities in `tauri.conf.json`'s `app.security.capabilities` array -- in which case only the listed ones are used. See [examples/core.md](examples/core.md) for all capability fields and patterns. --- ### Pattern 2: Scoped Permissions (Allow/Deny) Restrict plugin operations to specific paths using inline scope objects. Deny always takes precedence. ```json { "identifier": "fs:allow-read-text-file", "allow": [{ "path": "$APPDATA/**" }] } ``` ```json { "identifier": "fs:deny-read-text-file", "deny": [{ "path": "$APPDATA/secrets/**" }] } ``` **Key rule:** Deny always supersedes allow. If a path matches both an allow and a deny scope, it is blocked. Use `$APPDATA`, `$HOME`, `$RESOURCE` and other Tauri path variables -- not hardcoded OS paths. See [examples/core.md](examples/core.md) for filesystem, HTTP, and shell scope patterns. --- ### Pattern 3: Window-Specific Capabilities Different windows get different permission sets. An editor window gets write access; a viewer window gets read-only access. ```json { "identifier": "editor-capability", "windows": ["editor"], "permissions": ["core:default", "fs:allow-write-text-file"] } ``` ```json { "identifier": "viewer-capability", "windows": ["viewer"], "permissions": ["core:default", "fs:allow-read-text-file"] } ``` **Key rule:** A window not listed in any capability's `windows` array has zero IPC access. Windows listed in multiple capabilities get the merged permissions of all matching capabilities. See [examples/core.md](examples/core.md) for multi-window and wildcard patterns. --- ### Pattern 4: Platform-Specific Capabilities Use the `platforms` field to restrict capabilities to specific operating systems. ```json { "identifier": "desktop-features", "windows": ["main"], "platforms": ["linux", "macOS", "windows"], "permissions": ["core:default", "shell:allow-open", "global-shortcut:default"] } ``` **Key rule:** Platform values are `"linux"`, `"macOS"`, `"windows"`, `"iOS"`, `"android"`. Splitting by platform prevents permission errors for platform-specific plugins. See [examples/core.md](examples/core.md) for mobile-specific capability examples. --- ### Pattern 5: Custom Permission Definitions Define permissions for your own Tauri commands using TOML files in `src-tauri/permissions/`. ```toml # src-tauri/permissions/default.toml [default] description = "Default app permissions" permissions = ["allow-greet", "allow-get-settings"] ``` ```toml # src-tauri/permissions/admin.toml [[permission]] identifier = "allow-admin-ops" description = "Allow admin operations" commands.allow = ["reset_database", "export_all_data"] ``` **Key point:** Tauri auto-generates `allow-*` and `deny-*` permissions for every command registered in `generate_handler![]`. Custom permission files let you group them and add scopes. See [examples/custom-permissions.md](examples/custom-permissions.md) for permission sets and custom scope definitions. --- ### Pattern 6: Remote Domain Access Grant remote web content access to Tauri APIs using the `remote` field with URL patterns. ```json { "identifier": "remote-api-access", "windows": ["main"], "remote": { "urls": ["https://*.mydomain.dev"] }, "permissions": ["core:default", "notification:default"] } ``` **Key rule:** Remote URLs gain access to the specified Tauri APIs -- understand the security implications. On Linux and Android, Tauri cannot distinguish between iframe requests and window requests, so remote access should be used cautiously on those platforms. See [examples/core.md](examples/core.md) for remote access patterns and security considerations. </patterns> --- <decision_framework> ## Decision Framework ### What Permissions Does My App Need? ``` What does the app do? |-- Reads/writes files? | +-- tauri-plugin-fs permissions with path scopes |-- Makes HTTP requests from Rust backend? | +-- tauri-plugin-http permissions with URL scopes |-- Opens file/folder dialogs? | +-- tauri-plugin-dialog permissions |-- Runs external processes? | +-- tauri-plugin-shell permissions (desktop only) |-- Shows system notifications? | +-- tauri-plugin-notification permissions |-- Uses persistent key-value storage? | +-- tauri-plugin-store permissions +-- Basic app + window lifecycle only? +-- core:default is sufficient ``` ### How Granular Should Capabilities Be? ``` How many windows does the app have? |-- Single window? | +-- One capability file with all permissions is fine |-- Multiple windows with SAME needs? | +-- One capability file listing all windows in the array +-- Multiple windows with DIFFERENT needs? +-- Separate capability files per window (principle of least privilege) Does the app need platform-specific features? |-- Same features on all platforms? | +-- Omit the platforms field +-- Different features per platform? +-- Separate capability files with platforms field ``` ### Where to Put the Scope? ``` Is the scope for a built-in plugin? |-- YES -> Inline in the capability file permissions array | { "identifier": "fs:allow-read-text-file", "allow": [...] } +-- NO -> Is it for your custom commands? |-- YES -> Define in src-tauri/permissions/*.toml +-- NO -> It might not need a scope -- simple allow/deny suffices ``` </decision_framework> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Missing `core:default` in a capability -- basic app lifecycle commands fail silently - No capability file at all in `src-tauri/capabilities/` -- ALL IPC calls fail at runtime - Using v1 `tauri.allowlist` in config -- completely removed in v2, does nothing - Granting `fs:allow-read-text-file` without path scope -- allows reading ANY file on the system - Missing `windows` array in capability -- permissions apply to no windows (useless capability) **Medium Priority Issues:** - Using wildcard `"windows": ["*"]` in production -- every window gets these permissions, including dynamic ones - Not splitting capabilities by platform -- desktop-only plugins (shell, autostart, global-shortcut) cause errors on mobile - Overly broad HTTP scopes (`http:allow-fetch` without URL scope) -- allows requests to any URL - Granting write permissions when only read is needed -- violates least privilege **Common Mistakes:** - Forgetting that deny always wins -- adding a deny scope and wondering why the allow scope "doesn't work" - Using OS paths (`/home/user/`) instead of Tauri path variables (`$HOME/`) in scopes - Expecting compile-time errors for missing permissions -- they are runtime errors only - Using `app.security.capabilities` in `tauri.conf.json` to list some capabilities, then wondering why unlisted capability files are ignored (explicit list overrides auto-discovery) - Capability identifier containing digits after first character (identifiers are restricted to `[a-z]` plus hyphens) **Gotchas & Edge Cases:** - **Stale ACL in builds**: If you add a new permission but the build does not regenerate, the old ACL is embedded. Clean build (`cargo clean`) or ensure `build.rs` has `cargo:rerun-if-changed` for the capabilities directory - **Schema path**: The `$schema` field must point to the correct generated schema (`../gen/schemas/desktop-schema.json` or `mobile-schema.json`). Run `cargo tauri dev` once to generate schemas - **Merged capabilities**: A window listed in multiple capabilities gets ALL permissions from ALL matching capabilities merged together -- there is no "override" mechanism - **Remote access on Linux/Android**: Tauri cannot distinguish iframe requests from window requests on these platforms, making remote domain restrictions less effective - **Permission identifier format**: Plugin permissions use `plugin:permission-name`, app-defined permissions use just `permission-name` (no prefix). Using the wrong format causes "permission not found" errors - **TOML vs JSON for permissions**: Capability files accept JSON or TOML. Custom permission definitions (in `src-tauri/permissions/`) must be TOML only </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST create at least one capability file in `src-tauri/capabilities/` -- without it, ALL plugin and core API calls fail at runtime)** **(You MUST include `core:default` in every capability -- without it, basic app lifecycle commands fail)** **(You MUST scope permissions to specific windows using the `windows` array -- a window not listed in any capability has zero IPC access)** **(You MUST use deny scopes to restrict sensitive paths -- deny ALWAYS takes precedence over allow)** **(You MUST use `plugin:permission-name` format for plugin permissions and plain `permission-name` for app commands)** **Failure to follow these rules will cause silent runtime permission denials that do not surface at compile time.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.