بنقرة واحدة
tauri
Use when working with Tauri 2 projects (tauri.conf.json, src-tauri/, capabilities, permissions,
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Use when working with Tauri 2 projects (tauri.conf.json, src-tauri/, capabilities, permissions,
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Use when writing or reviewing code in this Tauri app that can behave differently on Windows — spawning a subprocess (git/gh/any Command), path handling, cfg-gated platform APIs, the pty/terminal, clipboard, ssh, line endings, or a new native crate — and before cutting a release. The dev box is macOS, so Windows regressions do not surface locally; they only show in the Windows CI build or on a user's machine. Encodes the Windows pitfalls this repo has actually shipped and fixed.
Expert workflow for reviewing Tauri 2 desktop app security. Covers capabilities/permissions least-privilege, IPC trust boundary, command input validation, CSP, scope restriction, isolation pattern, and frontend-side hygiene.
Expert workflow for reviewing Tauri 2 desktop app performance. Covers IPC payload optimization, async command efficiency, state lock contention, large-data streaming via Channel/Response, frontend bundle size, and startup time.
| name | tauri |
| description | Use when working with Tauri 2 projects (tauri.conf.json, src-tauri/, capabilities, permissions, |
Tauri 2 is a Rust-based framework for building lightweight, secure cross-platform desktop apps with web frontends. This skill covers Tauri 2 specifically (not v1 — APIs differ).
| Task | Solution | Notes |
|---|---|---|
| Define backend command | #[tauri::command] async fn name(...) | Register in generate_handler! |
| Call from frontend | invoke<T>('name', { args }) | Import from @tauri-apps/api/core |
| Grant frontend access | Add to src-tauri/capabilities/*.json | Permission = "plugin:command" |
| Restrict file access | Permission with allow: [{ path: "$APPDATA/x/*" }] | Use Tauri path vars, not absolute |
| Manage shared state | app.manage(Mutex::new(state)) + State<'_, Mutex<T>> | Tauri wraps in Arc internally |
| Stream large data | tauri::ipc::Channel<T> | Avoids JSON overhead |
| Return binary efficiently | tauri::ipc::Response::new(bytes) | Skip serde JSON encoding |
| Custom error type | thiserror::Error + manual Serialize | Don't return Result<T, String> |
Lock across .await | tokio::sync::Mutex | std::sync::Mutex panics if held across await |
| Add a plugin | .plugin(tauri_plugin_xxx::init()) in builder | Plus pnpm add @tauri-apps/plugin-xxx |
| Strict CSP | tauri.conf.json > app.security.csp | Tauri auto-injects nonces for bundled assets |
my-tauri-app/
├── src/ # Frontend (React/Vue/Svelte/...)
├── src-tauri/
│ ├── Cargo.toml
│ ├── tauri.conf.json # Main config
│ ├── capabilities/
│ │ └── default.json # IPC permissions per window
│ ├── permissions/ # Custom permission definitions
│ ├── icons/
│ └── src/
│ ├── main.rs # Just calls into lib
│ ├── lib.rs # tauri::Builder setup, generate_handler!
│ └── commands/ # Command modules
└── package.json
src-tauri/src/lib.rs#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.manage(AppState::default())
.invoke_handler(tauri::generate_handler![
commands::greet,
commands::save_note,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
// src-tauri/src/commands/notes.rs
use serde::Serialize;
use tauri::State;
use tokio::sync::Mutex;
#[derive(Debug, thiserror::Error)]
pub enum NoteError {
#[error("invalid path")]
InvalidPath,
#[error("io: {0}")]
Io(#[from] std::io::Error),
}
impl Serialize for NoteError {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.to_string())
}
}
#[derive(Default)]
pub struct AppState {
pub last_saved: Option<String>,
}
#[tauri::command]
pub async fn save_note(
contents: String,
state: State<'_, Mutex<AppState>>,
app: tauri::AppHandle,
) -> Result<(), NoteError> {
let dir = app.path().app_data_dir().map_err(|_| NoteError::InvalidPath)?;
let path = dir.join("note.md");
tokio::fs::create_dir_all(&dir).await?;
tokio::fs::write(&path, contents).await?;
state.lock().await.last_saved = Some(path.to_string_lossy().into_owned());
Ok(())
}
// src/lib/notes.ts
import { invoke } from '@tauri-apps/api/core'
export async function saveNote(contents: string): Promise<void> {
return invoke<void>('save_note', { contents })
}
// src-tauri/capabilities/main.json
{
"identifier": "main",
"description": "Permissions granted to the main window",
"windows": ["main"],
"permissions": [
"core:default",
"core:event:default",
{
"identifier": "fs:allow-write-text-file",
"allow": [{ "path": "$APPDATA/notes/*" }]
}
],
"platforms": ["macOS", "windows", "linux"]
}
| Field | Type | Required | Notes |
|---|---|---|---|
identifier | string | ✓ | Unique name |
permissions | array | ✓ | "plugin:command" strings or scoped objects |
description | string | — | Human-readable purpose |
windows | string[] | — | Window labels; supports glob |
webviews | string[] | — | Webview labels; supports glob |
platforms | string[] | — | macOS / windows / linux / iOS / android |
local | boolean | — | Default true; for local app URLs |
remote.urls | string[] | — | Allowed remote URL patterns (URLPattern syntax) |
Security rule: A window with no matching capability has zero IPC access. Use this — make new windows opt-in.
Three forms inside permissions: []:
"core:default" // simple string
"fs:allow-read-text-file" // plugin:command
{ // scoped object
"identifier": "fs:allow-write-text-file",
"allow": [{ "path": "$APPDATA/notes/*" }],
"deny": [{ "path": "$APPDATA/notes/secret" }]
}
| Var | Maps to (macOS sandboxed) |
|---|---|
$APPDATA | ~/Library/Application Support/<bundle-id> |
$APPCONFIG | ~/Library/Application Support/<bundle-id> |
$APPCACHE | ~/Library/Caches/<bundle-id> |
$APPLOG | ~/Library/Logs/<bundle-id> |
$DOCUMENT | ~/Documents (requires user-selected-files entitlement under sandbox) |
$HOME | sandbox container home |
$RESOURCE | bundled resources |
Define reusable permission sets in src-tauri/permissions/<name>.toml:
[[permission]]
identifier = "read-notes"
description = "Read user notes from app data"
commands.allow = ["read_text_file", "read_dir"]
[[scope.allow]]
path = "$APPDATA/notes/*"
Then reference in capability: "permissions": ["fs:read-notes"].
// Register
.manage(Mutex::new(AppState::default()))
// Use in command (sync mutex — fine if NOT held across .await)
#[tauri::command]
fn get_count(state: State<'_, std::sync::Mutex<AppState>>) -> u32 {
state.lock().unwrap().count
}
// Use in async command — MUST be tokio::sync::Mutex if held across .await
#[tauri::command]
async fn save(state: State<'_, tokio::sync::Mutex<AppState>>) -> Result<(), Error> {
let mut s = state.lock().await;
do_async_work().await?;
s.last_saved = Some(now());
Ok(())
}
Pitfall: type mismatch between manage(T) and State<'_, U> causes a runtime panic. Use a type alias.
&str or State<'_, T> directly with naked async — wrap return in Result<T, E> (this satisfies the lifetime requirement)tokio::fs, tokio::sync::*, never block the runtimeuse tauri::ipc::Channel;
#[derive(Clone, Serialize)]
#[serde(tag = "event", content = "data")]
enum DownloadEvent {
Progress { downloaded: u64, total: u64 },
Done,
}
#[tauri::command]
async fn download(url: String, on_event: Channel<DownloadEvent>) -> Result<(), Error> {
// ... fetch
on_event.send(DownloadEvent::Progress { downloaded: 1024, total: 10240 })?;
on_event.send(DownloadEvent::Done)?;
Ok(())
}
Frontend:
import { Channel, invoke } from '@tauri-apps/api/core'
const channel = new Channel<DownloadEvent>()
channel.onmessage = (e) => console.log(e)
await invoke('download', { url: '...', onEvent: channel })
#[tauri::command]
fn read_image(path: String) -> Result<tauri::ipc::Response, Error> {
let bytes = std::fs::read(path)?;
Ok(tauri::ipc::Response::new(bytes))
}
Frontend gets an ArrayBuffer directly — no JSON parse cost.
tauri.conf.json:
{
"app": {
"security": {
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: data:; connect-src 'self' https://api.example.com; font-src 'self' data:"
}
}
}
Tauri auto-adds nonces for bundled assets. Avoid unsafe-eval. If using WASM, add 'wasm-unsafe-eval' to script-src.
| v1 | v2 |
|---|---|
@tauri-apps/api/fs | @tauri-apps/plugin-fs |
@tauri-apps/api/shell | @tauri-apps/plugin-shell |
@tauri-apps/api/dialog | @tauri-apps/plugin-dialog |
@tauri-apps/api/tauri (invoke) | @tauri-apps/api/core |
@tauri-apps/api/notification | @tauri-apps/plugin-notification |
@tauri-apps/api/clipboard | @tauri-apps/plugin-clipboard-manager |
generate_handler! — frontend gets "command not found"invoke_handler! calls — only the last one wins; use a single call$APP_DATA is wrong; correct is $APPDATAstd::sync::Mutex across .await — panic at runtimeunwrap() in command — panics propagate to JS as an opaque errorResult<T, String> — works but loses type safety; use thiserror enumVec<u8> via JSON — encodes as base64, slow; use Response::new(bytes)@tauri-apps/api/tauri doesn't exist in v2| Topic | URL |
|---|---|
| Capabilities | https://v2.tauri.app/reference/acl/capability/ |
| Permissions | https://v2.tauri.app/security/permissions/ |
| CSP | https://v2.tauri.app/security/csp/ |
| Calling Rust | https://v2.tauri.app/develop/calling-rust/ |
| State management | https://v2.tauri.app/develop/state-management/ |