ワンクリックで
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 職業分類に基づく
| 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/ |
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.