developing-tauri-plugins
Guides the user through Tauri plugin development, including creating plugin extensions, configuring permissions, and building mobile plugins for iOS and Android platforms.
How do I install this agent skill?
npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill developing-tauri-pluginsIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill is a technical guide for developing Tauri plugins. It contains standard code snippets and commands for Rust, JavaScript, Kotlin, and Swift. No malicious patterns or security risks were identified.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
- Runlayerwarn
1/1 file flagged
What does this agent skill do?
Developing Tauri Plugins
Tauri plugins extend application functionality through modular Rust crates with optional JavaScript bindings and native mobile implementations.
Plugin Architecture
A complete plugin includes:
- Rust crate (
tauri-plugin-{name}) - Core logic - JavaScript bindings (
@scope/plugin-{name}) - NPM package - Android library (Kotlin) - Optional
- iOS package (Swift) - Optional
Creating a Plugin
npx @tauri-apps/cli plugin new my-plugin # Basic
npx @tauri-apps/cli plugin new my-plugin --android --ios # With mobile
npx @tauri-apps/cli plugin android add # Add to existing
npx @tauri-apps/cli plugin ios add
Project Structure
tauri-plugin-my-plugin/
├── src/
│ ├── lib.rs, commands.rs, desktop.rs, mobile.rs, error.rs
├── permissions/ # Permission TOML files
├── guest-js/index.ts # TypeScript API
├── android/, ios/ # Native mobile code
├── build.rs, Cargo.toml
Plugin Implementation
Main Plugin File (lib.rs)
use tauri::{plugin::{Builder, TauriPlugin}, Manager, Runtime};
mod commands;
mod error;
pub use error::{Error, Result};
#[cfg(desktop)] mod desktop;
#[cfg(mobile)] mod mobile;
#[cfg(desktop)] use desktop::MyPlugin;
#[cfg(mobile)] use mobile::MyPlugin;
pub struct MyPluginState<R: Runtime>(pub MyPlugin<R>);
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("my-plugin")
.invoke_handler(tauri::generate_handler![commands::do_something])
.setup(|app, api| {
app.manage(MyPluginState(MyPlugin::new(app, api)?));
Ok(())
})
.build()
}
Plugin with Configuration
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Config { pub timeout: Option<u64>, pub enabled: bool }
pub fn init<R: Runtime>() -> TauriPlugin<R, Config> {
Builder::<R, Config>::new("my-plugin")
.setup(|app, api| {
let config = api.config();
Ok(())
})
.build()
}
Commands (commands.rs)
use tauri::{command, ipc::Channel, Runtime, State};
use crate::{MyPluginState, Result};
#[command]
pub async fn do_something<R: Runtime>(
state: State<'_, MyPluginState<R>>, input: String,
) -> Result<String> {
state.0.do_something(input).await
}
#[command]
pub async fn upload<R: Runtime>(path: String, on_progress: Channel<u32>) -> Result<()> {
for i in 0..=100 { on_progress.send(i)?; }
Ok(())
}
Desktop Implementation (desktop.rs)
use tauri::{AppHandle, Runtime};
use crate::Result;
pub struct MyPlugin<R: Runtime> { app: AppHandle<R> }
impl<R: Runtime> MyPlugin<R> {
pub fn new(app: &AppHandle<R>, _api: tauri::plugin::PluginApi<R, ()>) -> Result<Self> {
Ok(Self { app: app.clone() })
}
pub async fn do_something(&self, input: String) -> Result<String> {
Ok(format!("Desktop: {}", input))
}
}
Mobile Implementation (mobile.rs)
use tauri::{AppHandle, Runtime};
use serde::{Deserialize, Serialize};
use crate::Result;
#[derive(Serialize)] struct MobileRequest { value: String }
#[derive(Deserialize)] struct MobileResponse { result: String }
pub struct MyPlugin<R: Runtime> { app: AppHandle<R> }
impl<R: Runtime> MyPlugin<R> {
pub fn new(app: &AppHandle<R>, _api: tauri::plugin::PluginApi<R, ()>) -> Result<Self> {
Ok(Self { app: app.clone() })
}
pub async fn do_something(&self, input: String) -> Result<String> {
let response: MobileResponse = self.app
.run_mobile_plugin("doSomething", MobileRequest { value: input })
.map_err(|e| crate::Error::Mobile(e.to_string()))?;
Ok(response.result)
}
}
Error Handling (error.rs)
use serde::{Serialize, Serializer};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("IO error: {0}")] Io(#[from] std::io::Error),
#[error("Mobile error: {0}")] Mobile(String),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where S: Serializer { serializer.serialize_str(self.to_string().as_str()) }
}
pub type Result<T> = std::result::Result<T, Error>;
Lifecycle Events
Builder::new("my-plugin")
.setup(|app, api| { Ok(()) }) // Plugin init
.on_navigation(|window, url| url.scheme() != "dangerous") // Block nav
.on_webview_ready(|window| {}) // Window created
.on_event(|app, event| { match event { tauri::RunEvent::Exit => {} _ => {} }})
.on_drop(|app| {}) // Cleanup
.build()
JavaScript Bindings (guest-js/index.ts)
import { invoke, Channel } from '@tauri-apps/api/core';
export async function doSomething(input: string): Promise<string> {
return invoke('plugin:my-plugin|do_something', { input });
}
export async function upload(path: string, onProgress: (p: number) => void): Promise<void> {
const channel = new Channel<number>();
channel.onmessage = onProgress;
return invoke('plugin:my-plugin|upload', { path, onProgress: channel });
}
Plugin Permissions
Permission File (permissions/default.toml)
[default]
description = "Default permissions"
permissions = ["allow-do-something"]
[[permission]]
identifier = "allow-do-something"
description = "Allows do_something command"
commands.allow = ["do_something"]
[[permission]]
identifier = "allow-upload"
description = "Allows upload command"
commands.allow = ["upload"]
[[set]]
identifier = "full-access"
description = "Full plugin access"
permissions = ["allow-do-something", "allow-upload"]
Build Script (build.rs)
const COMMANDS: &[&str] = &["do_something", "upload"];
fn main() { tauri_plugin::Builder::new(COMMANDS).build(); }
Scoped Permissions
use tauri::ipc::CommandScope;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct PathScope { pub path: String }
#[command]
pub async fn read_file(path: String, scope: CommandScope<'_, PathScope>) -> Result<String> {
let allowed = scope.allows().iter().any(|s| path.starts_with(&s.path));
let denied = scope.denies().iter().any(|s| path.starts_with(&s.path));
if denied || !allowed { return Err(Error::PermissionDenied); }
// Read file...
}
Android Plugin (Kotlin)
package com.example.myplugin
import android.app.Activity
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@InvokeArg
class DoSomethingArgs {
lateinit var value: String // Required
var optional: String? = null // Optional
var withDefault: Int = 42 // Default value
}
@TauriPlugin
class MyPlugin(private val activity: Activity) : Plugin(activity) {
@Command
fun doSomething(invoke: Invoke) {
val args = invoke.parseArgs(DoSomethingArgs::class.java)
CoroutineScope(Dispatchers.IO).launch { // Use IO for blocking ops
try {
invoke.resolve(JSObject().apply { put("result", "Android: ${args.value}") })
} catch (e: Exception) { invoke.reject(e.message) }
}
}
}
Android Permissions
@TauriPlugin(permissions = [
Permission(strings = [android.Manifest.permission.CAMERA], alias = "camera")
])
class MyPlugin(private val activity: Activity) : Plugin(activity) {
@Command override fun checkPermissions(invoke: Invoke) { super.checkPermissions(invoke) }
@Command override fun requestPermissions(invoke: Invoke) { super.requestPermissions(invoke) }
}
Android Events & JNI
// Emit event
trigger("dataReceived", JSObject().apply { put("data", "value") })
// Lifecycle
override fun onNewIntent(intent: Intent) {
trigger("newIntent", JSObject().apply { put("action", intent.action) })
}
// Call Rust via JNI
companion object { init { System.loadLibrary("my_plugin") } }
external fun processData(input: String): String // Java_com_example_myplugin_MyPlugin_processData
iOS Plugin (Swift)
import SwiftRs
import Tauri
import UIKit
class DoSomethingArgs: Decodable {
let value: String // Required
var optional: String? // Optional
}
class MyPlugin: Plugin {
@objc public func doSomething(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(DoSomethingArgs.self)
invoke.resolve(["result": "iOS: \(args.value)"])
}
}
@_cdecl("init_plugin_my_plugin")
func initPlugin() -> Plugin { return MyPlugin() }
iOS Permissions
import AVFoundation
class MyPlugin: Plugin {
@objc override func checkPermissions(_ invoke: Invoke) {
var result: [String: String] = [:]
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized: result["camera"] = "granted"
case .denied, .restricted: result["camera"] = "denied"
default: result["camera"] = "prompt"
}
invoke.resolve(result)
}
@objc override func requestPermissions(_ invoke: Invoke) {
AVCaptureDevice.requestAccess(for: .video) { _ in self.checkPermissions(invoke) }
}
}
iOS Events & FFI
// Emit event
trigger("dataReceived", data: ["data": "value"])
// Call Rust via FFI
@_silgen_name("process_data_ffi")
private static func processDataFFI(_ input: UnsafePointer<CChar>) -> UnsafeMutablePointer<CChar>?
@objc public func hybrid(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(DoSomethingArgs.self)
guard let ptr = MyPlugin.processDataFFI(args.value) else { invoke.reject("FFI failed"); return }
invoke.resolve(["result": String(cString: ptr)])
ptr.deallocate()
}
Using the Plugin
Register (src-tauri/src/lib.rs)
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_my_plugin::init())
.run(tauri::generate_context!())
.expect("error running application");
}
Configure (tauri.conf.json)
{ "plugins": { "my-plugin": { "timeout": 60, "enabled": true } } }
Permissions (capabilities/default.json)
{ "identifier": "default", "windows": ["main"], "permissions": ["my-plugin:default"] }
Frontend Usage
import { doSomething, upload } from '@myorg/plugin-my-plugin';
const result = await doSomething('hello');
await upload('/path/to/file', (p) => console.log(`${p}%`));
Best Practices
- Separate platform code in
desktop.rsandmobile.rs - Use
thiserrorfor structured error handling - Use async for I/O operations; request only necessary permissions
- Android: Commands run on main thread - use coroutines for blocking work
- iOS: Clean up FFI resources properly; use
invoke.reject()/invoke.resolve()
Android 16KB Page Size
For NDK < 28, add to .cargo/config.toml:
[target.aarch64-linux-android]
rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/dchuk/claude-code-tauri-skills/developing-tauri-plugins">View developing-tauri-plugins on skillZs</a>