src/skills/desktop-plugins-tauri/SKILL.md
Tauri 2.x official plugin ecosystem, plugin APIs, permissions, and custom plugin development
npx skillsauth add agents-inc/skills desktop-plugins-tauriInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Tauri plugins follow a dual-install pattern: Cargo crate (Rust backend) + npm package (JS frontend). Every plugin must be registered with
.plugin()in Rust AND have permissions granted in a capability file. Missing any step causes runtime errors, not compile errors. Custom plugins usetauri::plugin::Builderwith optional mobile support (Swift/Kotlin). There are 30+ official plugins covering fs, http, dialog, store, notification, shell, updater, sql, log, stronghold, deep-link, global-shortcut, and more.Current version: Tauri 2.x (stable). All plugins require Rust 1.77.2+.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST complete ALL four installation steps for every plugin: 1) cargo add crate, 2) npm install bindings, 3) .plugin() registration in Rust, 4) permissions in capability file -- missing any step causes runtime errors)
(You MUST scope plugin permissions in capability files -- never grant unscoped fs:allow-read-text-file or http:default without URL restrictions)
(You MUST use @tauri-apps/plugin-* npm packages for JS bindings -- not @tauri-apps/api/* which is the core API)
(You MUST use #[cfg(desktop)] guard when registering desktop-only plugins -- mobile builds will fail otherwise)
(You MUST use tauri::plugin::Builder with an init() convention when creating custom plugins -- not raw command registration)
</critical_requirements>
Auto-detection: tauri-plugin, @tauri-apps/plugin, tauri_plugin, plugin registration, .plugin(), tauri-plugin-fs, tauri-plugin-http, tauri-plugin-store, tauri-plugin-dialog, tauri-plugin-notification, tauri-plugin-shell, tauri-plugin-updater, tauri-plugin-log, tauri-plugin-sql, tauri-plugin-stronghold, tauri-plugin-deep-link, tauri-plugin-global-shortcut, tauri-plugin-autostart, tauri-plugin-clipboard-manager, tauri-plugin-window-state, tauri-plugin-single-instance, tauri-plugin-barcode-scanner, tauri-plugin-biometric, tauri-plugin-os, tauri-plugin-process, custom plugin, plugin development, npx tauri plugin new
When to use:
When NOT to use:
Key patterns covered:
Detailed resources:
Tauri plugins extend the core framework with native capabilities through a dual-architecture design: a Rust backend crate providing the implementation, and an npm package providing typed JavaScript bindings. This separation enforces security -- every plugin operation must be explicitly permitted in a capability file.
Plugin architecture principles:
tauri::plugin::Builder with an init() export.When to use plugins vs custom commands:
When NOT to use a plugin:
navigator.clipboard for simple text copy in some contexts)Every official plugin requires exactly four steps. Missing any step causes runtime errors, not compile errors.
# Step 1: Add Rust crate
cargo add tauri-plugin-store
# Step 2: Add JS bindings
npm add @tauri-apps/plugin-store
# Step 3: Register plugin in Rust (src-tauri/src/lib.rs)
# .plugin(tauri_plugin_store::Builder::new().build())
# Step 4: Add permissions to capability file (src-tauri/capabilities/main.json)
# "store:default"
Why all four steps: Cargo crate = backend implementation, npm package = typed JS bindings, .plugin() = runtime activation, capability permission = frontend authorization. Any missing piece causes a runtime error with an unhelpful message.
Shortcut: cargo tauri add <plugin> handles steps 1 and 3 automatically. You still need npm install (step 2) and permissions (step 4).
See examples/core.md for multi-plugin registration and permission scoping.
Plugins operate under least-privilege. Scope permissions to specific paths, URLs, or commands.
{
"permissions": [
"core:default",
{
"identifier": "fs:allow-read-text-file",
"allow": [{ "path": "$APPDATA/**" }]
},
{
"identifier": "http:default",
"allow": [{ "url": "https://api.example.com/**" }]
}
]
}
Why scoping matters: Unscoped fs:allow-read-text-file grants access to ANY file on the system. Unscoped http:default allows requests to ANY domain. Always restrict to the minimum required scope.
See examples/core.md for shell command scoping and window-specific permissions.
Desktop-only plugins (shell, autostart, global-shortcut, single-instance, window-state, positioner) must be wrapped in #[cfg(desktop)] to prevent mobile build failures.
tauri::Builder::default()
.setup(|app| {
#[cfg(desktop)]
{
app.handle().plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
None,
));
app.handle().plugin(tauri_plugin_global_shortcut::Builder::new().build());
}
Ok(())
})
Key point: Without #[cfg(desktop)], the Rust compiler will fail on mobile targets because these crates do not support iOS/Android.
Three storage plugins serve different needs:
| Plugin | Use Case | Encryption | Query | Platform | | ---------- | -------------------------- | -------------- | -------------------------------- | -------- | | Store | App preferences, settings | No | Key-value only | All | | Stronghold | Secrets, API keys, tokens | Yes (Argon2) | Key-value only | Desktop | | SQL | Structured data, relations | No (app-level) | Full SQL (SQLite/MySQL/Postgres) | All |
Decision: User preferences and simple config? Store. Sensitive credentials? Stronghold. Structured relational data? SQL.
See examples/data-storage.md for complete API examples for each.
The updater plugin requires cryptographic signatures -- this cannot be disabled. Updates check an endpoint, verify the signature, download, and install.
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
const update = await check();
if (update) {
await update.downloadAndInstall((event) => {
// event.event: "Started" | "Progress" | "Finished"
});
await relaunch();
}
Key point: Generate signing keys with cargo tauri signer generate. Set TAURI_SIGNING_PRIVATE_KEY during builds. The public key goes in tauri.conf.json. Losing the private key means you cannot ship updates to existing users.
See examples/lifecycle.md for endpoint JSON format and Rust API.
Custom plugins use tauri::plugin::Builder with the init() convention.
use tauri::plugin::{Builder, TauriPlugin};
use tauri::Runtime;
#[tauri::command]
fn my_command() -> String {
"Hello from plugin".into()
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("my-plugin")
.invoke_handler(tauri::generate_handler![my_command])
.setup(|app, _api| {
// Initialize state, start background tasks
Ok(())
})
.build()
}
Key points: Plugin commands are invoked as plugin:my-plugin|my_command from JS. Scaffold a full plugin project with npx @tauri-apps/cli plugin new <name>. The template includes desktop.rs, mobile.rs, permissions, and JS bindings.
See examples/custom-plugins.md for lifecycle hooks, configuration, and mobile support.
</patterns><decision_framework>
What native capability do you need?
|
+-- File system read/write?
| +-- tauri-plugin-fs (scoped to specific directories)
|
+-- File/folder picker dialog?
| +-- tauri-plugin-dialog (open, save, message, ask)
|
+-- HTTP requests bypassing CORS?
| +-- tauri-plugin-http (scope to specific domains)
|
+-- Persistent key-value storage?
| +-- Sensitive data (tokens, keys)? -> tauri-plugin-stronghold
| +-- App preferences/settings? -> tauri-plugin-store
|
+-- Relational/structured data?
| +-- tauri-plugin-sql (SQLite, MySQL, PostgreSQL)
|
+-- System notifications?
| +-- tauri-plugin-notification (check permissions first on macOS/mobile)
|
+-- Run external processes?
| +-- tauri-plugin-shell (desktop only, scope allowed commands)
|
+-- Auto-update?
| +-- tauri-plugin-updater (requires signed releases)
|
+-- Structured logging?
| +-- tauri-plugin-log (targets: stdout, file, webview)
|
+-- Custom URL scheme handling?
| +-- tauri-plugin-deep-link (configure per-platform)
|
+-- System-wide keyboard shortcuts?
| +-- tauri-plugin-global-shortcut (desktop only)
|
+-- Launch on system startup?
| +-- tauri-plugin-autostart (desktop only)
|
+-- Single app instance?
| +-- tauri-plugin-single-instance (desktop only)
|
+-- Remember window position/size?
| +-- tauri-plugin-window-state (desktop only)
|
+-- Clipboard access?
| +-- tauri-plugin-clipboard-manager
|
+-- Mobile camera/scanner?
| +-- tauri-plugin-barcode-scanner (mobile only)
|
+-- Biometric auth?
| +-- tauri-plugin-biometric (mobile only)
|
+-- OS/platform info?
| +-- tauri-plugin-os
|
+-- App restart/exit?
+-- tauri-plugin-process
Is this a reusable capability shared across projects?
+-- YES -> Custom plugin (npx @tauri-apps/cli plugin new)
+-- NO -> Is it complex enough to need its own permission model?
+-- YES -> Custom plugin
+-- NO -> Regular Tauri command (simpler, framework skill)
</decision_framework>
<red_flags>
High Priority Issues:
.plugin(), permissions) -- causes runtime error with unhelpful messagefs:allow-read-text-file without path restriction) -- grants access to entire filesystemhttp:default without URL pattern) -- allows requests to any domainshell:allow-execute without command allowlist) -- allows running arbitrary commands@tauri-apps/api/* imports for plugin functionality -- plugins use @tauri-apps/plugin-* packages#[cfg(desktop)] -- breaks mobile buildsMedium Priority Issues:
isPermissionGranted() before sending notifications on macOS/mobileshell:allow-execute when only shell:allow-open (URLs/files) is neededsql:allow-execute permission (default only includes read operations)stronghold.save() after modifications (changes are lost)Common Mistakes:
cargo tauri add and assuming all four steps are done (npm install and permissions still needed)Gotchas & Edge Cases:
.init() (fs, dialog, shell, notification), others use Builder::new().build() (store, updater, global-shortcut, log) -- check each plugin's docsautoSave: false, you must call store.save() manually. When autoSave is a number, it debounces saves by that many milliseconds.sql:allow-execute must be added explicitly for INSERT/UPDATE/DELETEUint8Array (not strings) -- use TextEncoder/TextDecoder for string conversionCtrl+C) silently fails or overrides the system binding depending on the OS.plugin() call is independent.</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST complete ALL four installation steps for every plugin: 1) cargo add crate, 2) npm install bindings, 3) .plugin() registration in Rust, 4) permissions in capability file -- missing any step causes runtime errors)
(You MUST scope plugin permissions in capability files -- never grant unscoped fs:allow-read-text-file or http:default without URL restrictions)
(You MUST use @tauri-apps/plugin-* npm packages for JS bindings -- not @tauri-apps/api/* which is the core API)
(You MUST use #[cfg(desktop)] guard when registering desktop-only plugins -- mobile builds will fail otherwise)
(You MUST use tauri::plugin::Builder with an init() convention when creating custom plugins -- not raw command registration)
Failure to follow these rules will cause silent runtime errors, security vulnerabilities from unscoped permissions, or broken mobile builds.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events