Help us improve
Share bugs, ideas, or general feedback.
From cce-tauri
Develops Tauri v2+ cross-platform desktop/mobile apps with Rust backend, configuring tauri.conf.json, #[tauri::command] handlers, IPC (invoke/emit/channels), capabilities/permissions, and troubleshooting builds.
npx claudepluginhub nodnarbnitram/claude-code-extensions --plugin cce-tauriHow this skill is triggered — by the user, by Claude, or both
Slash command
/cce-tauri:tauri-v2The summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Build cross-platform desktop and mobile apps with web frontends and Rust backends.
Packages web frontends as native apps for Android, Windows, macOS, and Linux using Tauri v2. Handles build config, permissions, and native features like notifications and file system.
Guides cross-platform desktop app development with Electron and Tauri, covering framework selection, project setup, folder structure, and process separation for Windows, macOS, Linux.
Manages Tauri v2 apps via CLI: starts/recovers driver sessions, automates webviews, captures UI state/screenshots, executes JS, debugs IPC, handles mobile/remote devices. Use for terminal control of Tauri apps.
Share bugs, ideas, or general feedback.
Build cross-platform desktop and mobile apps with web frontends and Rust backends.
This skill prevents 8+ common errors and saves ~60% tokens.
| Metric | Without Skill | With Skill |
|---|---|---|
| Setup Time | ~2 hours | ~30 min |
| Common Errors | 8+ | 0 |
| Token Usage | High (exploration) | Low (direct patterns) |
generate_handler!// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: String) -> 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");
}
Why this matters: Commands not in generate_handler![] silently fail when invoked from frontend.
main.rsstays thin:src-tauri/src/main.rsshould only be a thin passthrough — all application logic lives inlib.rs:// src-tauri/src/main.rs #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { app_lib::run(); }This split is required for mobile builds — Tauri replaces
main()withmobile_entry_pointon mobile targets.
import { invoke } from '@tauri-apps/api/core';
const greeting = await invoke<string>('greet', { name: 'World' });
console.log(greeting); // "Hello, World!"
Why this matters: Use @tauri-apps/api/core (not @tauri-apps/api/tauri - that's v1 API).
// src-tauri/capabilities/default.json
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"windows": ["main"],
"permissions": ["core:default"]
}
Why this matters: Tauri v2 denies everything by default - explicit permissions required for all operations.
tauri::generate_handler![cmd1, cmd2, ...]Result<T, E> from commands for proper error handlingMutex<T> for shared state accessed from multiple commandslib.rs for shared code (required for mobile builds)#[cfg_attr(mobile, tauri::mobile_entry_point)] on pub fn run() in lib.rs for mobile compatibility&str) in async commands - use owned typesapp.path())Wrong - Borrowed type in async:
#[tauri::command]
async fn bad(name: &str) -> String { // Compile error!
name.to_string()
}
Correct - Owned type:
#[tauri::command]
async fn good(name: String) -> String {
name
}
Why: Async commands cannot borrow data across await points; Tauri requires owned types for async command parameters.
| Issue | Root Cause | Solution |
|---|---|---|
| "Command not found" | Missing from generate_handler! | Add command to handler macro |
| "Permission denied" | Missing capability | Add to capabilities/default.json |
| Plugin feature silently fails | Plugin installed but permission not in capability | Add plugin permission string to capabilities/default.json |
| Updater fails in production | Unsigned artifacts or HTTP endpoint | Generate keys with cargo tauri signer generate, use HTTPS endpoint only |
| Sidecar not found | externalBin not in tauri.conf.json or missing executable | Add path to bundle.externalBin, ensure binary is bundled |
| Feature works on desktop, breaks on mobile | Desktop-only API used | Check if API has mobile support — some plugins are desktop-only |
| State panic on access | Type mismatch in State<T> | Use exact type from .manage() |
| White screen on launch | Frontend not building | Check beforeDevCommand in config |
| IPC timeout | Blocking async command | Remove blocking code or use spawn |
| Mobile build fails | Missing Rust targets | Run rustup target add <target> |
references/capabilities-reference.mdreferences/ipc-patterns.mdreferences/plugin-reference.mdreferences/updater-distribution-reference.mdreferences/advanced-runtime-reference.md{
"$schema": "./gen/schemas/desktop-schema.json",
"productName": "my-app",
"version": "1.0.0",
"identifier": "com.example.myapp",
"build": {
"devUrl": "http://localhost:5173",
"frontendDist": "../dist",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [{
"label": "main",
"title": "My App",
"width": 800,
"height": 600
}],
"security": {
"csp": "default-src 'self'; img-src 'self' data:",
"capabilities": ["default"]
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": ["icons/icon.icns", "icons/icon.ico", "icons/icon.png"]
}
}
Key settings:
build.devUrl: Must match your frontend dev server portapp.security.capabilities: Array of capability file identifiersPlugin configuration — Some plugins require additional tauri.conf.json blocks (e.g., store, updater). Always check the specific plugin docs at v2.tauri.app/plugin/<plugin-name>/ for required config keys.
my-tauri-app/
├── src/ # Frontend source
├── src-tauri/
│ ├── src/
│ │ ├── main.rs # Thin passthrough — calls lib::run()
│ │ └── lib.rs # ALL application logic lives here
│ ├── capabilities/
│ │ └── default.json # Capability definitions (grant permissions here)
│ ├── tauri.conf.json # App configuration (devUrl, bundle, security)
│ ├── Cargo.toml # Rust dependencies
│ └── build.rs # Build script (required for tauri-build)
└── package.json
Why lib.rs owns all logic: Tauri replaces main() with #[cfg_attr(mobile, tauri::mobile_entry_point)] on mobile. All commands, state, and builder setup must live in lib.rs::run().
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Key settings:
[lib] section: Required for mobile buildscrate-type: Must include all three types for cross-platformUse Result<T, E> and thiserror for type-safe error propagation across the IPC boundary. See references/ipc-patterns.md for full implementation details.
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Not found: {0}")]
NotFound(String),
}
impl serde::Serialize for AppError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: serde::ser::Serializer {
serializer.serialize_str(self.to_string().as_ref())
}
}
#[tauri::command]
fn risky_operation() -> Result<String, AppError> {
Ok("success".into())
}
All command arguments must implement serde::Deserialize, and return types must implement serde::Serialize. This is how Tauri bridges JSON over the IPC boundary.
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUserArgs {
name: String,
email: String,
role: Option<String>, // Optional fields use Option<T>
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}
#[tauri::command]
fn create_user(args: CreateUserArgs) -> Result<User, String> {
Ok(User { id: 1, name: args.name })
}
Common serde pitfalls:
Option<T> maps to optional JS arguments (can be undefined or null)#[serde(tag = "type")] or similar to be JSON-safeSerialize (see Error Handling Pattern above)Tauri state manages application data across commands. See references/ipc-patterns.md for more complex state patterns.
use std::sync::Mutex;
use tauri::State;
struct AppState {
counter: u32,
}
#[tauri::command]
fn increment(state: State<'_, Mutex<AppState>>) -> u32 {
let mut s = state.lock().unwrap();
s.counter += 1;
s.counter
}
// In builder:
tauri::Builder::default()
.manage(Mutex::new(AppState { counter: 0 }))
Events are fire-and-forget notifications. See references/ipc-patterns.md for bidirectional examples.
use tauri::Emitter;
#[tauri::command]
fn start_task(app: tauri::AppHandle) {
std::thread::spawn(move || {
app.emit("task-progress", 50).unwrap();
app.emit("task-complete", "done").unwrap();
});
}
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('task-progress', (e) => {
console.log('Progress:', e.payload);
});
// Call unlisten() when done
Channels provide high-frequency, typed streaming from Rust to Frontend. See references/ipc-patterns.md for full implementation details.
use tauri::ipc::Channel;
#[derive(Clone, serde::Serialize)]
#[serde(tag = "event", content = "data")]
enum DownloadEvent {
Progress { percent: u32 },
Complete { path: String },
}
#[tauri::command]
async fn download(url: String, on_event: Channel<DownloadEvent>) {
for i in 0..=100 {
on_event.send(DownloadEvent::Progress { percent: i }).unwrap();
}
on_event.send(DownloadEvent::Complete { path: "/downloads/file".into() }).unwrap();
}
import { invoke, Channel } from '@tauri-apps/api/core';
const channel = new Channel<DownloadEvent>();
channel.onmessage = (msg) => console.log(msg.event, msg.data);
await invoke('download', { url: 'https://...', onEvent: channel });
Tauri v2 uses WebviewWindow for unified window and webview management.
use tauri::Manager;
#[tauri::command]
fn focus_window(app: tauri::AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_focus();
}
}
Why this matters: Use tauri::WebviewWindow and app.get_webview_window("label") in v2 — the v1 app.get_window() API is removed in v2.
Located in references/:
capabilities-reference.md - Permission patterns and examplesipc-patterns.md - Complete IPC examplesplugin-reference.md - Official plugin install, registration, and permission stringsupdater-distribution-reference.md - Signing, HTTPS requirements, and bundle shippingadvanced-runtime-reference.md - TrayIconBuilder, sidecars, deep links, and asset protocolsNote: For deep dives on specific topics, see the reference files above.
| Package | Version | Purpose |
|---|---|---|
@tauri-apps/cli | ^2 (v2+) | CLI tooling |
@tauri-apps/api | ^2 (v2+) | Frontend APIs |
tauri | ^2 (v2+) | Rust core |
tauri-build | ^2 (v2+) | Build scripts |
*Last verified: 2026-04-02. Always check official changelog for feature timing.
| Package | Version | Purpose | Key Permission |
|---|---|---|---|
tauri-plugin-fs | ^2 (v2+) | File system access | fs:default |
tauri-plugin-dialog | ^2 (v2+) | Native dialogs | dialog:default |
tauri-plugin-shell | ^2 (v2+) | Shell commands, open URLs | shell:default |
tauri-plugin-http | ^2 (v2+) | HTTP client | http:default |
tauri-plugin-store | ^2 (v2+) | Key-value storage | store:default |
Plugin permissions are mandatory. Installing a plugin without adding its permission string to a capability file causes silent runtime failures. See
references/plugin-reference.mdfor full install + permission details for all official plugins.
Symptoms: App launches but shows blank white screen
Solution:
devUrl matches your frontend dev server portbeforeDevCommand runs your dev serverSymptoms: invoke() returns undefined instead of expected value
Solution:
generate_handler![]Symptoms: Android/iOS build fails with missing target
Solution:
# Android targets
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
# iOS targets (macOS only)
rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim
Not all Tauri APIs and plugins support mobile (iOS/Android). Before using any plugin or API in a mobile build:
v2.tauri.app/plugin/<name>/ for platform support matrixTrayIconBuilder), window labels/multi-window, some shell plugin featurestauri::AppHandle is mobile-safe#[cfg(desktop)] / #[cfg(mobile)] for platform-specific Rust logic#[tauri::command]
fn platform_info() -> String {
#[cfg(desktop)]
return "desktop".to_string();
#[cfg(mobile)]
return "mobile".to_string();
}
Before using this skill, verify:
npx tauri info shows correct Tauri v2 versionssrc-tauri/capabilities/default.json exists with at least core:defaultgenerate_handler![]lib.rs contains shared code (for mobile support)