tauri-v2 — Claude Code skill: Tauri v2 apps with a Rust backend
Walks your agent through Rust commands, IPC and capabilities in Tauri v2, and heads off the build errors that usually eat an afternoon.
npx skills add nodnarbnitram/claude-code-extensions --skill tauri-v2 -a claude-codeWhat tauri-v2 does
This skill covers Tauri v2 and later: a web frontend talking to a Rust backend. The agent gets patterns for tauri.conf.json, commands marked with #[tauri::command], invoke and emit calls from the frontend, and capability files. There is also a table of common failures with the root cause and the fix, from "Command not found" to a white screen on launch.
Some rules are easy to forget and the skill spells them out. Every command has to be listed in tauri::generate_handler![], otherwise the frontend call fails silently. Async commands take owned types only, so &str will not compile. Shared state goes through Mutex<T>, and application logic lives in lib.rs because mobile builds replace main() with mobile_entry_point.
Tauri v2 denies everything by default, so a plugin permission has to be added to capabilities/default.json before the plugin works. Mobile builds need Rust targets installed with rustup target add. Reference files cover capabilities, IPC, official plugins, the updater and distribution, plus tray, sidecars and deep links.
What it helps with
- register Rust commands and call them from the frontend with
invoke - set up
tauri.conf.jsonand capabilities without permission errors - debug a command that silently does nothing or panics on state access
- fix a white screen or an IPC timeout at launch
- build the app for mobile targets
Example prompts for the agent
- Add a Rust command that reads a file and call it from the frontend with invoke
- Why does invoking my command from the frontend do nothing? Check generate_handler registration
- Set up capabilities for the store plugin in Tauri v2
How to install tauri-v2
1Skills CLI (Vercel)
npx skills add nodnarbnitram/claude-code-extensions --skill tauri-v2 -a claude-code2Manually: put the files in place
~/.claude/skills/tauri-v2/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/tauri-v2/
1Skills CLI (Vercel)
npx skills add nodnarbnitram/claude-code-extensions --skill tauri-v2 -a codex2Manually: put the files in place
.agents/skills/tauri-v2/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add nodnarbnitram/claude-code-extensions --skill tauri-v2 -a cursor2Manually: put the files in place
.cursor/skills/tauri-v2/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install nodnarbnitram/claude-code-extensions tauri-v22Manually: put the files in place
.github/skills/tauri-v2/SKILL.md1Skills CLI (Vercel)
npx skills add nodnarbnitram/claude-code-extensions --skill tauri-v2 -a gemini-cli2Platform CLI
gemini skills install https://github.com/nodnarbnitram/claude-code-extensions --path plugins/cce-tauri/skills/tauri-v2Manually: put the files in place
.devin/skills/tauri-v2/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add nodnarbnitram/claude-code-extensions --skill tauri-v2 -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about tauri-v2
How do I install tauri-v2 in Claude?
Run npx skills add nodnarbnitram/claude-code-extensions --skill tauri-v2 -a claude-code. The skill lands in ~/.claude/skills/tauri-v2/SKILL.md.
What does the tauri-v2 skill do?
It gives the agent patterns and rules for Tauri v2: Rust commands, IPC with invoke and emit, capabilities, config, and fixes for common build errors.
Why is my command not reachable from the frontend?
It is probably missing from tauri::generate_handler![]. Unregistered commands fail silently when invoked.
Source text
SKILL.md · MIT
---
name: tauri-v2
description: "Tauri v2+ cross-platform app development with Rust backend. Use when configuring tauri.conf.json, implementing Rust commands (#[tauri::command]), setting up IPC patterns (invoke, emit, channels), configuring permissions/capabilities, troubleshooting build issues, or deploying desktop/mobile apps. Triggers on Tauri, src-tauri, invoke, emit, capabilities.json."
version: 1.0.1
---
# Tauri v2+ Development Skill
> Build cross-platform desktop and mobile apps with web frontends and Rust backends.
## Before You Start
**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) |
### Known Issues This Skill Prevents
1. Permission denied errors from missing capabilities
2. IPC failures from unregistered commands in `generate_handler!`
3. State management panics from type mismatches
4. Mobile build failures from missing Rust targets
5. White screen issues from misconfigured dev URLs
## Quick Start
### Step 1: Create a Tauri Command
```rust
// 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.rs` stays thin:** `src-tauri/src/main.rs` should only be a thin passthrough — all application logic lives in `lib.rs`:
> ```rust
> // 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()` with `mobile_entry_point` on mobile targets.
### Step 2: Call from Frontend
```typescript
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).
### Step 3: Add Required Permissions
```json
// 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.
## Critical Rules
### Always Do
- Register every command in `tauri::generate_handler![cmd1, cmd2, ...]`
- Return `Result<T, E>` from commands for proper error handling
- Use `Mutex<T>` for shared state accessed from multiple commands
- Add capabilities before using any plugin features
- Use `lib.rs` for shared code (required for mobile builds)
- Use `#[cfg_attr(mobile, tauri::mobile_entry_point)]` on `pub fn run()` in `lib.rs` for mobile compatibility
### Never Do
- Never use borrowed types (`&str`) in async commands - use owned types
- Never block the main thread - use async for I/O operations
- Never hardcode paths - use Tauri path APIs (`app.path()`)
- Never skip capability setup - even "safe" operations need permissions
### Common Mistakes
**Wrong - Borrowed type in async:**
```rust
#[tauri::command]
async fn bad(name: &str) -> String { // Compile error!
name.to_string()
}
```
**Correct - Owned type:**
```rust
#[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.
## Known Issues Prevention
| 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>` |
## Deep-Dive References
- **Security & permissions** → [`references/capabilities-reference.md`](references/capabilities-reference.md)
- **IPC decision guide** → [`references/ipc-patterns.md`](references/ipc-patterns.md)
- **Official plugins** → [`references/plugin-reference.md`](references/plugin-reference.md)
- **Updater & distribution** → [`references/updater-distribution-reference.md`](references/updater-distribution-reference.md)
- **Tray, sidecars, deep links** → [`references/advanced-runtime-reference.md`](references/advanced-runtime-reference.md)
## Configuration Reference
### tauri.conf.json
```json
{
"$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 port
- `app.security.capabilities`: Array of capability file identifiers
**Plugin 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.
## Project Structure
```
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()`.
### Cargo.toml
```toml
[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 builds
- `crate-type`: Must include all three types for cross-platform
## Common Patterns
### Error Handling Pattern
Use `Result<T, E>` and `thiserror` for type-safe error propagation across the IPC boundary. See [`references/ipc-patterns.md`](references/ipc-patterns.md) for full implementation details.
```rust
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())
}
```
### Serde Boundary Rules
All command arguments must implement `serde::Deserialize`, and return types must implement `serde::Serialize`. This is how Tauri bridges JSON over the IPC boundary.
```rust
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:**
- Field names are camelCase in JS, snake_case in Rust — Tauri automatically converts between them
- `Option<T>` maps to optional JS arguments (can be `undefined` or `null`)
- Complex enums need `#[serde(tag = "type")]` or similar to be JSON-safe
- Error types must also implement `Serialize` (see Error Handling Pattern above)
### State Management Pattern
Tauri state manages application data across commands. See [`references/ipc-patterns.md`](references/ipc-patterns.md) for more complex state patterns.
```rust
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 }))
```
### Event Emission Pattern
Events are fire-and-forget notifications. See [`references/ipc-patterns.md`](references/ipc-patterns.md) for bidirectional examples.
```rust
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();
});
}
```
```typescript
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('task-progress', (e) => {
console.log('Progress:', e.payload);
});
// Call unlisten() when done
```
### Channel Streaming Pattern
Channels provide high-frequency, typed streaming from Rust to Frontend. See [`references/ipc-patterns.md`](references/ipc-patterns.md) for full implementation details.
```rust
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();
}
```
```typescript
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 });
```
### Window Access Pattern
Tauri v2 uses `WebviewWindow` for unified window and webview management.
```rust
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.
## Bundled Resources
### References
Located in `references/`:
- [`capabilities-reference.md`](references/capabilities-reference.md) - Permission patterns and examples
- [`ipc-patterns.md`](references/ipc-patterns.md) - Complete IPC examples
- [`plugin-reference.md`](references/plugin-reference.md) - Official plugin install, registration, and permission strings
- [`updater-distribution-reference.md`](references/updater-distribution-reference.md) - Signing, HTTPS requirements, and bundle shipping
- [`advanced-runtime-reference.md`](references/advanced-runtime-reference.md) - `TrayIconBuilder`, sidecars, deep links, and asset protocols
> **Note:** For deep dives on specific topics, see the reference files above.
## Dependencies
### Required
| 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](https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/CHANGELOG.md) for feature timing.*
### Optional (Plugins)
| 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.md`](references/plugin-reference.md) for full install + permission details for all official plugins.
## Official Documentation
- [Tauri v2+ Documentation](https://v2.tauri.app/)
- [Commands Reference](https://v2.tauri.app/develop/calling-rust/)
- [Capabilities & Permissions](https://v2.tauri.app/security/capabilities/)
- [Configuration Reference](https://v2.tauri.app/reference/config/)
## Troubleshooting
### White Screen on Launch
**Symptoms:** App launches but shows blank white screen
**Solution:**
1. Verify `devUrl` matches your frontend dev server port
2. Check `beforeDevCommand` runs your dev server
3. Open DevTools (Cmd+Option+I / Ctrl+Shift+I) to check for errors
### Command Returns Undefined
**Symptoms:** `invoke()` returns undefined instead of expected value
**Solution:**
1. Verify command is in `generate_handler![]`
2. Check Rust command actually returns a value
3. Ensure argument names match (camelCase in JS, snake_case in Rust by default)
### Mobile Build Failures
**Symptoms:** Android/iOS build fails with missing target
**Solution:**
```bash
# 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
```
### Desktop vs Mobile Behavioral Differences
Not all Tauri APIs and plugins support mobile (iOS/Android). Before using any plugin or API in a mobile build:
1. **Check the plugin page** at `v2.tauri.app/plugin/<name>/` for platform support matrix
2. **Common desktop-only items**: System tray (`TrayIconBuilder`), window labels/multi-window, some shell plugin features
3. **Mobile-safe patterns**: IPC commands/events/channels work on all platforms; `tauri::AppHandle` is mobile-safe
4. **Conditional compilation**: Use `#[cfg(desktop)]` / `#[cfg(mobile)]` for platform-specific Rust logic
```rust
#[tauri::command]
fn platform_info() -> String {
#[cfg(desktop)]
return "desktop".to_string();
#[cfg(mobile)]
return "mobile".to_string();
}
```
## Setup Checklist
Before using this skill, verify:
- [ ] `npx tauri info` shows correct Tauri v2 versions
- [ ] `src-tauri/capabilities/default.json` exists with at least `core:default`
- [ ] All commands registered in `generate_handler![]`
- [ ] `lib.rs` contains shared code (for mobile support)
- [ ] Required Rust targets installed for target platforms
© nodnarbnitram · MIT · nodnarbnitram/claude-code-extensions