← Catalog

tauri-plugins

Use when adding an official Tauri v2 plugin — picking the right plugin (fs/dialog/shell/http/store/notification/clipboard/global-shortcut/logging/os/opener/process/single-instance/autostart/deep-link/sql/websocket/upload/stronghold/cli), installing it (Cargo + npm), registering it in Rust, and granting the required capability permissions.

Tauri v2 official plugins

Tauri v2 splits most APIs out of core into versioned plugins under tauri-apps/plugins-workspace. Every plugin ships three pieces you must wire together: a Rust crate (tauri-plugin-<name>), a JS package (@tauri-apps/plugin-<name>), and a set of permission identifiers that must be granted in a capability file. Skipping the capability grant is the single most common cause of “not allowed” errors at runtime.

Read this skill alongside tauri-security (capabilities + scopes) and tauri-setup (project layout). The five categories below cover every official plugin in the v2 docs.

How to add any plugin (3 steps)

  1. Install both sides (run from project root):

    cd src-tauri && cargo add tauri-plugin-<name>
    bun add @tauri-apps/plugin-<name>     # or npm/pnpm/yarn

    Some mobile-capable plugins need cargo add --target 'cfg(any(target_os = "android", target_os = "ios"))' tauri-plugin-<name>.

  2. Register in Rustsrc-tauri/src/lib.rs:

    tauri::Builder::default()
        .plugin(tauri_plugin_<name>::init())
        .setup(|app| Ok(()))
        .run(tauri::generate_context!())
        .expect("error while running tauri application");

    A few plugins use a builder (Builder::default().build()) instead of init() — noted per-plugin below.

  3. Grant permissionssrc-tauri/capabilities/default.json:

    {
      "identifier": "default",
      "windows": ["main"],
      "permissions": [
        "core:default",
        "<plugin>:default",        // or finer-grained: "<plugin>:allow-<cmd>"
      ]
    }

    Most plugins ship <plugin>:default covering the common safe commands; anything destructive (write, execute, exit) requires opting in to a specific identifier.

See templates/lib.rs and templates/capabilities-plugins.json for a working multi-plugin setup.


Filesystem & data

fs — scoped filesystem access

Installcargo add tauri-plugin-fs · bun add @tauri-apps/plugin-fs
Rust.plugin(tauri_plugin_fs::init())
JSimport { readTextFile, writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'
Permsfs:default, plus opt-ins like fs:allow-read-text-file, fs:allow-write-text-file, fs:allow-app-write-recursive
ScopePath scopes go on the permission, not the plugin. Use $HOME, $APPDATA, $DOCUMENT, etc. placeholders in the capability "allow" array.

Scopes are mandatory for anything outside the app’s own data dir. Example fragment grants read access to $HOME/.config/myapp/**:

{ "identifier": "fs:allow-read-text-file",
  "allow": [{ "path": "$HOME/.config/myapp/**" }] }

store — JSON key-value persistence

Installcargo add tauri-plugin-store · bun add @tauri-apps/plugin-store
Rust.plugin(tauri_plugin_store::Builder::default().build())
JSconst store = await load('settings.json'); await store.set('k', v); await store.save();
Permsstore:default (covers get/set/save/load)
ScopeFiles live under the app data dir; no path scope needed.

sql — SQLite / MySQL / Postgres

Installcargo add tauri-plugin-sql --features sqlite (or mysql, postgres) · bun add @tauri-apps/plugin-sql
Rust.plugin(tauri_plugin_sql::Builder::default().build()) — optionally .add_migrations("sqlite:app.db", vec![...])
JSconst db = await Database.load('sqlite:app.db'); await db.execute(...);
Permssql:default, plus sql:allow-load, sql:allow-execute, sql:allow-select per command
ScopeConnection strings are NOT scoped by capabilities; validate in app code.

stronghold — encrypted secrets vault (IOTA Stronghold)

Installcargo add tauri-plugin-stronghold · bun add @tauri-apps/plugin-stronghold
Rust.plugin(tauri_plugin_stronghold::Builder::new(|password| { /* argon2 -> 32 bytes */ }).build())
JSconst stronghold = await Stronghold.load(path, pw); const client = await stronghold.loadClient('app');
Permsstronghold:default
NoteYou must supply the password-hash function in Rust; the JS side just supplies the password string.

persisted-scope — re-grant fs/asset scopes across restarts

