1---2name: tauri-plugins3description: 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.4---56# Tauri v2 official plugins78Tauri v2 splits most APIs out of core into versioned plugins under9`tauri-apps/plugins-workspace`. Every plugin ships three pieces you must10wire together: a Rust crate (`tauri-plugin-<name>`), a JS package11(`@tauri-apps/plugin-<name>`), and a set of permission identifiers that12must be granted in a capability file. Skipping the capability grant is the13single most common cause of "not allowed" errors at runtime.1415Read this skill alongside `tauri-security` (capabilities + scopes) and16`tauri-setup` (project layout). The five categories below cover every17official plugin in the v2 docs.1819## How to add any plugin (3 steps)20211. **Install both sides** (run from project root):2223 ```sh24 cd src-tauri && cargo add tauri-plugin-<name>25 bun add @tauri-apps/plugin-<name> # or npm/pnpm/yarn26 ```2728 Some mobile-capable plugins need `cargo add --target 'cfg(any(target_os = "android", target_os =29 "ios"))' tauri-plugin-<name>`.30312. **Register in Rust** — `src-tauri/src/lib.rs`:3233 ```rust34 tauri::Builder::default()35 .plugin(tauri_plugin_<name>::init())36 .setup(|app| Ok(()))37 .run(tauri::generate_context!())38 .expect("error while running tauri application");39 ```4041 A few plugins use a builder (`Builder::default().build()`) instead of42 `init()` — noted per-plugin below.43443. **Grant permissions** — `src-tauri/capabilities/default.json`:4546 ```jsonc47 {48 "identifier": "default",49 "windows": ["main"],50 "permissions": [51 "core:default",52 "<plugin>:default", // or finer-grained: "<plugin>:allow-<cmd>"53 ]54 }55 ```5657 Most plugins ship `<plugin>:default` covering the common safe commands;58 anything destructive (write, execute, exit) requires opting in to a59 specific identifier.6061See `templates/lib.rs` and `templates/capabilities-plugins.json` for a62working multi-plugin setup.6364---6566## Filesystem & data6768### `fs` — scoped filesystem access6970| | |71| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |72| Install | `cargo add tauri-plugin-fs` · `bun add @tauri-apps/plugin-fs` |73| Rust | `.plugin(tauri_plugin_fs::init())` |74| JS | `import { readTextFile, writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs'` |75| Perms | `fs:default`, plus opt-ins like `fs:allow-read-text-file`, `fs:allow-write-text-file`, `fs:allow-app-write-recursive` |76| Scope | Path scopes go on the *permission*, not the plugin. Use `$HOME`, `$APPDATA`, `$DOCUMENT`, etc. placeholders in the capability `"allow"` array. |7778Scopes are mandatory for anything outside the app's own data dir. Example79fragment grants read access to `$HOME/.config/myapp/**`:8081```jsonc82{ "identifier": "fs:allow-read-text-file",83 "allow": [{ "path": "$HOME/.config/myapp/**" }] }84```8586### `store` — JSON key-value persistence8788| | |89| ------- | ----------------------------------------------------------------------------------------- |90| Install | `cargo add tauri-plugin-store` · `bun add @tauri-apps/plugin-store` |91| Rust | `.plugin(tauri_plugin_store::Builder::default().build())` |92| JS | `const store = await load('settings.json'); await store.set('k', v); await store.save();` |93| Perms | `store:default` (covers get/set/save/load) |94| Scope | Files live under the app data dir; no path scope needed. |9596### `sql` — SQLite / MySQL / Postgres9798| | |99| ------- | ------------------------------------------------------------------------------------------------------------------ |100| Install | `cargo add tauri-plugin-sql --features sqlite` (or `mysql`, `postgres`) · `bun add @tauri-apps/plugin-sql` |101| Rust | `.plugin(tauri_plugin_sql::Builder::default().build())` — optionally `.add_migrations("sqlite:app.db", vec![...])` |102| JS | `const db = await Database.load('sqlite:app.db'); await db.execute(...);` |103| Perms | `sql:default`, plus `sql:allow-load`, `sql:allow-execute`, `sql:allow-select` per command |104| Scope | Connection strings are NOT scoped by capabilities; validate in app code. |105106### `stronghold` — encrypted secrets vault (IOTA Stronghold)107108| | |109| ------- | -------------------------------------------------------------------------------------------------------- |110| Install | `cargo add tauri-plugin-stronghold` · `bun add @tauri-apps/plugin-stronghold` |111| Rust | `.plugin(tauri_plugin_stronghold::Builder::new(\|password\| { /* argon2 -> 32 bytes */ }).build())` |112| JS | `const stronghold = await Stronghold.load(path, pw); const client = await stronghold.loadClient('app');` |113| Perms | `stronghold:default` |114| Note | You must supply the password-hash function in Rust; the JS side just supplies the password string. |115116### `persisted-scope` — re-grant fs/asset scopes across restarts117118| | |119| ------- | ----------------------------------------------------------------------------- |120| Install | `cargo add tauri-plugin-persisted-scope` |121| Rust | `.plugin(tauri_plugin_persisted_scope::init())` |122| JS | none — purely persists runtime-extended scopes for `fs` and `asset` protocol. |123| Perms | none extra; piggybacks on `fs` permissions. |124125### `upload` — multipart file upload/download with progress126127| | |128| ------- | ------------------------------------------------------------------------------------------------- |129| Install | `cargo add tauri-plugin-upload` · `bun add @tauri-apps/plugin-upload` |130| Rust | `.plugin(tauri_plugin_upload::init())` |131| JS | `await upload(url, filePath, ({progress, total}) => {...}, headers)` and matching `download(...)` |132| Perms | `upload:default` |133| Scope | URLs are unrestricted; pair with `http` scope discipline. |134135---136137## Shell & process138139### `shell` — spawn sidecars or whitelisted external commands140141| | |142| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |143| Install | `cargo add tauri-plugin-shell` · `bun add @tauri-apps/plugin-shell` |144| Rust | `.plugin(tauri_plugin_shell::init())` |145| JS | `Command.create('node', ['-v']).execute()` or `Command.sidecar('binaries/my-cli')` |146| Perms | `shell:default` covers `open`. `shell:allow-execute` is **scope-mandatory** — must list commands and arg patterns. |147| Scope (capability) | `{ "identifier": "shell:allow-execute", "allow": [{ "name": "node", "cmd": "node", "args": [{ "validator": "\\-v" }], "sidecar": false }] }` |148149Sidecars must also be listed in `tauri.conf.json` → `bundle.externalBin`.150151### `opener` — open URLs / files with the OS default handler152153| | |154| ------- | ----------------------------------------------------------------------------------------------------------------------- |155| Install | `cargo add tauri-plugin-opener` · `bun add @tauri-apps/plugin-opener` |156| Rust | `.plugin(tauri_plugin_opener::init())` |157| JS | `await openUrl('https://...')`, `await openPath('/path/to/file')`, `await revealItemInDir(path)` |158| Perms | `opener:default`, plus `opener:allow-open-url`, `opener:allow-open-path`, `opener:allow-reveal-item-in-dir` |159| Note | Replaces v1's `shell.open`. Prefer `opener` over `shell` for "open in Finder/browser" — no execute capability required. |160161### `process` — exit/relaunch the app162163| | |164| ------- | -------------------------------------------------------------------------------- |165| Install | `cargo add tauri-plugin-process` · `bun add @tauri-apps/plugin-process` |166| Rust | `.plugin(tauri_plugin_process::init())` |167| JS | `await exit(0)`, `await relaunch()` |168| Perms | `process:default`, or specifically `process:allow-exit`, `process:allow-restart` |169170### `single-instance` — enforce one running instance171172| | |173| ------- | --------------------------------------------------------------------------------------------------------------------------- |174| Install | `cargo add tauri-plugin-single-instance --features deep-link` (feature optional) |175| Rust | `.plugin(tauri_plugin_single_instance::init(\|app, argv, cwd\| { /* focus main */ }))` — call FIRST in builder chain |176| JS | none |177| Perms | none — desktop-only, no commands exposed. |178179### `autostart` — launch at OS login180181| | |182| ------- | ----------------------------------------------------------------------------------------- |183| Install | `cargo add tauri-plugin-autostart` · `bun add @tauri-apps/plugin-autostart` |184| Rust | `.plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, Some(vec!["--flag"])))` |185| JS | `await enable()`, `await disable()`, `await isEnabled()` |186| Perms | `autostart:default` |187188### `cli` — parse CLI args passed to the bundled binary189190| | |191| ------- | ------------------------------------------------------------------------------------------ |192| Install | `cargo add tauri-plugin-cli` · `bun add @tauri-apps/plugin-cli` |193| Rust | `.plugin(tauri_plugin_cli::init())`; declare args under `plugins.cli` in `tauri.conf.json` |194| JS | `const matches = await getMatches(); matches.args.verbose.value` |195| Perms | `cli:default` |196197---198199## UI feedback200201### `dialog` — file pickers, message/confirm/ask dialogs202203| | |204| ------- | --------------------------------------------------------------------------------------------------------------------------------------------- |205| Install | `cargo add tauri-plugin-dialog` · `bun add @tauri-apps/plugin-dialog` |206| Rust | `.plugin(tauri_plugin_dialog::init())` |207| JS | `await open({ multiple: true, filters: [...] })`, `await save(...)`, `await ask('Sure?')`, `await message('Hi')` |208| Perms | `dialog:default` — fine-grained: `dialog:allow-open`, `dialog:allow-save`, `dialog:allow-message`, `dialog:allow-ask`, `dialog:allow-confirm` |209210### `notification` — OS notifications211212| | |213| ------- | --------------------------------------------------------------------------------------------------------- |214| Install | `cargo add tauri-plugin-notification` · `bun add @tauri-apps/plugin-notification` |215| Rust | `.plugin(tauri_plugin_notification::init())` |216| JS | `if (await isPermissionGranted() === false) await requestPermission(); sendNotification({ title, body })` |217| Perms | `notification:default` |218| Mobile | Android needs the `POST_NOTIFICATIONS` runtime permission (handled by the JS request flow). |219220### `clipboard-manager` — read/write clipboard text & images221222| | |223| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |224| Install | `cargo add tauri-plugin-clipboard-manager` · `bun add @tauri-apps/plugin-clipboard-manager` |225| Rust | `.plugin(tauri_plugin_clipboard_manager::init())` |226| JS | `await writeText('hi'); const s = await readText();` also `readImage()/writeImage(bytes)` |227| Perms | `clipboard-manager:default` (no read by default), then `clipboard-manager:allow-read-text`, `allow-write-text`, `allow-read-image`, `allow-write-image` |228229### `global-shortcut` — system-wide hotkeys (desktop only)230231| | |232| ------- | --------------------------------------------------------------------------------------- |233| Install | `cargo add tauri-plugin-global-shortcut` · `bun add @tauri-apps/plugin-global-shortcut` |234| Rust | `.plugin(tauri_plugin_global_shortcut::Builder::new().build())` |235| JS | `await register('CmdOrCtrl+Shift+K', () => {...})`, `unregister`, `unregisterAll` |236| Perms | `global-shortcut:default` |237238---239240## Network241242### `http` — fetch-style HTTP client (no CORS, scoped)243244| | |245| ------- | ------------------------------------------------------------------------------------------------------------------------- |246| Install | `cargo add tauri-plugin-http` · `bun add @tauri-apps/plugin-http` |247| Rust | `.plugin(tauri_plugin_http::init())` |248| JS | `import { fetch } from '@tauri-apps/plugin-http'; const r = await fetch('https://api.example.com/x', { method: 'POST' })` |249| Perms | `http:default` (covers `fetch`). Scope URLs in the capability. |250| Scope | `{ "identifier": "http:default", "allow": [{ "url": "https://api.example.com/*" }] }` |251252### `websocket` — outbound WebSocket client253254| | |255| ------- | -------------------------------------------------------------------------------------------- |256| Install | `cargo add tauri-plugin-websocket` · `bun add @tauri-apps/plugin-websocket` |257| Rust | `.plugin(tauri_plugin_websocket::init())` |258| JS | `const ws = await WebSocket.connect('wss://...'); ws.addListener(msg => ...); ws.send('hi')` |259| Perms | `websocket:default` |260261### `localhost` — serve the frontend over `http://localhost:<port>` instead of `tauri://`262263| | |264| ------- | ------------------------------------------------------------------------------------------------------- |265| Install | `cargo add tauri-plugin-localhost` |266| Rust | `.plugin(tauri_plugin_localhost::Builder::new(1430).build())` |267| JS | none |268| Perms | none |269| Caveat | Loses some of Tauri's security model — only use when a third-party SDK demands a real `http://` origin. |270271---272273## Platform info274275### `os` — platform / arch / version / hostname276277| | |278| ------- | ---------------------------------------------------------------------------------------------------------------- |279| Install | `cargo add tauri-plugin-os` · `bun add @tauri-apps/plugin-os` |280| Rust | `.plugin(tauri_plugin_os::init())` |281| JS | `platform()`, `version()`, `arch()`, `hostname()`, `locale()` (all sync after import) |282| Perms | `os:default` (covers most getters); individual `os:allow-platform`, `os:allow-version`, etc. for tighter setups. |283284### `log` — structured logging from JS + Rust to stdout/file/webview285286| | |287| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |288| Install | `cargo add tauri-plugin-log` · `bun add @tauri-apps/plugin-log` |289| Rust | `.plugin(tauri_plugin_log::Builder::new().targets([Target::new(TargetKind::Stdout), Target::new(TargetKind::LogDir { file_name: None })]).build())` |290| JS | `import { info, warn, error } from '@tauri-apps/plugin-log'; await info('booted')` |291| Perms | `log:default` |292293---294295## Linking296297### `deep-link` — receive `myapp://...` URLs from OS298299| | | | |300| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | --------- |301| Install | `cargo add tauri-plugin-deep-link` · `bun add @tauri-apps/plugin-deep-link` | | |302| Rust | `.plugin(tauri_plugin_deep_link::init())`; in `setup`, call `app.deep_link().on_open_url(\ | event\ | { ... })` |303| JS | `await onOpenUrl(urls => ...)`, `await getCurrent()` | | |304| Perms | `deep-link:default` | | |305| Config | Mandatory: `tauri.conf.json` → `plugins.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. | | |306307---308309## Common pitfalls310311- **Forgot the capability grant.** Most "not allowed by ACL" / "plugin command X not allowed" errors312 come from registering the plugin in Rust but never adding `<plugin>:default` (or a finer313 permission) to a capability file. Check `src-tauri/capabilities/*.json`.314- **`shell:allow-execute` without scope.** The permission alone does nothing; you must list each315 allowed command + arg validator under `"allow"`. Missing scope = silent no-op or rejected command.316- **fs paths.** Capability scopes use placeholders (`$HOME`, `$APPDATA`, `$DOCUMENT`, `$DOWNLOAD`,317 `$RESOURCE`, `$TEMP`), not literal paths. Globs are `**` for recursive. Without a scope entry,318 every `fs` call outside the app data dir fails.319- **Deep-link without manifest config.** `plugins.deep-link.desktop.schemes` (and `.mobile.schemes`)320 in `tauri.conf.json` is required. macOS will silently ignore links until the bundle is rebuilt321 with the right `CFBundleURLTypes`; Android needs the intent filter regenerated.322- **`single-instance` plugin order.** Register it as the FIRST plugin on the builder — later plugins323 running in the duplicate process can corrupt state before the duplicate exits.324- **Mobile-only vs desktop-only.** `global-shortcut`, `cli`, `single-instance`, `autostart`,325 `localhost` are desktop-only. `notification` and `deep-link` need extra mobile setup.326- **`opener` vs `shell`.** "Open this file in Finder" / "open URL in browser" should use `opener`,327 not `shell`. The `shell` plugin's `open` is deprecated for that use case and pulls in the much328 more powerful (and more permission-hungry) execute machinery.329- **Plugin name skew.** The crate is `tauri-plugin-clipboard-manager`, the npm package is330 `@tauri-apps/plugin-clipboard-manager`, the JS API surface is `clipboard-manager` permissions —331 not `clipboard`. Same kind of dash-suffix exists for `global-shortcut` and `single-instance`.332333## Templates334335- `templates/lib.rs` — `tauri::Builder` chain registering opener, dialog, fs, store, log, and336 notification.337- `templates/capabilities-plugins.json` — matching capability file with the right `<plugin>:default`338 grants plus scoped `fs:allow-read-text-file` on `$APPCONFIG/**`.