Fix: Tauri 2 Not Working — Capabilities, Plugin Permissions, Mobile Build, and v1 Migration
Quick Answer
How to fix Tauri 2 errors — invoke command not allowed by capabilities, plugin permission missing, tauri.conf.json schema, mobile init/build failures, updater migration, and v1 allowlist conversion.
The Error
You upgrade to Tauri 2 and a working invoke call from the frontend fails:
import { invoke } from "@tauri-apps/api/core";
await invoke("greet", { name: "Alice" });
// Error: Command greet not allowed by ACLOr you add the dialog plugin and it refuses to open:
import { open } from "@tauri-apps/plugin-dialog";
const path = await open();
// Error: dialog.open not allowed. Required: dialog:defaultOr cargo tauri android init fails on a fresh project:
Error: NDK not found. Set ANDROID_NDK_HOME or install via Android Studio.Or your v1 tauri.conf.json doesn’t validate:
Error parsing tauri.conf.json:
unknown field `allowlist`, expected one of `productName`, `version`, `identifier`, ...Why This Happens
Tauri 2 introduced a permissions-and-capabilities model that replaces v1’s allowlist. Every command (built-in or custom) and every plugin call must be explicitly permitted, scoped to specific windows/webviews. Three sources of friction:
- Capabilities are required. v1’s “expose everything by default in dev, lock down in prod” pattern is gone. Tauri 2 requires you to grant explicit permissions in capability files. Missing permissions → command blocked, even in dev.
- Plugin permissions are separate from app permissions. Every plugin (
dialog,fs,shell,updater,notification) ships its own permission set. Adding a plugin toCargo.tomldoesn’t grant permission to call it — you also add it to a capability. - Mobile is a separate build target.
cargo tauri buildproduces desktop binaries; mobile needscargo tauri android/cargo tauri ios. Toolchain setup (NDK, Xcode) trips up most first-time mobile builds. - Config schema is different.
tauri.conf.jsonwas completely restructured.allowlistis gone;bundle,app,buildare flatter. Old configs don’t parse.
Fix 1: Create a Capability File
Capabilities live in src-tauri/capabilities/. Create default.json:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability granted to the main window",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:default",
"core:webview:default",
"core:event:default",
"dialog:default",
"fs:default",
"shell:default"
]
}Reference it in tauri.conf.json:
{
"app": {
"security": {
"csp": null
},
"windows": [{ "label": "main", "title": "My App" }]
}
}The capabilities/ directory is auto-loaded — you don’t list capability files in tauri.conf.json. They apply by matching the windows field.
To grant a permission to all windows:
{
"identifier": "all",
"windows": ["*"],
"permissions": [...]
}Pro Tip: Split capabilities by feature. default.json for the main window, admin.json scoped to an admin window with broader permissions. Reduces the risk of widening one window’s surface accidentally.
Fix 2: Add Plugin Permissions
When you add a plugin to Cargo.toml:
[dependencies]
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
tauri-plugin-shell = "2"And register it in src-tauri/src/lib.rs:
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}You also need to grant the plugin’s permissions in a capability. The plugin docs list available permission identifiers:
{
"identifier": "default",
"windows": ["main"],
"permissions": [
"dialog:default",
"dialog:allow-open",
"dialog:allow-save",
"fs:default",
"fs:allow-read-file",
"fs:allow-write-file",
"shell:default",
"shell:allow-open"
]
}For finer-grained scopes (e.g. only read files under a specific directory):
{
"identifier": "fs:scope-app",
"allow": [{ "path": "$APPDATA/*" }],
"deny": [{ "path": "$APPDATA/secrets/*" }]
}The $APPDATA, $DOCUMENT, $HOME placeholders resolve to platform-appropriate paths at runtime.
Common Mistake: Adding tauri-plugin-fs to Cargo.toml but forgetting permissions: ["fs:default"] in the capability. The plugin loads, the API is callable, but every call returns “not allowed by ACL.”
Fix 3: Custom Commands Need Permissions Too
A #[tauri::command] function isn’t automatically callable from the frontend — you must register it as a permission in your app’s manifest:
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}For the command to be callable from the frontend, add it to the capability’s permissions. The identifier is <plugin-or-app>:allow-<command-name>. For app-level commands, the convention is to reference them via the app’s permission set generated under src-tauri/permissions/:
// src-tauri/permissions/default.toml
"$schema" = "schemas/schema.json"
[default]
description = "Allow app commands"
permissions = ["allow-greet"]Then in the capability:
"permissions": [
"core:default",
"<your-app-identifier>:allow-greet"
]The exact identifier depends on your app’s bundle identifier. Run cargo tauri build once to generate the schemas and inspect src-tauri/gen/schemas/.
Pro Tip: Start with broad core:default while developing, then narrow as you go. Don’t try to write a tight ACL on day one — you’ll fight the system instead of building features.
Fix 4: Migrate tauri.conf.json Schema
v1 → v2 config structure changed significantly:
// v1 (does NOT work in v2):
{
"tauri": {
"allowlist": {
"all": false,
"dialog": { "open": true }
},
"windows": [{ "title": "..." }],
"bundle": { ... }
}
}
// v2:
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"productName": "My App",
"version": "0.1.0",
"identifier": "com.example.myapp",
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devUrl": "http://localhost:5173",
"frontendDist": "../dist"
},
"app": {
"windows": [{ "title": "My App", "width": 1200, "height": 800 }],
"security": { "csp": null }
},
"bundle": {
"active": true,
"targets": "all",
"icon": ["icons/icon.icns", "icons/icon.ico"]
}
}Key changes:
- Top-level
tauri.*is gone. Sections moved to top-level:app,build,bundle. allowlistremoved — replaced bycapabilities/*.jsonfiles (see Fix 1).devPath→devUrl;distDir→frontendDist.identifier(bundle ID likecom.example.myapp) is now required at the top level.
The official migrate tool handles most of this. Install the Tauri 2 CLI, then run migrate from your project root:
# Via Cargo
cargo install tauri-cli --version "^2.0.0" --locked
# Or via npm (recommended for JS-heavy projects)
npm install -D @tauri-apps/cli@latest
cargo tauri migrate
# or: npx @tauri-apps/cli migrateRun on a clean git branch, review the diff, fix anything migrate couldn’t infer.
Fix 5: Mobile Setup (Android)
cargo tauri android init requires the Android SDK and NDK:
# Set environment variables (add to ~/.zshrc or ~/.bashrc):
export ANDROID_HOME=$HOME/Library/Android/sdk # macOS
export ANDROID_HOME=$HOME/Android/Sdk # Linux
export ANDROID_HOME="$env:LOCALAPPDATA/Android/Sdk" # Windows PowerShell
export NDK_HOME=$ANDROID_HOME/ndk/26.1.10909125 # Use installed NDK version
# Install Rust targets:
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
# Init:
cargo tauri android init
cargo tauri android devInstall Android Studio first — it manages SDK/NDK versions through SDK Manager. The exact NDK version path changes with each release.
For iOS (macOS only):
xcode-select --install
rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim
cargo tauri ios init
cargo tauri ios devCommon Mistake: Trying mobile builds on Windows for iOS. iOS builds require Xcode and macOS. Use a Mac mini, GitHub Actions macOS runners, or a cloud-based macOS service.
Fix 6: Updater Plugin Migration
The updater is a plugin in v2:
# Cargo.toml
[dependencies]
tauri-plugin-updater = "2"// lib.rs
use tauri_plugin_updater::UpdaterExt;
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.setup(|app| {
let handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
if let Ok(Some(update)) = handle.updater().unwrap().check().await {
let _ = update.download_and_install(|_, _| {}, || {}).await;
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}// tauri.conf.json
{
"plugins": {
"updater": {
"endpoints": ["https://releases.example.com/{{target}}/{{arch}}/{{current_version}}"],
"pubkey": "..."
}
}
}Permissions in the capability:
"permissions": [
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install"
]The signature key flow is unchanged — generate with tauri signer generate and sign releases with the private key.
Fix 7: CSP and Webview Security
Tauri 2 enforces stricter CSP. The default is null (no enforcement) for dev convenience, but production builds should set one:
{
"app": {
"security": {
"csp": "default-src 'self' tauri:; script-src 'self' tauri:; style-src 'self' 'unsafe-inline'"
}
}
}tauri: is the scheme for Tauri’s IPC bridge. Without tauri: in script-src/default-src, the frontend can’t call commands.
For frontends that need unsafe-inline styles (most frameworks), include 'unsafe-inline' in style-src. For Vite dev mode, also add ws:// for HMR:
"csp": "default-src 'self' tauri: ws://localhost:1420; script-src 'self' tauri:; style-src 'self' 'unsafe-inline'"Fix 8: IPC and Type-Safe Bindings
In v2, invoke lives in @tauri-apps/api/core:
import { invoke } from "@tauri-apps/api/core";
const result = await invoke<string>("greet", { name: "Alice" });For type safety across many commands, use tauri-specta to generate TypeScript bindings from your Rust commands:
[dependencies]
specta = "2.0.0-rc"
tauri-specta = { version = "2.0.0-rc", features = ["typescript"] }use specta::Type;
use tauri_specta::{collect_commands, Builder};
#[derive(Type, serde::Serialize)]
struct Greeting { message: String }
#[tauri::command]
#[specta::specta]
fn greet(name: String) -> Greeting {
Greeting { message: format!("Hello, {}!", name) }
}
pub fn run() {
let builder = Builder::<tauri::Wry>::new()
.commands(collect_commands![greet]);
#[cfg(debug_assertions)]
builder.export(specta_typescript::Typescript::default(), "../src/bindings.ts").unwrap();
tauri::Builder::default()
.invoke_handler(builder.invoke_handler())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}src/bindings.ts is regenerated on each debug build. Import typed commands:
import { commands } from "./bindings";
const result = await commands.greet("Alice"); // Fully typedStill Not Working?
A few less-obvious failures:
tauri devruns but window is blank. Frontend dev server didn’t start, ordevUrldoesn’t match. CheckbeforeDevCommandruns your dev server first.- CSP blocks IPC. Add
'self' tauri:todefault-src/script-src. Without it, the frontend can’t invoke any command. fs:allow-read-fileworks but reads fail. You also need a scope. Add afs:scope-apppermission that lists allowed paths.- Bundle is huge. Build with
--releaseand strip debug symbols: addstrip = trueto[profile.release]inCargo.toml. - Android:
gradle build failed. Open the project in Android Studio once to let it fix Gradle plugin versions. Then go back tocargo tauri android build. - macOS: notarization fails after
cargo tauri build. SetsigningIdentityand configurebundle.macOS.providerShortName. Without notarization, users see “App is damaged” errors. - Cross-compiling from Linux to Windows: missing system libs. Use the official Tauri GitHub Actions workflow templates. Cross-compile is fragile; use OS-native runners.
- Frontend works in browser but blank in Tauri. Hard-coded
localhost:5173URLs in your code don’t work — Tauri serves fromtauri://localhost. Use relative paths.
For related desktop and cross-platform issues, see Tauri not working, Electron not working, Rust trait not implemented, and Vite failed to resolve import.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Tauri Not Working — Command Not Found, IPC Error, File System Permission Denied, or Build Fails
How to fix Tauri app issues — Rust command registration, invoke IPC, tauri.conf.json permissions, fs scope, window management, and common build errors on Windows/macOS/Linux.
Fix: Electron Forge Not Working — Makers, Code Signing, Native Modules, and Publishers
How to fix Electron Forge errors — forge.config.js makers per OS, code signing on macOS (notarytool) and Windows, native module rebuild via electron-rebuild, Vite/Webpack plugin, auto-updater, and GitHub publisher.
Fix: Node.js fs.watch Not Working — Cross-Platform Quirks, chokidar Migration, Recursive Watch
How to fix Node.js file watching — fs.watch unreliable on Linux, missing events on save, recursive watch on Windows/macOS, chokidar polling fallback, ignoring patterns, debouncing, and EMFILE errors.
Fix: Maturin Not Working — develop Errors, ABI3 Wheels, manylinux, and macOS Universal Builds
How to fix Maturin errors — maturin develop fails outside venv, abi3 forward compatibility, manylinux wheel auditing, macOS universal2 cross-compile, pyproject.toml vs Cargo.toml conflicts, and PyO3 feature flags.