tauri について
Tauri でデスクトップアプリを開発するためのスキルです。Rust コマンド、IPC、権限、パス、ファイル操作を扱い、セキュリティ境界や生成バインディングの同期、上流資料による検証を重視します。
主な機能
- 01Rust コマンドと権限の設計
- 02IPC と型付きバインディングの指針
- 03セキュリティ設定と CSP
- 04クロスプラットフォームのパス処理
- 05ファイル操作とネイティブ境界の設計
活用シーン
- 01フロントエンドとバックエンド間の Tauri コマンド実装
- 02ウィンドウ権限や CSP の調整
- 03TypeScript バインディングの生成と確認
- 04クロスプラットフォームのファイルパス処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill tauriインストール後、Claude Code で/tauriを入力して読み込まれたか確認します。
同梱ファイルreferences/rust-command-errors.md
詳細
Tauri のパターン
参照リポジトリ
- Tauri:Rust バックエンドと Web フロントエンドを備えたデスクトップアプリフレームワーク
上流の情報源に基づく確認
Tauri のコマンドの動作、権限、ケイパビリティ、CSP、アセットプロトコル、パス API、プラグインのファイルシステム動作、または IPC のセマンティクスが正確性に影響する場合は、記憶だけに頼らず、ソースに基づいて確認してください。DeepWiki MCP が利用可能であれば、tauri-apps/tauri に絞った質問をしてください。利用できない場合や、リポジトリがインデックス化されていない場合は、上流のソースまたは公式ドキュメントを直接参照してください。DeepWiki は概要を把握するために使い、重要な詳細はコードを変更する前に、ローカルの生成済みバインディング、インストール済みの Rust クレート、TypeScript の型、ソース、または公式ドキュメントで確認してください。
リポジトリ内で確認できるコマンドの命名規則や、アプリ固有のラッパー規約については、DeepWiki で確認する必要はありません。
コマンド、権限、セキュリティ
- 用途を絞った Rust API を
#[tauri::command]で公開し、generate_handler!で登録してください。失敗する可能性のある処理ではResult<T, E>を返してください。 - コマンドの入力は Rust 側で検証してください。TypeScript の呼び出し元は信頼境界ではありません。
- 現在、Epicenter のケイパビリティではウィンドウラベルごとにコマンドへのアクセスを許可しています。コマンドの登録と、関連するケイパビリティの許可設定に一貫性を持たせてください。提案中の ADR-0402 ではこれらをホスト全体に適用される定数に置き換える予定ですが、まだ実装されていないため、ネイティブ呼び出しを追加する際に前提としてはいけません。
- CSP、
devCsp、アセットプロトコルの設定、convertFileSrc、freezePrototype、リモート IPC は、セキュリティ上慎重に扱うべき設定です。 - 長期間存続する Rust オブジェクトは、フロントエンドの
ResourceIdを持つ Tauri リソースにしてください。複雑な長期間存続するオブジェクトをコマンドの応答を通じてシリアライズしてはいけません。
Webview の CSP
app.security.csp: null を決してリリースしないでください(CSP が完全に無効になります)。
最も重要なディレクティブは connect-src です。これを API のオリジンと
Tauri の IPC に限定すると、同一オリジンに挿入されたスクリプトが、メモリ内の
シークレット(トークン、キー)を攻撃者のホストに流出させるのを防げます。まずは
制限の厳しいポリシーを設定し、アセットプロトコル、wasm、ワーカー、メディア、
開発サーバーのオリジンなど、アプリが実際に使用するソースだけを追加してください。
csp(本番環境)と devCsp(開発用のオーバーライド。tauri dev 実行時に
csp を置き換えます)の両方を設定してください。
"security": {
// Tauri's tauri-codegen hashes every inline <script> in the built
// frontendDist and injects the hashes, so production script-src does NOT
// need 'unsafe-inline' (a SvelteKit SPA still boots via its hash).
"csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost https://api.example.com wss://api.example.com; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'",
// Dev loads from the Vite server (not the hashed build), so its inline/HMR
// scripts ARE unhashed: devCsp must keep 'unsafe-inline' (+ 'unsafe-eval')
// and add the localhost dev origins.
"devCsp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost http://localhost:5173 ws://localhost:5173 https://api.example.com wss://api.example.com; img-src 'self' data: blob: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; object-src 'none'; base-uri 'self'"
}
ルール:connect-src には必ず ipc: http://ipc.localhost を含めてください。含めないと
invoke() が動作しません。アセットプロトコルが実際に有効な場合
(convertFileSrc)に限り、asset: / http://asset.localhost を指定してください。
実際に tauri dev を実行し、リリースビルドも作成して、必ずスモークテストを行ってください。
その際は Webview のコンソールを確認し、CSP 違反がないことを確かめてください。
型付き IPC と生成されたバインディング
Tauri アプリで tauri-specta を使用する場合は、Rust のコマンドレジストリ、生成された TypeScript バインディング、アプリが所有するコマンドサーフェスを同期させてください。コマンドで TypeScript が使用するエラーを変更するとき、または生成された Result が IPC を通じてどのように渡されるかを選択するときは、Rust コマンドのエラーを参照してください。
- すべての型付きコマンドを
tauri_specta::collect_commands!ビルダーに登録します。 - フロントエンドがリッスンするイベントのペイロードは、そのイベント型を返すコマンドがない場合でも、
tauri_specta::collect_events!に登録します。 - tauri-specta v2 RC のイベントでは、イベント型に
#[tauri_specta(event_name = "...")]を使用します。インストール済みのマクロドキュメントまたはローカルのマクロソースでその形式が存在すると確認できない限り、#[tauri_specta::event(...)]を作らないでください。 lib.rsがビルダー用にイベントとコマンドのペイロード型をインポートする場合は、それらを所有する Rust モジュールから再エクスポートします。bindings.gen.tsは派生出力として扱います。コマンド、エラー、または生成モードを変更した場合は、再生成し、両方の利用側を確認します。コマンドの変更が Rust のコンパイル形式を修正するだけで、公開 IPC コントラクトを変更しない場合は、生成ファイルに広範な変更が生じないようにします。tauri::ipc::Responseをそのまま返すコマンドは、本文がspecta::Typeではないため、specta では生成できません。これらは別のtauri::generate_handler!ルートを通してマウントし、小規模な手書きの TypeScript ラッパーを用意します。
IPC の変更を検証するには、通常、両側での確認が必要です。
cargo check --manifest-path apps/epicenter/src-tauri/Cargo.toml
cargo test --manifest-path apps/epicenter/src-tauri/Cargo.toml export_types
バインディングの生成によって無関係なセクションが書き換えられた場合は、コミットする前に差分を確認してください。
コンテキストの検出
パス API を選ぶ前に、実行コンテキストを特定してください。
| コンテキスト | 場所 | 適切な API |
|---|---|---|
| Tauri フロントエンド | apps/*/src/**/*.ts、apps/*/src/**/*.svelte | @tauri-apps/api/path |
| Node.js/Bun バックエンド | packages/**/*.ts、CLI ツール | Node.js の path モジュール |
ルール:コードがブラウザー(Tauri WebView)で実行される場合は、Tauri のパス API を使用してください。Node.js/Bun で実行される場合は、Node.js の path モジュールを使用してください。
@tauri-apps/api/path で利用できる関数
パス操作
| 関数 | 用途 | 例 |
|---|---|---|
join(...paths) | プラットフォームの区切り文字でパス要素を結合します | await join(baseDir, 'workspaces', id) |
dirname(path) | 親ディレクトリを取得します | await dirname('/foo/bar/file.txt') → /foo/bar |
basename(path, ext?) | ファイル名を取得します。拡張子は任意で除外できます | await basename('/foo/bar.txt', '.txt') → bar |
extname(path) | ファイル拡張子を取得します | await extname('file.txt') → .txt |
normalize(path) | .. と . の要素を解決します | await normalize('/foo/bar/../baz') → /foo/baz |
resolve(...paths) | 絶対パスに解決します | await resolve('relative', 'path') |
isAbsolute(path) | パスが絶対パスかどうかを確認します | await isAbsolute('/foo') → true |
プラットフォーム定数
| 関数 | 用途 | 戻り値 |
|---|---|---|
sep() | プラットフォームのパス区切り文字 | Windows では \、POSIX では / |
delimiter() | プラットフォームのパス区切り文字 | Windows では ;、POSIX では : |
Tauri v2 では、sep() と delimiter() は同期処理です。ディレクトリやパスを操作するヘルパー関数のほとんどは、バックエンドを呼び出すため非同期処理です。
ベースディレクトリ
| 機能 | 用途 |
|---|---|
appLocalDataDir() | アプリのローカルデータディレクトリ |
appDataDir() | アプリのローミングデータディレクトリ |
appConfigDir() | アプリの設定ディレクトリ |
appCacheDir() | アプリのキャッシュディレクトリ |
appLogDir() | アプリのログディレクトリ |
tempDir() | システムの一時ディレクトリ |
resourceDir() | アプリのリソースディレクトリ |
resolveResource(path) | リソースを基準にパスを解決 |
パターン
パスの構築(正しい方法)
import { appLocalDataDir, dirname, join } from '@tauri-apps/api/path';
// Join path segments; handles platform separators automatically
const baseDir = await appLocalDataDir();
const filePath = await join(baseDir, 'workspaces', workspaceId, 'data.json');
// Get parent directory; cleaner than manual slicing
const parentDir = await dirname(filePath);
await mkdir(parentDir, { recursive: true });
パスのログ出力(例外)
人が読みやすいログ出力では、ファイルシステム操作には使用しないため、/ をハードコードしても問題ありません。
// OK for logging; consistent cross-platform log output
const logPath = pathSegments.join('/');
console.log(`[Persistence] Loading from ${logPath}`);
アンチパターン
禁止:文字列の手動連結
// BAD: Hardcoded separator breaks on Windows
const filePath = baseDir + '/' + 'workspaces' + '/' + id;
// BAD: Template literal with hardcoded separator
const filePath = `${baseDir}/workspaces/${id}`;
// GOOD: Use join()
const filePath = await join(baseDir, 'workspaces', id);
禁止:親ディレクトリの手動抽出
// BAD: Manual slicing is error-prone
const parentSegments = pathSegments.slice(0, -1);
const parentDir = await join(baseDir, ...parentSegments);
// GOOD: Use dirname()
const parentDir = await dirname(filePath);
禁止:ファイルシステム操作での区切り文字のハードコード
// BAD: Windows uses backslashes
const configPath = appDir + '/config.json';
// GOOD: Platform-agnostic
const configPath = await join(appDir, 'config.json');
禁止:パス形式の決めつけ
// BAD: Splitting on '/' fails on Windows paths
const parts = filePath.split('/');
// GOOD: Use dirname/basename for extraction
const dir = await dirname(filePath);
const file = await basename(filePath);
インポートパターン
常に @tauri-apps/api/path からインポートしてください。
import {
appLocalDataDir,
dirname,
join,
basename,
extname,
normalize,
resolve,
sep,
} from '@tauri-apps/api/path';
非同期処理に関する注意
Tauri のパスヘルパーの多くは、IPC 経由で Rust バックエンドと通信するため、非同期です。ヘルパーが Promise を返すと決めつける前に、インストールされている TypeScript の型定義を必ず確認してください。appLocalDataDir()、join()、dirname() などのディレクトリヘルパーやパス操作ヘルパーは非同期です。一方、sep() や delimiter() などの単純な定数は、Tauri v2 では同期処理です。
const baseDir = await appLocalDataDir();
const filePath = await join(baseDir, 'file.txt');
const parent = await dirname(filePath);
const separator = sep();
ファイルシステム操作
ファイル操作には、Tauri のパス API と組み合わせて @tauri-apps/plugin-fs を使用してください。
import { appLocalDataDir, dirname, join } from '@tauri-apps/api/path';
import { mkdir, readFile, writeFile } from '@tauri-apps/plugin-fs';
async function saveData(segments: string[], data: Uint8Array) {
const baseDir = await appLocalDataDir();
const filePath = await join(baseDir, ...segments);
// Ensure parent directory exists
const parentDir = await dirname(filePath);
await mkdir(parentDir, { recursive: true });
await writeFile(filePath, data);
}
ネイティブ側の所有権境界
ネイティブ側がデータを所有する場合は、フロントエンドが制御するパスよりも、アプリが所有する識別子を優先してください。
- Rust が録音ディレクトリを所有する場合、録音操作には録音 ID を渡してください。
- モデルの選択ではパスを渡せますが、Rust 側で正規化し、許可されたアプリデータモデルのディレクトリ外の値は拒否してください。
- Markdown のエクスポート、ダウンロード、一時ファイルには、広範なファイルシステム権限ではなく、アプリが所有するディレクトリをルートとする目的に特化したコマンドを使用してください。
- ケイパビリティ、プラグイン、またはコマンドを削除する場合は、それについて記載している古いドキュメントや UI も削除してください。提案中の ADR-0402 では、ケイパビリティの検証を 1 つのスキーマ比較に統合する予定です。現在のテストでは、引き続き個別のウィンドウ権限を検証しています。
フロントエンドはユーザーの意図を記憶できます。ファイルシステムの境界を強制するのは Rust です。