Installcargo add tauri-plugin-persisted-scope
Rust.plugin(tauri_plugin_persisted_scope::init())
JSnone — purely persists runtime-extended scopes for fs and asset protocol.
Permsnone extra; piggybacks on fs permissions.

upload — multipart file upload/download with progress

Installcargo add tauri-plugin-upload · bun add @tauri-apps/plugin-upload
Rust.plugin(tauri_plugin_upload::init())
JSawait upload(url, filePath, ({progress, total}) => {...}, headers) and matching download(...)
Permsupload:default
ScopeURLs are unrestricted; pair with http scope discipline.

Shell & process

shell — spawn sidecars or whitelisted external commands

Installcargo add tauri-plugin-shell · bun add @tauri-apps/plugin-shell
Rust.plugin(tauri_plugin_shell::init())
JSCommand.create('node', ['-v']).execute() or Command.sidecar('binaries/my-cli')
Permsshell:default covers open. shell:allow-execute is scope-mandatory — must list commands and arg patterns.
Scope (capability){ "identifier": "shell:allow-execute", "allow": [{ "name": "node", "cmd": "node", "args": [{ "validator": "\\-v" }], "sidecar": false }] }

Sidecars must also be listed in tauri.conf.jsonbundle.externalBin.

opener — open URLs / files with the OS default handler

Installcargo add tauri-plugin-opener · bun add @tauri-apps/plugin-opener
Rust.plugin(tauri_plugin_opener::init())
JSawait openUrl('https://...'), await openPath('/path/to/file'), await revealItemInDir(path)
Permsopener:default, plus opener:allow-open-url, opener:allow-open-path, opener:allow-reveal-item-in-dir
NoteReplaces v1’s shell.open. Prefer opener over shell for “open in Finder/browser” — no execute capability required.

process — exit/relaunch the app

Installcargo add tauri-plugin-process · bun add @tauri-apps/plugin-process
Rust.plugin(tauri_plugin_process::init())
JSawait exit(0), await relaunch()
Permsprocess:default, or specifically process:allow-exit, process:allow-restart

single-instance — enforce one running instance

Installcargo add tauri-plugin-single-instance --features deep-link (feature optional)
Rust.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| { /* focus main */ })) — call FIRST in builder chain
JSnone
Permsnone — desktop-only, no commands exposed.

autostart — launch at OS login

Installcargo add tauri-plugin-autostart · bun add @tauri-apps/plugin-autostart
Rust.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec!["--flag"])))
JSawait enable(), await disable(), await isEnabled()
Permsautostart:default

cli — parse CLI args passed to the bundled binary

Installcargo add tauri-plugin-cli · bun add @tauri-apps/plugin-cli
Rust.plugin(tauri_plugin_cli::init()); declare args under plugins.cli in tauri.conf.json
JSconst matches = await getMatches(); matches.args.verbose.value
Permscli:default

UI feedback

dialog — file pickers, message/confirm/ask dialogs

Installcargo add tauri-plugin-dialog · bun add @tauri-apps/plugin-dialog
Rust.plugin(tauri_plugin_dialog::init())
JSawait open({ multiple: true, filters: [...] }), await save(...), await ask('Sure?'), await message('Hi')
Permsdialog:default — fine-grained: dialog:allow-open, dialog:allow-save, dialog:allow-message, dialog:allow-ask, dialog:allow-confirm

notification — OS notifications

Installcargo add tauri-plugin-notification · bun add @tauri-apps/plugin-notification
Rust.plugin(tauri_plugin_notification::init())
JSif (await isPermissionGranted() === false) await requestPermission(); sendNotification({ title, body })
Permsnotification:default
MobileAndroid needs the POST_NOTIFICATIONS runtime permission (handled by the JS request flow).

clipboard-manager — read/write clipboard text & images

Installcargo add tauri-plugin-clipboard-manager · bun add @tauri-apps/plugin-clipboard-manager
Rust.plugin(tauri_plugin_clipboard_manager::init())
JSawait writeText('hi'); const s = await readText(); also readImage()/writeImage(bytes)
Permsclipboard-manager:default (no read by default), then clipboard-manager:allow-read-text, allow-write-text, allow-read-image, allow-write-image

global-shortcut — system-wide hotkeys (desktop only)

Installcargo add tauri-plugin-global-shortcut · bun add @tauri-apps/plugin-global-shortcut
Rust.plugin(tauri_plugin_global_shortcut::Builder::new().build())
JSawait register('CmdOrCtrl+Shift+K', () => {...}), unregister, unregisterAll
Permsglobal-shortcut:default

Network

