.claude/skills/tauri-2/tauri-impl/tauri-impl-plugin-development/SKILL.md
Use when building custom Tauri 2 plugins, adding lifecycle hooks, or defining plugin permissions. Prevents incorrect plugin Builder chain ordering and missing build.rs permission generation. Covers plugin::Builder API, plugin commands and state, lifecycle hooks, mobile plugin development, and permission patterns. Keywords: tauri plugin development, plugin Builder, lifecycle hooks, on_event, on_navigation, build.rs, custom plugin.
npx skillsauth add OpenAEC-Foundation/OpenAEC-Workspace-Composer tauri-impl-plugin-developmentInstall 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.
| Method | Purpose | Example |
|--------|---------|---------|
| Builder::new("name") | Create plugin builder | Builder::new("my-plugin") |
| .invoke_handler() | Register plugin commands | .invoke_handler(generate_handler![cmd]) |
| .setup() | Plugin initialization hook | .setup(\|app, api\| { Ok(()) }) |
| .on_event() | App event handler | .on_event(\|app, event\| { }) |
| .on_navigation() | Webview navigation filter | .on_navigation(\|window, url\| true) |
| .on_webview_ready() | Webview ready hook | .on_webview_ready(\|window\| { }) |
| .on_drop() | Plugin cleanup hook | .on_drop(\|app\| { }) |
| .build() | Finalize and return TauriPlugin | .build() |
| Pattern | Example |
|---------|---------|
| Command prefix | plugin:<name>\|<command> |
| JS invoke call | invoke('plugin:my-plugin\|do_something', { arg: 'value' }) |
| Item | Description |
|------|-------------|
| build.rs location | Plugin crate root |
| Command list | const COMMANDS: &[&str] = &["cmd_a", "cmd_b"]; |
| Builder call | tauri_plugin::Builder::new(COMMANDS).build(); |
| Generated permissions | allow-cmd-a, deny-cmd-a, allow-cmd-b, deny-cmd-b |
NEVER use multiple .invoke_handler() calls on the app Builder -- only the last one takes effect. Plugin commands use their own .invoke_handler() on the plugin Builder.
NEVER forget the R: Runtime generic on plugin command functions -- plugin commands MUST use <R: Runtime> to remain runtime-agnostic.
NEVER register plugin state on the app Builder -- use app.manage() inside the plugin's .setup() hook.
NEVER skip build.rs permission generation -- without it, plugin commands will fail with "command not allowed" at runtime.
ALWAYS use the plugin:<name>|<command> prefix when invoking plugin commands from JavaScript.
ALWAYS return TauriPlugin<R> (or TauriPlugin<R, C> with config) from the plugin init function.
// src-tauri/src/plugins/my_plugin.rs (or separate crate)
use tauri::plugin::{Builder, TauriPlugin};
use tauri::Runtime;
#[tauri::command]
async fn do_something<R: Runtime>(
app: tauri::AppHandle<R>,
value: String,
) -> Result<String, String> {
Ok(format!("Processed: {}", value))
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("my-plugin")
.invoke_handler(tauri::generate_handler![do_something])
.build()
}
Register in the app:
// src-tauri/src/lib.rs
tauri::Builder::default()
.plugin(my_plugin::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
use std::sync::Mutex;
use tauri::plugin::{Builder, TauriPlugin};
use tauri::{Manager, Runtime};
#[derive(Default)]
struct PluginState {
count: u32,
}
#[tauri::command]
async fn increment<R: Runtime>(
app: tauri::AppHandle<R>,
) -> Result<u32, String> {
let state = app.state::<Mutex<PluginState>>();
let mut s = state.lock().unwrap();
s.count += 1;
Ok(s.count)
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("counter")
.invoke_handler(tauri::generate_handler![increment])
.setup(|app, _api| {
app.manage(Mutex::new(PluginState::default()));
Ok(())
})
.build()
}
Read configuration from tauri.conf.json under plugins.<name>:
use serde::Deserialize;
use tauri::plugin::{Builder, TauriPlugin};
use tauri::Runtime;
#[derive(Deserialize)]
struct PluginConfig {
timeout: u64,
api_key: String,
}
pub fn init<R: Runtime>() -> TauriPlugin<R, PluginConfig> {
Builder::<R, PluginConfig>::new("my-plugin")
.setup(|app, api| {
let config = api.config();
println!("Configured timeout: {}", config.timeout);
Ok(())
})
.build()
}
Configuration in tauri.conf.json:
{
"plugins": {
"my-plugin": {
"timeout": 30,
"api_key": "abc123"
}
}
}
use tauri::plugin::Builder;
use tauri::{RunEvent, Runtime};
pub fn init<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
Builder::new("lifecycle-demo")
.setup(|app, _api| {
// Runs once during plugin initialization
// Use for state registration, background tasks
Ok(())
})
.on_navigation(|window, url| {
// Return false to BLOCK navigation
// Return true to ALLOW navigation
url.scheme() != "forbidden"
})
.on_webview_ready(|window| {
// Runs when a webview becomes ready
// Use for per-window setup, listeners
})
.on_event(|app, event| {
match event {
RunEvent::ExitRequested { api, .. } => {
// Optionally prevent exit
// api.prevent_exit();
}
RunEvent::Exit => {
// Final cleanup before process ends
}
_ => {}
}
})
.on_drop(|app| {
// Runs when plugin is destroyed
// Use for resource cleanup
})
.build()
}
For plugins distributed as separate crates, auto-generate permissions:
// build.rs (in the plugin crate root)
const COMMANDS: &[&str] = &["do_something", "get_status", "configure"];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();
}
This generates permissions like allow-do-something, deny-do-something etc. Reference them in capability files:
{
"identifier": "default",
"windows": ["main"],
"permissions": [
"my-plugin:allow-do-something",
"my-plugin:allow-get-status"
]
}
Access command-level and global-level scopes in plugin commands:
use tauri::ipc::{CommandScope, GlobalScope};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct ScopeEntry {
path: String,
}
#[tauri::command]
async fn scoped_read<R: tauri::Runtime>(
command_scope: CommandScope<'_, ScopeEntry>,
global_scope: GlobalScope<'_, ScopeEntry>,
path: String,
) -> Result<String, String> {
let allowed = command_scope.allows();
let denied = command_scope.denies();
// Validate path against scope entries before proceeding
Ok("data".into())
}
For a standalone plugin crate:
tauri-plugin-my-plugin/
src/
lib.rs # Plugin init function, commands
commands.rs # Command implementations (optional split)
state.rs # Plugin state types (optional)
permissions/
default.toml # Default permission set
build.rs # Permission auto-generation
Cargo.toml
For an in-app plugin (simpler):
src-tauri/src/
plugins/
my_plugin.rs # Plugin init + commands
lib.rs # Registers plugin via .plugin()
development
Use when integrating Vite with a backend framework, rendering Vite assets from server-side templates, or setting up dev/production HTML serving. Prevents incorrect manifest.json traversal and missing CSS chunk resolution in production. Covers build.manifest configuration, .vite/manifest.json structure, ManifestChunk properties, dev mode HTML setup, production rendering, CSS/JS chunk resolution, and modulepreload polyfill. Keywords: backend integration, manifest.json, ManifestChunk, Django, Laravel, Rails, modulepreload.
development
Use when encountering dev server startup failures, HMR issues, proxy errors, CORS blocks, or module not found errors during development. Prevents misconfiguring server.hmr behind reverse proxies and forgetting appType: 'custom' in middleware mode. Covers HMR full-reload debugging, proxy configuration, CORS setup, HTTPS certificates, server.fs.strict violations, port conflicts, WebSocket failures, file watcher issues, and middleware mode. Keywords: dev server, HMR, proxy, CORS, HTTPS, WebSocket, port conflict, server.fs.strict, middleware mode, file watcher.
development
Use when encountering pre-bundling errors, dependency resolution failures, stale cache issues, or slow development server startup. Prevents excluding CJS dependencies from pre-bundling (which breaks runtime module resolution) and misconfiguring optimizeDeps. Covers CJS/ESM conversion failures, missing dependency auto-discovery, optimizeDeps configuration, monorepo linked dependencies, cache invalidation, browser cache staleness, and large dependency tree performance. Keywords: pre-bundling, optimizeDeps, CJS, ESM, cache, dependency resolution, monorepo, node_modules/.vite.
development
Use when encountering Vite build failures, chunk size warnings, or version-specific build errors. Prevents the common mistake of using deprecated rollupOptions in v8 or misconfiguring build targets and minifiers. Covers Rolldown/Rollup bundling failures, CSS minification errors, sourcemap problems, library mode build failures, BundleError handling, and asset processing errors. Keywords: build error, Rolldown, chunk size, sourcemap, library mode, minify, BundleError, rollupOptions, build.target.