tauri
$
npx mdskill add EpicenterHQ/epicenter/tauriHandles Tauri commands, file paths, and cross-platform desktop app security
- Solves Tauri command setup, permissions, and filesystem path issues in desktop apps
- Uses Rust commands, @tauri-apps/api, and native filesystem APIs
- Analyzes security config, capabilities, and platform-specific file operations
- Provides precise code patterns for Tauri app development workflows
SKILL.md
.github/skills/tauriView on GitHub ↗
---
name: tauri
description: Tauri commands, permissions, capabilities, security config, path handling, cross-platform file ops, and native filesystem APIs. Use when mentioning Tauri, desktop apps, Rust commands, invoke, capabilities, permissions, ResourceId, file paths, or platform differences.
metadata:
author: epicenter
version: '1.0'
---
# Tauri Patterns
## Reference Repositories
- [Tauri](https://github.com/tauri-apps/tauri) — Desktop app framework with Rust backend and web frontend
## When to Apply This Skill
Use this pattern when you need to:
- Add or change Tauri commands, permissions, capabilities, or security config.
- Build file paths in Tauri frontend code running in the webview.
- Choose correctly between `@tauri-apps/api/path` and Node/Bun `path` APIs.
- Replace manual slash concatenation with `join()`, `dirname()`, and related helpers.
- Handle cross-platform filesystem behavior for desktop apps.
- Combine Tauri path APIs with `@tauri-apps/plugin-fs` operations.
## Commands, Permissions, And Security
- Expose focused Rust APIs with `#[tauri::command]`, register them with `generate_handler!`, and return `Result<T, E>` for fallible work.
- Validate command inputs on the Rust side. TypeScript callers are not the trust boundary.
- Keep capabilities least-privilege in `app.security.capabilities`, scoped to the windows or webviews that need them. Avoid broad permission wildcards.
- Treat CSP, `devCsp`, asset protocol configuration, `convertFileSrc`, `freezePrototype`, and remote IPC as security-sensitive config.
- Long-lived Rust objects should be Tauri resources with frontend `ResourceId`s. Do not serialize complex long-lived objects through command responses.
## Context Detection
Before choosing a path API, determine your execution context:
| Context | Location | Correct API |
| ----------------------- | ---------------------------------------------- | ---------------------- |
| **Tauri frontend** | `apps/*/src/**/*.ts`, `apps/*/src/**/*.svelte` | `@tauri-apps/api/path` |
| **Node.js/Bun backend** | `packages/**/*.ts`, CLI tools | Node.js `path` module |
**Rule**: If the code runs in the browser (Tauri webview), use Tauri's path APIs. If it runs in Node.js/Bun, use the Node.js `path` module.
## Available Functions from `@tauri-apps/api/path`
### Path Manipulation
| Function | Purpose | Example |
| ---------------------- | ------------------------------------------ | ------------------------------------------------- |
| `join(...paths)` | Join path segments with platform separator | `await join(baseDir, 'workspaces', id)` |
| `dirname(path)` | Get parent directory | `await dirname('/foo/bar/file.txt')` → `/foo/bar` |
| `basename(path, ext?)` | Get filename, optionally strip extension | `await basename('/foo/bar.txt', '.txt')` → `bar` |
| `extname(path)` | Get file extension | `await extname('file.txt')` → `.txt` |
| `normalize(path)` | Resolve `..` and `.` segments | `await normalize('/foo/bar/../baz')` → `/foo/baz` |
| `resolve(...paths)` | Resolve to absolute path | `await resolve('relative', 'path')` |
| `isAbsolute(path)` | Check if path is absolute | `await isAbsolute('/foo')` → `true` |
### Platform Constants
| Function | Purpose | Returns |
| ------------- | ----------------------- | ---------------------------- |
| `sep()` | Platform path separator | `\` on Windows, `/` on POSIX |
| `delimiter()` | Platform path delimiter | `;` on Windows, `:` on POSIX |
### Base Directories
| Function | Purpose |
| ----------------------- | ---------------------------------- |
| `appLocalDataDir()` | App's local data directory |
| `appDataDir()` | App's roaming data directory |
| `appConfigDir()` | App's config directory |
| `appCacheDir()` | App's cache directory |
| `appLogDir()` | App's log directory |
| `tempDir()` | System temp directory |
| `resourceDir()` | App's resource directory |
| `resolveResource(path)` | Resolve path relative to resources |
## Patterns
### Constructing Paths (Correct)
```typescript
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 });
```
### Logging Paths (Exception)
For human-readable log output, hardcoded `/` is acceptable since it's not used for filesystem operations:
```typescript
// OK for logging - consistent cross-platform log output
const logPath = pathSegments.join('/');
console.log(`[Persistence] Loading from ${logPath}`);
```
## Anti-Patterns
### Never: Manual String Concatenation
```typescript
// 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);
```
### Never: Manual Parent Directory Extraction
```typescript
// 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);
```
### Never: Hardcoded Separators in Filesystem Operations
```typescript
// BAD: Windows uses backslashes
const configPath = appDir + '/config.json';
// GOOD: Platform-agnostic
const configPath = await join(appDir, 'config.json');
```
### Never: Assuming Path Format
```typescript
// 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);
```
## Import Pattern
Always import from `@tauri-apps/api/path`:
```typescript
import {
appLocalDataDir,
dirname,
join,
basename,
extname,
normalize,
resolve,
sep,
} from '@tauri-apps/api/path';
```
## Note on Async
All Tauri path functions are **async** because they communicate with the Rust backend via IPC. Always `await` them:
```typescript
// All path operations return Promises
const baseDir = await appLocalDataDir();
const filePath = await join(baseDir, 'file.txt');
const parent = await dirname(filePath);
const separator = await sep();
```
## Filesystem Operations
Use `@tauri-apps/plugin-fs` for file operations, combined with Tauri path APIs:
```typescript
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);
}
```