http — fetch-style HTTP client (no CORS, scoped)

Installcargo add tauri-plugin-http · bun add @tauri-apps/plugin-http
Rust.plugin(tauri_plugin_http::init())
JSimport { fetch } from '@tauri-apps/plugin-http'; const r = await fetch('https://api.example.com/x', { method: 'POST' })
Permshttp:default (covers fetch). Scope URLs in the capability.
Scope{ "identifier": "http:default", "allow": [{ "url": "https://api.example.com/*" }] }

websocket — outbound WebSocket client

Installcargo add tauri-plugin-websocket · bun add @tauri-apps/plugin-websocket
Rust.plugin(tauri_plugin_websocket::init())
JSconst ws = await WebSocket.connect('wss://...'); ws.addListener(msg => ...); ws.send('hi')
Permswebsocket:default

localhost — serve the frontend over http://localhost:<port> instead of tauri://

Installcargo add tauri-plugin-localhost
Rust.plugin(tauri_plugin_localhost::Builder::new(1430).build())
JSnone
Permsnone
CaveatLoses some of Tauri’s security model — only use when a third-party SDK demands a real http:// origin.

Platform info

os — platform / arch / version / hostname

Installcargo add tauri-plugin-os · bun add @tauri-apps/plugin-os
Rust.plugin(tauri_plugin_os::init())
JSplatform(), version(), arch(), hostname(), locale() (all sync after import)
Permsos:default (covers most getters); individual os:allow-platform, os:allow-version, etc. for tighter setups.

log — structured logging from JS + Rust to stdout/file/webview

Installcargo add tauri-plugin-log · bun add @tauri-apps/plugin-log
Rust.plugin(tauri_plugin_log::Builder::new().targets([Target::new(TargetKind::Stdout), Target::new(TargetKind::LogDir { file_name: None })]).build())
JSimport { info, warn, error } from '@tauri-apps/plugin-log'; await info('booted')
Permslog:default

Linking

Installcargo add tauri-plugin-deep-link · bun add @tauri-apps/plugin-deep-link
Rust.plugin(tauri_plugin_deep_link::init()); in setup, call `app.deep_link().on_open_url(\event\{ … })`
JSawait onOpenUrl(urls => ...), await getCurrent()
Permsdeep-link:default
ConfigMandatory: tauri.conf.jsonplugins.deep-link.desktop.schemes: ["myapp"] (Linux/Win) AND mobile schemes for iOS/Android. Tauri generates the macOS plist CFBundleURLTypes and Android intent filters from this — must rebuild after changing.

Common pitfalls

  • Forgot the capability grant. Most “not allowed by ACL” / “plugin command X not allowed” errors come from registering the plugin in Rust but never adding <plugin>:default (or a finer permission) to a capability file. Check src-tauri/capabilities/*.json.
  • shell:allow-execute without scope. The permission alone does nothing; you must list each allowed command + arg validator under "allow". Missing scope = silent no-op or rejected command.
  • fs paths. Capability scopes use placeholders ($HOME, $APPDATA, $DOCUMENT, $DOWNLOAD, $RESOURCE, $TEMP), not literal paths. Globs are ** for recursive. Without a scope entry, every fs call outside the app data dir fails.
  • Deep-link without manifest config. plugins.deep-link.desktop.schemes (and .mobile.schemes) in tauri.conf.json is required. macOS will silently ignore links until the bundle is rebuilt with the right CFBundleURLTypes; Android needs the intent filter regenerated.
  • single-instance plugin order. Register it as the FIRST plugin on the builder — later plugins running in the duplicate process can corrupt state before the duplicate exits.
  • Mobile-only vs desktop-only. global-shortcut, cli, single-instance, autostart, localhost are desktop-only. notification and deep-link need extra mobile setup.
  • opener vs shell. “Open this file in Finder” / “open URL in browser” should use opener, not shell. The shell plugin’s open is deprecated for that use case and pulls in the much more powerful (and more permission-hungry) execute machinery.
  • Plugin name skew. The crate is tauri-plugin-clipboard-manager, the npm package is @tauri-apps/plugin-clipboard-manager, the JS API surface is clipboard-manager permissions — not clipboard. Same kind of dash-suffix exists for global-shortcut and single-instance.

Templates

  • templates/lib.rstauri::Builder chain registering opener, dialog, fs, store, log, and notification.
  • templates/capabilities-plugins.json — matching capability file with the right <plugin>:default grants plus scoped fs:allow-read-text-file on $APPCONFIG/**.