Skip to main content
Technology & EngineeringTauri415 lines

Tauri Plugins

Tauri 2.0 plugin system including official plugins for filesystem, shell, dialog, notification, HTTP, clipboard, updater, and deep-link, plus community plugins and writing custom plugins.

Quick Summary27 lines
You are an expert in the Tauri 2.0 plugin ecosystem, including official plugins, community plugins, and building custom plugins for desktop and mobile applications.

## Key Points

- **Bypassing plugins with raw system calls**: Using `std::process::Command` directly instead of the shell plugin skips permission checks and platform abstraction.
- **Installing plugins without configuring capabilities**: Plugins do nothing without the corresponding permission grants in your capability files.
- **Granting wildcard permissions**: Using `"allow-*"` on plugins like `fs` defeats the security model. Grant only the specific permissions your app needs.
- **Reimplementing what official plugins provide**: Custom HTTP commands, file dialogs, or notification systems are worse than the maintained plugins.

## Quick Example

```bash
# Add the Rust dependency
cargo add tauri-plugin-fs

# Add the JavaScript bindings
npm install @tauri-apps/plugin-fs
```

```typescript
import { writeText, readText } from "@tauri-apps/plugin-clipboard-manager";

await writeText("Copied to clipboard!");
const text = await readText();
```
skilldb get tauri-skills/Tauri PluginsFull skill: 415 lines
Paste into your CLAUDE.md or agent config

Tauri Plugins — Extending Tauri Applications

You are an expert in the Tauri 2.0 plugin ecosystem, including official plugins, community plugins, and building custom plugins for desktop and mobile applications.

Core Philosophy

Tauri plugins are the sanctioned way to extend your app's capabilities beyond the core. Each plugin encapsulates a specific system capability -- file access, HTTP requests, notifications -- behind a permission boundary. The plugin system exists because Tauri deliberately ships with a minimal core. You opt into exactly the capabilities you need, which keeps your binary small and your attack surface narrow.

Prefer official plugins over reimplementing functionality in custom commands. The official plugins handle platform differences, implement the permission model correctly, and receive security updates. When you need something beyond what official plugins offer, check the community ecosystem before writing your own. Custom plugins make sense for app-specific system integrations or when wrapping a Rust crate for frontend access.

Anti-Patterns

  • Bypassing plugins with raw system calls: Using std::process::Command directly instead of the shell plugin skips permission checks and platform abstraction.
  • Installing plugins without configuring capabilities: Plugins do nothing without the corresponding permission grants in your capability files.
  • Granting wildcard permissions: Using "allow-*" on plugins like fs defeats the security model. Grant only the specific permissions your app needs.
  • Reimplementing what official plugins provide: Custom HTTP commands, file dialogs, or notification systems are worse than the maintained plugins.

Installing Plugins

Every plugin has a Rust crate and a JavaScript package:

# Add the Rust dependency
cargo add tauri-plugin-fs

# Add the JavaScript bindings
npm install @tauri-apps/plugin-fs

Register in your Rust setup:

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init())
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_shell::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Capabilities and Permissions

Each plugin requires explicit permissions in src-tauri/capabilities/default.json:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Default capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:default",
    "fs:allow-read",
    "fs:allow-write",
    "dialog:allow-open",
    "dialog:allow-save",
    "shell:allow-open",
    "notification:default"
  ]
}

Scoped Permissions

{
  "permissions": [
    {
      "identifier": "fs:allow-read",
      "allow": [
        { "path": "$APPDATA/**" },
        { "path": "$DOCUMENT/**" }
      ]
    },
    {
      "identifier": "fs:allow-write",
      "allow": [
        { "path": "$APPDATA/**" }
      ]
    }
  ]
}

Official Plugins

Filesystem (tauri-plugin-fs)

import { readTextFile, writeTextFile, readDir, exists, mkdir } from "@tauri-apps/plugin-fs";
import { appDataDir, join } from "@tauri-apps/api/path";

// Read a file
const dataDir = await appDataDir();
const configPath = await join(dataDir, "config.json");
const content = await readTextFile(configPath);

// Write a file
await writeTextFile(configPath, JSON.stringify({ theme: "dark" }));

// List directory contents
const entries = await readDir(dataDir);
for (const entry of entries) {
  console.log(entry.name, entry.isDirectory);
}

// Create directories
const logsDir = await join(dataDir, "logs");
if (!(await exists(logsDir))) {
  await mkdir(logsDir, { recursive: true });
}

Dialog (tauri-plugin-dialog)

import { open, save, message, ask, confirm } from "@tauri-apps/plugin-dialog";

// Open file picker
const selected = await open({
  multiple: false,
  filters: [
    { name: "Images", extensions: ["png", "jpg", "gif"] },
    { name: "All Files", extensions: ["*"] },
  ],
});
if (selected) {
  console.log("Selected:", selected);
}

// Save file dialog
const savePath = await save({
  defaultPath: "document.txt",
  filters: [{ name: "Text", extensions: ["txt"] }],
});

// Message dialog
await message("Operation complete!", { title: "Success", kind: "info" });

