Install
$ agentstack add skill-impertio-studio-tauri-2-claude-skill-package-tauri-syntax-commands ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
tauri-syntax-commands
Quick Reference
Command Definition (Rust)
| Pattern | Syntax | Notes | |---------|--------|-------| | Sync command | #[tauri::command] fn name() -> T | Runs on main thread | | Async command | #[tauri::command] async fn name() -> T | Spawned on tokio runtime | | Forced async | #[tauri::command(async)] fn name() -> T | Sync function on tokio runtime | | Renamed args | #[tauri::command(rename_all = "snake_case")] | JS passes snake_case keys |
Argument Types (Rust | T[]` | Array to Vec |
| Option | T \| null \| undefined | Nullable | | PathBuf | string | Path string | | Custom struct | object | Must #[derive(Deserialize)] |
Return Types (Rust -> JS)
| Rust Return | JS Result | Notes | |-------------|-----------|-------| | () | void / null | No return value | | T (Serialize) | T | Direct serialization | | Result | T or throw E | E must impl Serialize | | Response | ArrayBuffer | Binary data bypass |
Special Injected Parameters (NOT passed from JS)
| Rust Parameter | Purpose | Example | |----------------|---------|---------| | app: AppHandle | Application handle | app.emit("event", data) | | window: WebviewWindow | Calling window | window.label() | | state: State | Managed state | state.inner() | | request: tauri::ipc::Request | Raw IPC request | request.headers(), request.body() |
Frontend invoke() API
| Feature | Syntax | |---------|--------| | Import | import { invoke } from '@tauri-apps/api/core' | | Basic call | await invoke('command_name') | | With args | await invoke('cmd', { argName: value }) | | Typed return | await invoke('cmd') | | With headers | await invoke('cmd', args, { headers: {...} }) | | Binary data | await invoke('cmd', new Uint8Array([...])) |
Critical Warnings
NEVER use snakecase keys in JS invoke() arguments -- Tauri auto-converts camelCase to snakecase. Passing { file_path: "x" } will NOT match Rust parameter file_path.
NEVER forget to implement Serialize on error types -- Result requires E to implement Serialize to send errors to the frontend.
NEVER use &str parameters in async commands -- borrowed references cannot cross the async spawn boundary. Use String instead.
NEVER register commands with multiple invoke_handler() calls -- only the last one takes effect.
NEVER mark commands in lib.rs as pub -- glue code generation prevents it. Move to a separate module if pub is needed.
ALWAYS wrap invoke() calls in try/catch -- Rust Result::Err rejects the JS Promise.
ALWAYS use Channel for streaming data instead of multiple events -- channels are tied to the command invocation and type-safe.
ALWAYS derive serde::Deserialize on custom argument types and serde::Serialize on custom return types.
Essential Patterns
Pattern 1: Basic Command (Rust + TypeScript)
// src-tauri/src/commands.rs
#[tauri::command]
pub fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
// src/main.ts
import { invoke } from '@tauri-apps/api/core';
const greeting = await invoke('greet', { name: 'World' });
console.log(greeting); // "Hello, World!"
Pattern 2: Async Command with Error Handling
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("not found: {0}")]
NotFound(String),
}
// REQUIRED: Serialize impl for sending errors to frontend
impl serde::Serialize for Error {
fn serialize(&self, serializer: S) -> Result
where S: serde::ser::Serializer {
serializer.serialize_str(self.to_string().as_ref())
}
}
#[tauri::command]
async fn read_file(path: String) -> Result {
let content = tokio::fs::read_to_string(&path).await?;
Ok(content)
}
try {
const content = await invoke('read_file', { path: '/tmp/data.txt' });
console.log(content);
} catch (error) {
// error is the serialized string: "not found: ..." or "io error: ..."
console.error('Failed:', error);
}
Pattern 3: Structured Error Types
For richer error handling on the frontend, use tagged enum serialization.
#[derive(serde::Serialize)]
#[serde(tag = "kind", content = "message")]
#[serde(rename_all = "camelCase")]
enum AppError {
Io(String),
NotFound(String),
Unauthorized(String),
}
#[tauri::command]
async fn load_data(id: u32) -> Result {
if id == 0 {
return Err(AppError::NotFound("Item not found".into()));
}
Ok(Data { /* ... */ })
}
try {
const data = await invoke('load_data', { id: 42 });
} catch (err: any) {
// err = { kind: 'notFound', message: 'Item not found' }
switch (err.kind) {
case 'notFound':
showNotFoundUI(err.message);
break;
case 'unauthorized':
redirectToLogin();
break;
default:
console.error('Unexpected error:', err);
}
}
Pattern 4: Special Injected Parameters
These parameters are injected by Tauri -- the frontend does NOT pass them.
use tauri::{AppHandle, State, WebviewWindow};
#[tauri::command]
async fn complex_command(
app: AppHandle, // injected: app handle
window: WebviewWindow, // injected: calling window
state: State, // injected: managed state
// Regular parameters (passed from JS):
query: String,
limit: Option,
) -> Result, String> {
println!("Called from window: {}", window.label());
let config = state.inner();
// ... use app, window, state as needed
Ok(vec![])
}
// Frontend only passes the regular parameters -- injected params are omitted
const items = await invoke('complex_command', {
query: 'search term',
limit: 10,
});
Pattern 5: Channel Streaming (Rust -> JS)
use tauri::ipc::Channel;
#[derive(Clone, serde::Serialize)]
struct DownloadProgress {
bytes_read: u64,
total: u64,
percent: f64,
}
#[tauri::command]
async fn download_file(
url: String,
on_progress: Channel,
) -> Result {
let response = reqwest::get(&url).await.map_err(|e| e.to_string())?;
let total = response.content_length().unwrap_or(0);
let mut bytes_read = 0u64;
// Stream progress updates
on_progress.send(DownloadProgress {
bytes_read,
total,
percent: 0.0,
}).unwrap();
// ... read chunks, updating progress ...
Ok("download complete".into())
}
import { invoke, Channel } from '@tauri-apps/api/core';
interface DownloadProgress {
bytesRead: number;
total: number;
percent: number;
}
const onProgress = new Channel();
onProgress.onmessage = (progress) => {
updateProgressBar(progress.percent);
console.log(`${progress.bytesRead}/${progress.total}`);
};
const result = await invoke('download_file', {
url: 'https://example.com/file.zip',
onProgress, // Pass the channel as a regular argument
});
Pattern 6: Binary Data with Request/Response
Bypass JSON serialization for large binary payloads.
use tauri::ipc::{Request, Response};
// Receiving binary data from JS
#[tauri::command]
fn upload(request: Request) -> Result {
let tauri::ipc::InvokeBody::Raw(data) = request.body() else {
return Err("Expected binary data".into());
};
let auth = request.headers().get("Authorization");
println!("Received {} bytes", data.len());
Ok(format!("Uploaded {} bytes", data.len()))
}
// Returning binary data to JS
#[tauri::command]
fn get_image(path: String) -> Response {
let data = std::fs::read(path).unwrap();
Response::new(data)
}
// Sending binary data
const data = new Uint8Array([1, 2, 3, 4, 5]);
const result = await invoke('upload', data, {
headers: { Authorization: 'Bearer token123' },
});
// Receiving binary data
const imageData = await invoke('get_image', { path: '/photo.jpg' });
Pattern 7: Command Registration
// src-tauri/src/lib.rs
mod commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
commands::greet,
commands::read_file,
commands::download_file,
commands::upload,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Rule: ALL commands MUST be listed in a single generate_handler![] macro call. Multiple invoke_handler() calls do NOT merge.
Argument Name Mapping
Tauri automatically converts between JS camelCase and Rust snake_case.
| Rust Parameter | JS Argument Key | |----------------|-----------------| | file_path: String | filePath | | user_name: String | userName | | item_count: u32 | itemCount | | is_active: bool | isActive |
To override this behavior:
#[tauri::command(rename_all = "snake_case")]
fn my_command(file_path: String) { }
// With rename_all = "snake_case", pass snake_case keys
await invoke('my_command', { file_path: '/tmp/file.txt' });
Channel vs Events
| Feature | Channel | Events | |---------|---------|--------| | Direction | Rust -> JS (response stream) | Bidirectional | | Lifetime | Tied to command invocation | App lifetime | | Targeting | Specific caller | Broadcast or filtered | | Type safety | Generic Channel | Serialized JSON | | Use case | Progress, streaming data | App-wide notifications |
ALWAYS use Channel for progress updates and streaming data from a command. NEVER use events for command-specific streaming -- channels are scoped to the invocation.
Runtime Generics (Plugin Authors)
When writing commands for plugins or libraries that need to be runtime-agnostic:
use tauri::{AppHandle, Runtime, WebviewWindow};
#[tauri::command]
async fn plugin_command(
app: AppHandle,
window: WebviewWindow,
data: String,
) -> Result {
Ok(format!("Processed: {}", data))
}
With the default wry feature, R resolves to Wry. This generic is only necessary for plugin/library code.
Reference Links
- [references/methods.md](references/methods.md) -- Command macro attributes, invoke signature, Channel API
- [references/examples.md](references/examples.md) -- Sync commands, async commands, binary data, channels, errors
- [references/anti-patterns.md](references/anti-patterns.md) -- All command + invoke mistakes
Official Sources
- https://v2.tauri.app/develop/calling-rust/
- https://docs.rs/tauri/latest/tauri/command/index.html
- https://docs.rs/tauri/latest/tauri/ipc/struct.Channel.html
- https://www.npmjs.com/package/@tauri-apps/api
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Impertio-Studio
- Source: Impertio-Studio/Tauri-2-Claude-Skill-Package
- License: MIT
- Homepage: https://github.com/OpenAEC-Foundation
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.