Desarrollo de apps Tauri con Claude Code: Vite, Rust commands y permisos
Guia practica para crear una app Tauri v2 con Claude Code, Vite, Rust, invoke, permisos de archivos, tests y revision.
Tauri permite convertir una interfaz web en una aplicacion de escritorio para Windows, macOS y Linux. La interfaz puede seguir hecha con React, Svelte u otro stack frontend, mientras que el lado nativo vive en Rust. Esa separacion es precisamente lo que hace que Claude Code sea util: puedes pedir un cambio de UI, un Rust command, una regla de permisos o una prueba sin mezclar todo en un diff imposible de revisar.
En esta guia usaremos Tauri v2 y una pequena app de notas locales. Veremos cuando elegir Tauri, como iniciar con Vite + React o Svelte, como escribir commands en Rust, como llamarlos con invoke, como pensar los permisos de archivos, que comandos de build y test ejecutar, y que prompts usar para revisar permisos con Claude Code.
Las fuentes oficiales revisadas son Tauri Create a Project, Calling Rust from the Frontend, Tauri Capabilities, File System plugin, Vite Getting Started, Cargo test y Claude Code setup. Para reforzar Rust, lee Rust con Claude Code. Para comparar con Electron, revisa Electron desktop app development.
Cuando elegir Tauri
Tauri encaja cuando necesitas una app local, pero quieres conservar la velocidad de desarrollo de una UI web. Algunos casos claros son una app privada de notas, un conversor de CSV para archivos internos, un visor de logs, una utilidad de desarrollo o una herramienta offline para trabajo de campo.
No elijas Tauri solo porque suena mas ligero que Electron. Tauri tambien trae responsabilidades: toolchain de Rust, empaquetado, firma de codigo, instaladores, comportamiento distinto por sistema operativo, identificador de aplicacion y estrategia de actualizacion. Si tu producto es basicamente una web con login y APIs remotas, una web normal o una PWA puede ser mas barata de mantener.
Un buen primer prompt para Claude Code es:
Vamos a crear una app Tauri v2 de notas locales.
No implementes todavia.
Divide el trabajo en React UI, Rust commands, capabilities y build/test.
El acceso a archivos debe quedarse dentro del directorio app data.
No propongas lectura o escritura de rutas arbitrarias.
Este prompt evita que el agente empiece agregando permisos demasiado amplios antes de que exista un modelo de seguridad.
Limite de arquitectura
En Tauri, el frontend no deberia tocar el sistema operativo directamente. Llama commands de Rust mediante invoke, Rust valida la entrada, accede al recurso local y devuelve un resultado tipado.
flowchart LR
UI["React o Svelte UI"] --> Invoke["invoke from @tauri-apps/api/core"]
Invoke --> Command["Rust command"]
Command --> Guard["allowlist y validacion de ruta"]
Guard --> AppData["directorio app data"]
Command --> Result["resultado tipado"]
Result --> UI
Capability["Tauri capability"] --> UI
El punto critico es este: las capabilities de Tauri limitan el acceso del frontend a APIs y plugins de Tauri, pero no hacen seguro automaticamente tu propio Rust command. Si tu command acepta cualquier ruta y escribe en ella, sigue siendo peligroso. Por eso hay que revisar las capabilities y la validacion de rutas en Rust.
Setup con React o Svelte
Para un proyecto nuevo, el camino oficial con create-tauri-app es el mas limpio. Elige React o Svelte y usa TypeScript.
npm create tauri-app@latest taskdesk
cd taskdesk
npm install
npm run tauri dev
Si ya tienes una app Vite, crea o prepara el frontend y luego inicializa Tauri. La guia de Vite documenta npm create vite@latest y actualmente requiere Node.js 20.19+ o 22.12+, asi que confirma la version antes de culpar a Tauri por errores de build.
node --version
npm create vite@latest taskdesk -- --template react-ts
cd taskdesk
npm install
npm install -D @tauri-apps/cli@latest
npm install @tauri-apps/api@latest
npx tauri init
npx tauri dev
Para Svelte cambia solo la plantilla:
npm create vite@latest taskdesk -- --template svelte-ts
Despues del setup, pide a Claude Code leer antes de editar:
Lee package.json, tauri.conf.json y src-tauri/src/lib.rs.
Resume la estructura actual del proyecto y sus scripts.
No modifiques archivos todavia.
Esto importa porque las plantillas de Tauri cambian. Es mejor partir del proyecto generado que forzar una estructura antigua.
Configuracion minima de Tauri
tauri.conf.json conecta el servidor de desarrollo, el build frontend, la ventana y la capability usada por la app.
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "TaskDesk",
"version": "0.1.0",
"identifier": "com.example.taskdesk",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:5173",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "TaskDesk",
"width": 1000,
"height": 700
}
],
"security": {
"capabilities": ["main-capability"]
}
},
"bundle": {
"active": true,
"targets": "all"
}
}
Revisa siempre tres cosas: que identifier no sea un valor de ejemplo, que devUrl coincida con el puerto de Vite y que frontendDist apunte al output real de produccion.
Commands en Rust
La app de notas necesita leer, escribir y listar archivos. Lo importante no es la llamada a fs, sino rechazar rutas absolutas, .. y cualquier salida del directorio de datos de la app.
// src-tauri/src/note_commands.rs
use serde::Serialize;
use std::{
fs,
path::{Component, Path, PathBuf},
};
use tauri::{AppHandle, Manager};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NoteFile {
name: String,
path: String,
bytes: u64,
is_dir: bool,
}
fn reject_unsafe_relative(path: &Path) -> Result<(), String> {
for component in path.components() {
match component {
Component::Normal(_) | Component::CurDir => {}
_ => return Err("use a relative path inside app data".to_string()),
}
}
Ok(())
}
fn app_data_root(app: &AppHandle) -> Result<PathBuf, String> {
let root = app
.path()
.app_data_dir()
.map_err(|error| format!("failed to get app data dir: {error}"))?;
fs::create_dir_all(&root).map_err(|error| format!("failed to create app data dir: {error}"))?;
root.canonicalize()
.map_err(|error| format!("failed to resolve app data dir: {error}"))
}
fn existing_path(app: &AppHandle, relative: &str) -> Result<PathBuf, String> {
let root = app_data_root(app)?;
let requested = Path::new(relative);
reject_unsafe_relative(requested)?;
let full = root
.join(requested)
.canonicalize()
.map_err(|error| format!("path does not exist: {error}"))?;
if !full.starts_with(&root) {
return Err("path escapes app data".to_string());
}
Ok(full)
}
#[tauri::command]
pub fn read_note(app: AppHandle, path: String) -> Result<String, String> {
let safe_path = existing_path(&app, &path)?;
fs::read_to_string(safe_path).map_err(|error| format!("failed to read note: {error}"))
}
Registra el command en lib.rs:
// src-tauri/src/lib.rs
mod note_commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
note_commands::read_note
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Invocar desde TypeScript
Usa un wrapper pequeno para no repetir nombres de commands en cada componente.
// src/lib/notesApi.ts
import { invoke } from "@tauri-apps/api/core";
export type NoteFile = {
name: string;
path: string;
bytes: number;
isDir: boolean;
};
export const notesApi = {
read(path: string) {
return invoke<string>("read_note", { path });
},
write(path: string, content: string) {
return invoke<void>("write_note", { path, content });
},
list(dir = ".") {
return invoke<NoteFile[]>("list_notes", { dir });
},
};
Un componente React minimo puede empezar asi:
import { useState } from "react";
import { notesApi } from "./lib/notesApi";
export default function App() {
const [content, setContent] = useState("");
const [message, setMessage] = useState("Ready");
async function saveNote() {
await notesApi.write("daily-note.txt", content);
setMessage("Saved");
}
return (
<main>
<textarea value={content} onChange={(event) => setContent(event.target.value)} />
<button onClick={saveNote}>Save</button>
<p>{message}</p>
</main>
);
}
Permisos y capabilities
Si el frontend usa directamente el File System plugin, define la capability con un alcance pequeno.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Main window permissions for TaskDesk.",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-app-read-recursive",
"fs:allow-app-write-recursive"
]
}
En el ejemplo de este articulo, los archivos se manejan desde Rust commands, asi que no hace falta dar al frontend permisos amplios sobre Downloads o Documents. El prompt de revision recomendado es:
Solo revisa, no edites.
Inspecciona src-tauri/capabilities y src-tauri/src/note_commands.rs.
Lista cada API expuesta al frontend y cada Rust command.
Indica que rutas de archivo puede tocar cada una.
Marca rutas absolutas, .., wildcards amplios y escrituras fuera de app data.
Propone el conjunto minimo de permisos que mantiene la funcionalidad.
Build, tests y casos reales
Separa los comandos de verificacion:
npm run build
cd src-tauri
cargo test
cd ..
npm run tauri build
npm run build valida Vite. cargo test valida Rust. npm run tauri build valida el empaquetado de escritorio. Para rutas peligrosas, usa tests pequenos:
#[cfg(test)]
mod tests {
use super::reject_unsafe_relative;
use std::path::Path;
#[test]
fn rejects_parent_directory() {
assert!(reject_unsafe_relative(Path::new("../secret.txt")).is_err());
}
}
Tres casos de uso naturales son una app de notas o diario, un conversor CSV/Markdown y un visor de logs para desarrolladores. Un cuarto caso es una herramienta offline de inventario, inspeccion o registro de eventos. En todos, el valor no es solo la UI, sino poder trabajar con datos locales sin abrir permisos de mas.
Los errores comunes son confundir capability con seguridad del Rust command, pedir a Claude Code que haga toda la app de una vez, dejar configuracion de desarrollo en produccion, probar en un solo sistema operativo y dar permisos de archivos demasiado amplios por comodidad.
CTA y resultado probado
Si quieres convertir este flujo en una practica repetible, revisa los productos y plantillas de ClaudeCodeLab o usa Claude Code training and consultation para adaptarlo a un repositorio real. En equipos, lo importante es documentar capabilities, CLAUDE.md, prompts de revision y comandos de verificacion.
Al probar este flujo en una app pequena de notas, el orden mas estable fue definir primero el limite de rutas en Rust, luego crear el wrapper TypeScript de invoke y finalmente revisar capabilities. Empezar por la UI se siente rapido, pero suele crear retrabajo cuando aparecen las reglas de almacenamiento y permisos.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.