// Confirmation dialog
const confirmed = await confirm("Delete this item?", {
  title: "Confirm Deletion",
  kind: "warning",
});

Shell (tauri-plugin-shell)

import { Command } from "@tauri-apps/plugin-shell";

// Execute a sidecar or allowed command
const output = await Command.create("echo", ["hello"]).execute();
console.log(output.stdout);

// Streaming output
const cmd = Command.create("long-running-process");
cmd.on("close", (data) => console.log("Exited with code:", data.code));
cmd.stdout.on("data", (line) => console.log("stdout:", line));
cmd.stderr.on("data", (line) => console.error("stderr:", line));
const child = await cmd.spawn();

// Open URL in default browser
import { open as shellOpen } from "@tauri-apps/plugin-shell";
await shellOpen("https://tauri.app");

Shell commands must be allowlisted in tauri.conf.json:

{
  "plugins": {
    "shell": {
      "open": true,
      "scope": [
        {
          "name": "echo",
          "cmd": "echo",
          "args": true
        }
      ]
    }
  }
}

HTTP (tauri-plugin-http)

import { fetch } from "@tauri-apps/plugin-http";

const response = await fetch("https://api.example.com/data", {
  method: "GET",
  headers: {
    Authorization: "Bearer token123",
  },
});
const data = await response.json();

// POST with body
const postResponse = await fetch("https://api.example.com/items", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "New Item" }),
});

Notification (tauri-plugin-notification)

import {
  isPermissionGranted,
  requestPermission,
  sendNotification,
} from "@tauri-apps/plugin-notification";

let permission = await isPermissionGranted();
if (!permission) {
  const result = await requestPermission();
  permission = result === "granted";
}

if (permission) {
  sendNotification({
    title: "Download Complete",
    body: "Your file has been downloaded successfully.",
  });
}

Clipboard (tauri-plugin-clipboard-manager)

import { writeText, readText } from "@tauri-apps/plugin-clipboard-manager";

await writeText("Copied to clipboard!");
const text = await readText();

Updater (tauri-plugin-updater)

import { check } from "@tauri-apps/plugin-updater";

const update = await check();
if (update) {
  console.log(`Update available: ${update.version}`);
  await update.downloadAndInstall();
  // Optionally restart
  import { relaunch } from "@tauri-apps/plugin-process";
  await relaunch();
}

Deep Link (tauri-plugin-deep-link)

// src-tauri/src/lib.rs
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_deep_link::init())
        .setup(|app| {
            #[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
            {
                use tauri_plugin_deep_link::DeepLinkExt;
                app.deep_link().register_all()?;
            }
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { onOpenUrl } from "@tauri-apps/plugin-deep-link";

await onOpenUrl((urls) => {
  console.log("Deep link opened:", urls);
  // Handle myapp://path/to/resource
});

Writing a Custom Plugin

Plugin Structure

tauri-plugin-myfeature/
├── Cargo.toml
├── src/
│   ├── lib.rs           # Plugin entry point
│   ├── commands.rs      # Tauri commands
│   ├── error.rs         # Error types
│   └── models.rs        # Data types
├── guest-js/            # TypeScript bindings
│   └── index.ts
├── permissions/         # Permission definitions
│   ├── default.toml
│   └── autogenerated/
├── package.json
└── build.rs

Rust Implementation

// src/lib.rs
use tauri::plugin::{Builder, TauriPlugin};
use tauri::{Manager, Runtime};

mod commands;
mod error;

pub use error::Error;

pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("myfeature")
        .invoke_handler(tauri::generate_handler![
            commands::do_something,
            commands::get_status,
        ])
        .setup(|app, api| {
            // Initialize plugin state
            app.manage(MyFeatureState::default());
            Ok(())
        })
        .build()
}

// src/commands.rs
use tauri::{command, State};

#[derive(Default)]
pub struct MyFeatureState {
    pub initialized: std::sync::Mutex<bool>,
}

#[command]
pub async fn do_something(state: State<'_, MyFeatureState>) -> Result<String, crate::Error> {
    let mut init = state.initialized.lock().unwrap();
    *init = true;
    Ok("Feature activated".into())
}

#[command]
pub fn get_status(state: State<'_, MyFeatureState>) -> bool {
    *state.initialized.lock().unwrap()
}

// src/error.rs
use serde::Serialize;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Feature not initialized")]
    NotInitialized,
    #[error("Internal error: {0}")]
    Internal(String),
}

impl Serialize for Error {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

TypeScript Bindings

// guest-js/index.ts
import { invoke } from "@tauri-apps/api/core";

export async function doSomething(): Promise<string> {
  return invoke<string>("plugin:myfeature|do_something");
}

export async function getStatus(): Promise<boolean> {
  return invoke<boolean>("plugin:myfeature|get_status");
}

Permission Definitions

# permissions/default.toml
[default]
description = "Default permissions for myfeature plugin"
permissions = ["allow-do-something", "allow-get-status"]

Using the Custom Plugin

// In the consuming app
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_myfeature::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Install this skill directly: skilldb add tauri-skills

Get CLI access →