Generate and edit Draw.io, Mermaid, and Excalidraw diagrams from natural language using a structured JSON spec.
Security
tauri-2-app
Try itScaffold a new Tauri 2 desktop app (Rust backend + TypeScript/React frontend) using a thin-frontend / rich-Rust-backend architecture with modular commands, trait-based platform abstractions, encrypted secrets at rest, single-instance enforcement, a self-hosted updater, and a cross-platform CI matrix. Three modes: full project scaffold, add a Tauri command slice, add a Rust module slice. Use this skill whenever the user says "create a new Tauri app", "scaffold a Tauri 2 project", "new desktop app with Tauri", "Tauri + React project", "add a Tauri command end-to-end", "add a Rust module to my Tauri app", "my Tauri conventions", or "/tauri-2-app" — even if they don't name the skill. Full good-pattern catalog and pitfall list live in the skill body.
What it does
Generate a production-grade Tauri 2 desktop app that keeps the **good** patterns from a battle-tested codebase (thin TS frontend that only invokes commands, rich Rust backend with modular , , , , , plugin-based features wired in , capability JSON per window, encrypted secrets at rest, single-instan…
The skill document
Tauri 2 App Scaffolder
Generate a production-grade Tauri 2 desktop app that keeps the good patterns from a battle-tested codebase (thin TS frontend that only invokes commands, rich Rust backend with modular commands/, state/, storage/, platform/, error/, plugin-based features wired in lib.rs, capability JSON per window, encrypted secrets at rest, single-instance enforcement, updater wired to a self-hosted JSON manifest, cross-platform CI matrix, target-specific Cargo dependencies, release-profile LTO, typed frontend command hooks) and eliminates the bad ones often seen in Tauri codebases (committed .backup / .orig / .temp files in src-tauri/src/, API keys in plaintext settings JSON, tokens in localStorage, cfg!(target_os) scattered through command bodies instead of trait-based platform code, hand-rolled date math instead of chrono::Utc::now(), raw std::fs reads/writes that bypass Tauri's capability checks, blocking I/O inside #[tauri::command] without tokio::task::spawn_blocking, missing #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] in main.rs causing a console flash on Windows, multi-instance apps without tauri-plugin-single-instance, devtools: true in release config, generic catch-all String errors with no thiserror enum at module boundaries, hardcoded company-specific bundle identifiers / R2 URLs / updater pubkeys / signing identities baked into the skill).
When to use this skill
Trigger on any of:
- "create a new Tauri app" / "scaffold a Tauri 2 project" / "new desktop app with Tauri"
- "Tauri + React project" / "Tauri + vanilla TS project" / "Rust backend desktop app"
- "use my Tauri patterns" / "the Tauri conventions I like" (in a Tauri context)
- "add a Tauri command end-to-end" / "wire up a command from Rust to React"
- "add a Rust module to my Tauri app" / "add a state slice"
- The user pastes a feature spec for a desktop feature (hotkey, tray, file I/O, system notification, OS-specific behavior) and asks you to wire it through the Rust backend and frontend
If unsure whether the user wants a brand-new app vs. an addition to an existing one, ask once — don't guess.
Three operating modes
Pick the mode from the user's request. If ambiguous, ask.
| Mode | Trigger | Output |
|---|---|---|
scaffold-app | "new Tauri app", "scaffold Tauri 2 project", empty directory | Full Tauri 2 app: package.json, vite.config.ts, tsconfig.json, index.html, src/ (React or vanilla TS), src-tauri/ with modular commands/, state/, storage/, platform/, error/, settings/, capability JSON, Info.plist + entitlements.plist (macOS), build.rs, GitHub Actions CI, single-instance + updater plugins wired. |
add-command | "add a `` command end-to-end", "wire up a command from Rust to TS" | New #[tauri::command] in the right src-tauri/src/commands/*.rs module, registered in lib.rs's invoke_handler, granted permissions in capabilities/default.json if it uses a plugin, plus a typed frontend hook in src/hooks/ that wraps invoke() with loading/error state. |
add-module | "add a `` module", "add a state/storage slice for X" | New src-tauri/src//mod.rs (+ optional submodules), with serde-derived types, a thiserror error enum, JSON persistence via the shared storage module, and a unit-test module + an integration test file in src-tauri/tests/. |
Workflow
Step 1 — Resolve versions (don't hard-code)
The user does not want hard-coded Cargo / npm versions baked into the skill. Before writing Cargo.toml or package.json:
- Check the user's environment first: run
cargo --version,rustc --version,node --version,npm --version(orpnpm --version). - Resolve the latest stable versions of the core stack at scaffold time — never hand-paste:
- Rust crates (look up on crates.io or via context7):
tauri(must be 2.x),tauri-build(2.x),tauri-plugin-opener,tauri-plugin-notification,tauri-plugin-dialog,tauri-plugin-fs,tauri-plugin-global-shortcut,tauri-plugin-single-instance,tauri-plugin-updater,tauri-plugin-process,serde,serde_json,thiserror,anyhow,tokio(withsync,rt-multi-thread,macros),tracing,tracing-subscriber(withenv-filter),chrono(don't hand-roll date math),dirs. - npm packages:
@tauri-apps/api(2.x),@tauri-apps/cli(2.x),@tauri-apps/plugin-*matching the Rust plugins,react+react-dom(if React),typescript,vite,@vitejs/plugin-react(if React).
- Rust crates (look up on crates.io or via context7):
- Pin the Rust edition to
2021unless the user asks otherwise. - Quote the resolved versions back to the user before generating, so they can object.
- Default to npm unless
pnpmorbunis present and the user prefers it. - Concrete resolution commands when context7 is unavailable:
npm view @tauri-apps/api version,cargo search --limit 1, or WebFetchhttps://crates.io/api/v1/crates/(max_stable_version). Never write a version you did not just resolve this session — no versions from memory, no invented numbers. - Tauri 1.x is poison. Training data is full of v1 patterns that look plausible and fail on v2. Never emit:
import { invoke } from '@tauri-apps/api/tauri'(the v2 path is@tauri-apps/api/core), atauri.allowlistor top-leveltaurikey intauri.conf.json(v2 usesapp/bundle/plugins+capabilities/*.json), or v1 plugin names/APIs. If you are not certain a conf key, capability permission string, or plugin API exists in Tauri 2, verify it against the installed schema (node_modules/@tauri-apps/cli/config.schema.json; valid permission strings appear insrc-tauri/gen/schemas/after the first build) or the plugin's docs before writing code that depends on it. Capabilitypermissionsentries use the short plugin name (dialog:allow-open,fs:allow-read-file) — never atauri-plugin-prefix. Note thatcargo checkcompilestauri-build, which validatestauri.conf.jsonandcapabilities/*.json— a schema or unknown-permission error there is a JSON problem; fix the conf/capability file against the schema, don't edit Rust code to appease it.
Step 2 — Gather inputs (ask once, in one batch)
For scaffold-app, use AskUserQuestion to collect:
- App name (kebab-case for the directory; the productName in
tauri.conf.jsoncan be Title Case). - Bundle identifier in reverse-DNS form (e.g.
com.example.myapp) — must come from the user, never hardcoded. This goes intauri.conf.jsonidentifierand the macOSInfo.plistCFBundleIdentifier. - Frontend flavor: React + Tailwind, React (no Tailwind), vanilla TypeScript, or "I'll wire it up myself".
- Plugins to wire: opener, notification, dialog, fs, global-shortcut, single-instance, updater, process. Default-ON:
opener,dialog,fs,single-instance. Updater is OFF by default — only wire it if the user has (or will have) a manifest URL. - Tray icon (yes/no). If yes, generate a minimal tray with Show/Hide/Quit.
- Encrypted-secrets module (yes/no). If yes, generate an
encryption/module usingaes-gcm+argon2for at-rest encryption of API keys/tokens. The salt-derivation source (machine UUID, keychain, etc.) is platform-specific — generate stubs, but do not ship a hardcoded "pepper" string. - Updater endpoint (optional). If provided, write it into
tauri.conf.jsonunderplugins.updater.endpoints. Never invent a URL or paste a public key from another project — ask the user to generate one withtauri signer generateand paste it in afterwards. - CI (default ON): GitHub Actions matrix building on macOS/Windows/Ubuntu.
- First command/module (optional) — if provided, also run the matching mode after scaffold.
For add-command: command name (snake_case), the module it belongs in (commands/.rs), inputs + return type, whether it's async, whether it needs AppHandle / State / a Tauri plugin. If the command does blocking I/O, confirm it needs tokio::task::spawn_blocking.
For add-module: module name (snake_case), the data type(s) it owns, whether it persists to JSON in app_data_dir, what error variants it needs.
Step 3 — Generate files
Use the templates in references/templates/ as the source of truth. Apply these rules:
- Use
Writefor new files. Never useEditon files you're creating fresh. - Replace all
{{AppName}},{{app_name}},{{BundleId}},{{Command}},{{Module}}placeholders consistently.{{app_name}}is snake_case for Rust crate / lib names;{{AppName}}is Title Case for product name; the directory is kebab-case. - Create directories before files. On Windows shell use PowerShell
New-Item -ItemType Directory -Force. - Do not run
create-tauri-appto bootstrap — write the files directly from templates so the layout matches the conventions inreferences/folder-layout.md. Usenpm installafterpackage.jsonis written, thencargo fetchfromsrc-tauri/to seed the Cargo cache. - For icons, run
npx @tauri-apps/cli icononce a source image exists, or generate a 1024×1024 placeholder withsharpif the user wants. Do not commit a placeholder square as the final icon — surface it as a TODO. - If a generator IS run (
create-tauri-appat the user's insistence,npx @tauri-apps/cli icon,tauri signer generate) and its output differs from this skill's layout: Tauri wins on file locations it mandates (src-tauri/shape,capabilities/,gen/, icon paths), this skill wins on everything Tauri doesn't mandate (thecommands//state//storage//platform/module split, frontend hooks layout). Reconcile deliberately and list every deviation in your report — never force the skill layout over a framework requirement, never silently abandon the skill's patterns.
Step 4 — Verify and report
- Run
npm install(or chosen PM). - Run
cd src-tauri && cargo build— must exit 0. (Don't runtauri devin CI — it opens a window.) If a fullcargo buildis prohibitively slow in this environment,cargo checkis the minimum acceptable bar — state which one you ran. - Run
cd src-tauri && cargo test— must exit 0 if any tests were generated. - Run
npm run build(frontend) — must exit 0.cargo check+npm run buildtogether are the floor; a fulltauri buildis optional (slow). - Optionally run
npm run tauri build -- --debugif the user wants a debug bundle; skip in CI by default since it's slow. - A scaffold that hasn't compiled is not delivered. Paste the actual proof lines in your report (cargo's
Finishedprofile line,test result: ok, Vite's✓ built in ...). Never report success from memory of the steps you intended, and never report partial success as success — if something is red, say exactly what is red. - CLI failure protocol (applies to
npm install,cargo build/check/test,npm run build): read the full error output — for cargo, fix the first error; later ones are usually cascade. Change exactly one thing, retry once. If the same step fails twice, stop scaffolding and surface the verbatim error to the user — do not keep generating files on a broken base, and do not re-run the identical command hoping for a different result. - Reply with a short summary: project path, versions chosen, plugins wired, bundle identifier, next steps (e.g. "fill in the macOS
Info.plistusage descriptions for any permissions you'll request", "generate updater keys withnpx tauri signer generate", "runnpm run tauri dev").
Project layout (canonical)
See references/folder-layout.md for the full tree, file-by-file purpose, and the rules that govern it.
Top-level shape:
{{app-name}}/
package.json
tsconfig.json # strict: true, noUnusedLocals, noUnusedParameters
vite.config.ts # port 5173, ignores src-tauri, base: './'
index.html # anti-flash theme script if dark/light theming is in scope
.gitignore # MUST include src-tauri/target/, dist/, .env, *.backup, *.orig, *.temp
.env.example # NEVER .env — only .env.example committed
src/ # frontend (TS / React)
main.tsx # entry: createRoot + render
App.tsx
tauriReady.ts # isTauriReady() guard
hooks/
useTauriCommand.ts # generic typed invoke() wrapper with loading/error
use.ts # per-domain hooks composing useTauriCommand
components/
styles/
src-tauri/
Cargo.toml # [lib] name = "{{app_name}}_lib", target-specific deps via [target.'cfg(...)']
build.rs # links macOS frameworks, Windows libs, Linux pthread/m as needed
tauri.conf.json # productName, identifier, bundle, plugins.updater (if enabled)
Info.plist # macOS bundle identifier + usage descriptions for permissions requested
entitlements.plist # macOS hardened-runtime entitlements (audio-input, apple-events, etc.)
capabilities/
default.json # main-window capability with granular permission scopes
icons/ # icon.png (1024), icon.ico, icon.icns, 32x32.png, 128x128.png, 128x128@2x.png
src/
main.rs # #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + embed_plist on macOS
lib.rs # pub mod ...; pub fn run() builds Tauri, registers plugins + invoke_handler
commands/ # ONE file per domain (recording.rs, settings.rs, system.rs, ...)
mod.rs # pub mod ...; pub use *;
system.rs # generic commands (write_file, dialogs, notifications)
.rs # per-domain command modules
state/ # pub struct AppState { ... } managed via tauri::Manager
mod.rs
storage/ # JSON-on-disk utilities: get_app_data_dir, load_json, save_json
mod.rs
settings/ # Settings struct (camelCase serde) + load/save + migrations
mod.rs
defaults.rs
storage.rs
commands.rs
platform/ # trait-based platform abstractions
mod.rs
traits.rs # PermissionChecker, FileOpener, AutostartManager
macos.rs # #[cfg(target_os = "macos")]
windows.rs
linux.rs
wrappers.rs # PlatformXxx wrappers managed in Tauri State
error/ # into_string_err! macro + ResultExt trait
mod.rs
encryption/ # (optional) AES-GCM + Argon2id for secrets at rest
mod.rs
tray.rs # (optional) tray icon, menu, click handlers
tests/ # integration tests; one file per domain
common/
mod.rs
_test.rs
.github/
workflows/
unit-tests.yml # cross-platform matrix: cargo test on macos/windows/ubuntu
publish.yml # (optional) tag-triggered release pipeline; user fills in CDN secrets
Dependency rules (enforced):
commands/files call intostate/,settings/,storage/,platform/,/modules — never the other way.- Domain modules (
export,history,sync, …) may depend onstorage/andplatform/traits, but not oncommands/or Tauri's runtime types directly (useAppHandleonly when persisting viastorage::get_app_data_dir). platform/.rsfiles are gated with#[cfg(target_os = "...")]and only these files containcfg!(target_os)checks. All other modules go throughplatform::traits::*and aPlatformwrapper managed inState.- Frontend
hooks/use.tsfiles composeuseTauriCommand— they never callinvoke()directly inline in a component.
Required code patterns
Full templates are in references/templates/. The full Keep/Eliminate rationale is in references/good-patterns.md and references/anti-patterns.md.
Keep
- Thin frontend, rich Rust backend. All file I/O, audio, system permissions, OS-specific behavior, and long-running work happen in Rust. The TS layer only invokes commands and renders state.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]inmain.rsto prevent a console window on Windows in release.embed_plist::embed_info_plist!("../Info.plist")inmain.rsunder#[cfg(target_os = "macos")]so the binary carries its plist when run standalone.tauri-plugin-single-instanceregistered first in the builder chain, with a callback that focuses the existing window when a second instance launches. Seereferences/templates/lib-rs.md.- Modular
commands/— one file per domain, all re-exported fromcommands/mod.rs, registered in a singletauri::generate_handler![...]call inlib.rs. Statefor shared app state managed viaapp.manage(Mutex::new(AppState::new())). Inside async commands, hold the lock for the minimum time and drop it before any.await.- Trait-based platform abstractions (
PermissionChecker,FileOpener,AutostartManager) inplatform/traits.rs, with#[cfg(target_os = "...")]implementations inplatform/macos.rs,windows.rs,linux.rs, andSend + Syncwrappers inplatform/wrappers.rsmanaged in Tauri State. Tests assertSend + Syncon the wrappers. storage/mod.rsshared utilities —get_app_data_dir,get_storage_path,load_json,save_json,generate_id,Timestampedtrait,prune_entries_by_age— used bysettings/,history/,error_log/, and any other persistent JSON store.- Settings with
#[serde(rename_all = "camelCase", default)]so the on-disk JSON matches frontend naming, and missing fields fall back toDefault. Add a migration pass indeserialize_settings(json: &Value) -> Settingswhen fields change shape. thiserrorenums at module boundaries;anyhow::Resultonly inside private helpers. Commands returnResultwith errors stringified at the#[tauri::command]boundary — use theinto_string_err!macro /ResultExt::into_string()fromerror/mod.rsto keep this clean.tokio::task::spawn_blockingfor any synchronous I/O or CPU-bound work inside anasynccommand — file pickers, native dialogs, ML inference, encryption. Awaiting blocking work directly stalls Tauri's IPC runtime.tracing+tracing-subscriberfor logs, initialized inpub fn run()before the builder. UseEnvFilter::from_default_env().add_directive(Level::INFO.into())soRUST_LOG=debugworks.- Encrypted secrets at rest (
encryption/mod.rs): AES-256-GCM ciphertext + Argon2id key derivation + platform-specific hardware-bound salt (machine UUID on macOS viaioreg IOPlatformExpertDevice, Windows viawmic csproduct get uuid, Linux via/etc/machine-idormachine-uidcrate). StoreEncryptedApiKey { ciphertext, nonce, salt }in settings. Auto-migrate legacy plaintext keys on load. capabilities/default.jsonwith granular permissions — only the scopes the app actually uses (e.g.fs:allow-read-file,dialog:allow-open), not blanket"*". One capability file per window (e.g.default.jsonformain,.jsonfor any auxiliary windows).- macOS
Info.plistusage descriptions for every permission the app requests (NSMicrophoneUsageDescription,NSAccessibilityUsageDescription, etc.). The app will silently fail without them. - macOS
entitlements.plistfor hardened runtime with only what's needed (com.apple.security.cs.allow-jit,com.apple.security.device.audio-input,com.apple.security.automation.apple-events— but only if used). build.rsthat links platform frameworks explicitly (Accelerate,Metal,CoreGraphics,Foundation,ApplicationServiceson macOS;user32on Windows;pthread,mon Linux). UseCARGO_CFG_TARGET_OSat build time, not host OS.- Target-specific Cargo deps via
[target.'cfg(target_os = "macos")'.dependencies]for platform crates (security-framework,cocoa,winapi,windows-sys,machine-uid). The default deps list stays portable. - Release profile in
Cargo.toml:lto = "fat",codegen-units = 1,opt-level = 3,strip = true,panic = "abort". Dev profile:opt-level = 1for tolerable rebuild times. - Vite config tuned for Tauri:
base: './'(relative paths so the bundled HTML loads fromtauri://localhost),server.port: 5173,server.strictPort: true,server.watch.ignored: ['**/src-tauri/**'],clearScreen: falseso Rust errors aren't hidden. - TypeScript
strict: truewithnoUnusedLocals,noUnusedParameters,noFallthroughCasesInSwitch, andisolatedModules. - Typed frontend
useTauriCommandhook insrc/hooks/useTauriCommand.tsthat wrapsinvoke(name, args)withdata,isLoading,error, andexecute. Per-domain hooks (useSettings,useRecording, …) compose it; components never callinvoke()inline. Seereferences/templates/use-tauri-command.md. isTauriReady()guard insrc/tauriReady.ts— frontend code that runs before the Tauri IPC bridge is ready (e.g. early auto-load) must call it.- GitHub Actions cross-platform matrix that runs
cargo teston macOS, Windows, and Ubuntu. Ubuntu needslibgtk-3-dev libayatana-appindicator3-dev pkg-config. tauri.conf.jsonwindows[].devtools: false(or omitted — Tauri defaults to off in release). DevTools should be opened from a build-time feature flag, not the production config.- Reset-state-on-startup pattern — in
pub fn run()'s.setup(|app| { ... }), clear any in-progress / stuck state from a previous run before showing the window. Crashes leave globals in odd places; treat each startup as recovering from "the app was force-quit". - Show window FIRST in
.setup()before any blocking initialization. Heavy work (ML model loading, large config parses) goes on a background thread that emits events back to the frontend. - Minimal comments in generated code (Rust and TS). Default to no comments. Only add one when the why is non-obvious —
// SAFETY:on anunsafeblock, a workaround for a specific upstream bug (with a link), a non-trivial invariant the code depends on, a platform quirk that isn't visible from the names. Never write Rust doc-comment blocks (///) on internal items; reserve them for genuinely public API surface (pubtypes crossing crate boundaries). Never restate what the next line does, never leave// TODOwithout an issue link. One short line max — no multi-line comment blocks.// SAFETY:onunsafeis the one place verbose justification is required; everywhere else, well-named identifiers carry the what and comments earn their place only when they carry why.
Eliminate (anti-patterns)
Every one of these is forbidden in generated code. Rationale for each is in references/anti-patterns.md.
- ❌ Committed
*.backup,*.orig,*.tempfiles insrc-tauri/src/(e.g.lib.rs.backup,macos.rs.orig). Add them to.gitignoreand never commit. These are merge-conflict / WIP artefacts, not source of truth. - ❌ Plaintext API keys / OAuth tokens in
settings.json. If the app stores any secret, route it throughencryption/and persist onlyEncryptedApiKey { ciphertext, nonce, salt }. Auto-migrate any plaintext encountered on load. - ❌ Tokens in browser
localStorage/sessionStorage. Anything sensitive lives in Rust-side encrypted storage; the frontend asks for it only when needed. - ❌
devtools: truein the releasetauri.conf.json. Gate dev tools behind a build-time feature or a separatetauri.dev.conf.json. - ❌
cfg!(target_os = "...")checks scattered through command bodies. Platform-specific behavior goes through aplatform::traits::*impl +cfg(target_os = "...")modules; commands stay portable. - ❌ Hand-rolled date / leap-year / ISO-8601 parsing. Use
chrono::Utc::now(),DateTime::parse_from_rfc3339,Duration::days(n). The "hand-rolled time math" pattern is an anti-pattern even when it works. - ❌ Raw
std::fs::read/std::fs::writeagainst user-chosen paths from inside a#[tauri::command]. Either go throughtauri-plugin-fs(which respects capability scopes) or restrict raw I/O to paths derived fromapp_handle.path().app_data_dir(). - ❌ Blocking I/O inside
async fncommands withouttokio::task::spawn_blocking. The Tauri IPC runtime is shared — a blocked task stalls all command throughput. - ❌
#[tauri::command] fn long_running(...) -> Result<...>that holds astd::sync::Mutexlock across an.await. Usetokio::sync::Mutex, or drop the guard before awaiting. - ❌ Missing
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]at the top ofmain.rs. Without it, a console window flashes behind the Tauri window on Windows in release. - ❌ App built without
tauri-plugin-single-instance. Double-clicking the app launches a second process, both fighting over global hotkeys, audio devices, sockets, and lock files. - ❌ Builder calls in
lib.rsthat bootinit_*before showing the window — model loading, audio device enumeration, network calls — block the user from seeing UI for seconds. - ❌ Empty
catch {}/let _ = result;swallowing errors silently. Either log viatracing::warn!/error!or propagate via?. - ❌ Mutex-locking the AppState for the duration of a command. Read or clone what you need, drop the guard, then do the work.
- ❌ Hardcoded bundle identifiers, R2 / Cloudflare / S3 URLs, updater public keys, Apple Team IDs, Windows certificate thumbprints in any generated file. These are user-specific; ask, don't assume.
- ❌ Inventing a Tauri updater pubkey. The user generates one with
npx tauri signer generate -w ~/.tauri/myapp.keyand pastes the public key intotauri.conf.json. Never paste another project's pubkey. - ❌
Stringerrors at module boundaries (fn foo() -> Result). Internal APIs usethiserrorenums; only#[tauri::command]returns convert toResultat the IPC boundary. - ❌ Multiple
createRoot(...)calls, or rendering React before the DOM root is wired. One entry point,main.tsx, renders into#appafterindex.htmlis parsed. - ❌ Frontend invoking
@tauri-apps/api/coreinvoke()inline in components instead of through a typed hook. Loses TS safety, scatters error handling, makes refactors painful. - ❌ Capability files with blanket permissions (
"core:default"only, no granular scopes; or"fs:default"withoutallow-read-file/allow-write-fileactually being used). Capabilities should describe exactly what the app needs and nothing more. - ❌
tauri.conf.jsonapp.security.csp: nullleft in production without an explicit decision. If CSP is genuinely too restrictive for the app, add a CSP that whiteliststauri://localhost+ your trusted origins, and document the choice. - ❌ Bundling external binaries (FFmpeg, ML models) without listing them in
tauri.conf.jsonbundle.externalBin/bundle.resources. They'll work incargo runbut be missing in the installer. - ❌ macOS
Info.plistmissingNSMicrophoneUsageDescription/NSAccessibilityUsageDescription/NSCameraUsageDescriptionfor permissions the app actually requests. The OS will refuse the prompt and the call returns "denied" silently. - ❌ macOS
entitlements.plistrequesting entitlements the app doesn't use (com.apple.security.cs.allow-unsigned-executable-memory,com.apple.security.cs.allow-jit) — they break notarization or weaken the sandbox. Only enable what's required. - ❌ Two binaries in
Cargo.toml([[bin]]× 2) without a clear sidecar reason. If the app has one main binary, keep one[[bin]]. Sidecars get their own crate inside a Cargo workspace, not a second[[bin]]in the same crate. - ❌
.envcommitted to git. Only.env.exampleis tracked;.envis in.gitignore.
Operating-mode playbooks
Mode 1 — scaffold-app
- Resolve versions per Step 1. Quote them.
- Ask the inputs per Step 2. Wait for answers.
- Generate, in this order:
-
Root:
package.json,tsconfig.json,tsconfig.node.json,vite.config.ts,index.html,.gitignore,.env.example,README.md,.github/workflows/unit-tests.yml. -
Frontend:
src/main.tsx(ormain.ts),src/App.tsx,src/tauriReady.ts,src/hooks/useTauriCommand.ts,src/styles.css. Tailwind config +postcss.config.jsonly if the user chose Tailwind. -
src-tauri/Cargo.toml(with target-specific deps + release profile),src-tauri/build.rs,src-tauri/tauri.conf.json,src-tauri/Info.plist,src-tauri/entitlements.plist,src-tauri/capabilities/default.json. -
src-tauri/icons/— placeholder 1024×1024 PNG only if the user hasn't supplied one. Surface as TODO; tell them to runnpx @tauri-apps/cli icon icons/icon.png. -
src-tauri/src/main.rs,src-tauri/src/lib.rs,src-tauri/src/error/mod.rs,src-tauri/src/state/mod.rs,src-tauri/src/storage/mod.rs,src-tauri/src/commands/mod.rs+src-tauri/src/commands/system.rs,src-tauri/src/platform/{mod,traits,macos,windows,linux,wrappers}.rs,src-tauri/src/settings/{mod,defaults,storage,commands}.rs.Checkpoint: run
npm installthen a firstcd src-tauri && cargo checkhere, before optional modules — errors localize to the core tree, and this check runstauri-build, which validatestauri.conf.json+capabilities/*.jsonand generatessrc-tauri/gen/schemas/(the authority for valid permission strings). Fix any conf/capability error now, per the Step-4 failure protocol, before writing more capability entries. -
Optional:
src-tauri/src/encryption/mod.rs,src-tauri/src/tray.rs,src-tauri/src/commands/.rsbased on user answers. -
src-tauri/tests/common/mod.rs+ at least one integration test file (e.g.system_test.rs) so CI has something real to run.
-
npm install.cd src-tauri && cargo build && cargo test— must succeed.npm run build— must succeed (catches TS / Vite errors).- If a first command/module was requested, run Mode 2 or Mode 3 for it.
- Report.
Mode 2 — add-command
For command {{command}} (snake_case) in module commands/{{domain}}.rs:
- Rust:
- Add (or extend)
src-tauri/src/commands/{{domain}}.rswith a#[tauri::command]async fn. - If the function does blocking work, wrap it in
tokio::task::spawn_blocking(move || { ... }).await.map_err(|e| e.to_string())?. - If it needs
AppHandle, take it as a parameter (Tauri injects). If it needs shared state, takeState<'_, Mutex>. - Errors: return
Result. Internally usethiserror+ theinto_string_err!macro fromerror/mod.rsto convert at the boundary. - Re-export from
commands/mod.rs(pub use {{domain}}::*;). - Register in
lib.rsinsidetauri::generate_handler![..., {{command}}].
- Add (or extend)
- Capabilities: if the command uses a plugin (notification, fs, dialog, global-shortcut), add the specific permission scope to
capabilities/default.json(e.g."dialog:allow-open"). Do not widen todefaultif a narrower scope works. - Frontend:
- Add a typed wrapper in
src/hooks/use{{Domain}}.tsthat composesuseTauriCommand({ command: "{{command}}" }). - Export the hook. Do not call
invoke()inline in a component.
- Add a typed wrapper in
- Test: add an integration test in
src-tauri/tests/{{domain}}_test.rsthat exercises the command's underlying function (the#[tauri::command]itself is hard to call without a full Tauri context — test the inner function).
After generating: cd src-tauri && cargo build && cargo test then npm run build from the project root. Apply the Step-4 proof-and-failure protocol — paste the green lines; the same step failing twice = stop and surface the verbatim error.
Mode 3 — add-module
For module {{module}} (snake_case):
- Create
src-tauri/src/{{module}}/mod.rs(split into submodules if multiple concerns). - Define data types with
#[derive(Debug, Clone, Serialize, Deserialize)]+#[serde(rename_all = "camelCase")]so JSON matches the frontend. - Define a
thiserrorerror enum:#[derive(Debug, thiserror::Error)] pub enum {{Module}}Error { ... }. - If the module persists JSON to disk, use the shared
storagemodule —get_app_data_dir,load_json,save_json. Don't roll your own file I/O. - Add
pub mod {{module}};tolib.rs. Re-export the public types you want callable from commands (pub use {{module}}::{Type, OtherType};). - Add a
#[cfg(test)]mod tests { ... }block at the bottom ofmod.rscovering pure logic (no Tauri context required). - Add a
src-tauri/tests/{{module}}_test.rsintegration test if the module has cross-cutting behavior.
After generating: cd src-tauri && cargo build && cargo test — Step-4 proof-and-failure protocol applies.
Cargo + npm packages (resolve latest stable at scaffold time)
Look these up at scaffold time — do not hand-paste versions:
Cargo (always)
tauri(2.x),tauri-build(2.x)serde,serde_json,thiserror,anyhowtokio(features:sync,rt-multi-thread,macros)tracing,tracing-subscriber(env-filter)chronodirs
Cargo (conditional, only if used)
tauri-plugin-opener,tauri-plugin-notification,tauri-plugin-dialog,tauri-plugin-fstauri-plugin-global-shortcut,tauri-plugin-single-instance,tauri-plugin-updater,tauri-plugin-processaes-gcm,argon2,base64,rand(encryption module)reqwest(withrustls-tls,json) — only if the app makes HTTP callstokio-tungstenite(withrustls-tls-native-roots) — only if WebSocket neededregex(withdefault-features = false,features = ["std", "perf"])embed_plist(macOS only, gated inmain.rs)
Cargo (target-specific)
- macOS:
security-framework,objc,cocoa,core-graphics,block - Windows:
winapi,windows-sys(with the specific feature flags the code uses, not blanket) - Linux:
machine-uid(only if the encryption module needs a machine ID)
Dev-dependencies
mockall,mockito(only if the code is structured for mocking)tokio-test,tempfile,assert_matches,pretty_assertionsproptest(only if a domain has property-test-shaped invariants)serde_yaml(only if tests parse CI YAML)
npm
@tauri-apps/api(2.x),@tauri-apps/cli(2.x)@tauri-apps/plugin-*matching the wired Rust plugins- React stack only if chosen:
react,react-dom,@types/react,@types/react-dom,@vitejs/plugin-react vite,typescript- Tailwind only if chosen:
tailwindcss,postcss,autoprefixer,@fontsource/*for any fonts used rimraf(clean scripts)sharponly if the user wants icon generation locally
Verification checklist before reporting "done"
Run this as a mechanical pass over the generated tree — grep, don't recall. The Eliminate list is easy to hold at file 1 and forgotten by file 30. Start with this grep block; every command must return nothing:
grep -rn "cfg!(target_os" src-tauri/src --include='*.rs' | grep -v "src/platform/" # platform checks outside platform/
grep -rn "@tauri-apps/api/tauri" src/ # v1 import path (v2 is /core)
grep -n "allowlist\|systemTray" src-tauri/tauri.conf.json # v1 config schema leaking in (v2: capabilities/, app.trayIcon)
grep -rn "tauri::api::" src-tauri/src --include='*.rs' # v1 Rust API paths (v2 moved these to plugins)
grep -rnE "pub (api_key|token|secret)\w*: String" src-tauri/src/ # plaintext secret fields (must be EncryptedApiKey)
grep -rn "localStorage.setItem\|sessionStorage.setItem" src/ # tokens/secrets in web storage
grep -n "\"devtools\": true" src-tauri/tauri.conf.json # devtools enabled in release config
grep -rn "std::fs::" src-tauri/src/commands/ # raw fs in command bodies
find src-tauri/src -name "*.backup" -o -name "*.orig" -o -name "*.temp" # WIP artefacts
A hit means fix it and re-run the grep — never rationalize it away. Then the full checklist:
-
npm installsucceeds. -
cd src-tauri && cargo buildexits 0. -
cd src-tauri && cargo testexits 0 (if tests were generated). -
npm run buildsucceeds (frontend bundles). -
npm run tauri build -- --debugsucceeds if the user wants a smoke check on the bundler (optional, slow). -
main.rscontains#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]. -
lib.rsregisterstauri_plugin_single_instance::initfirst in the builder chain when single-instance is wired. -
tauri.conf.jsonidentifieris not empty and matches what the user provided — not a genericcom.example.*. -
tauri.conf.jsonplugins.updater.pubkeyis empty / removed unless the user pasted a real one. Never invent a pubkey. -
tauri.conf.jsonapp.windows[].devtoolsisfalse(or omitted). -
capabilities/default.jsonhas granular permission scopes — no blanket allows beyondcore:default. -
Info.plisthas a usage description for every permission the generated code requests. -
.gitignoreincludessrc-tauri/target/,dist/,node_modules/,.env,*.backup,*.orig,*.temp. - No
.backup/.orig/.tempfiles exist anywhere undersrc-tauri/src/. - No
cfg!(target_os = "...")appears insrc-tauri/src/commands/**orsrc-tauri/src//**— only insrc-tauri/src/platform/**. - No
localStorage.setItem("token"/sessionStorage.setItem("token"insrc/. - No raw
std::fs::write(&user_supplied_path, ...)in command bodies (must go through plugin orapp_data_dir). - All
#[tauri::command]async fns that do blocking work usetokio::task::spawn_blocking. - No
cfg_if!or#[cfg]guard aroundembed_plist::embed_info_plist!is missing on macOS. -
Cargo.toml[profile.release]haslto,codegen-units,opt-level,strip,panicset. - GitHub Actions matrix workflow has macOS, Windows, and Ubuntu, and Ubuntu installs the GTK/AppIndicator system deps.
If any check fails, fix before reporting. Don't claim success with a known-broken scaffold.
Examples
Example 1: Fresh project
User: "Scaffold a new Tauri 2 desktop app called acme-notes. React frontend with Tailwind. Bundle identifier com.acme.notes. Wire single-instance, dialog, fs, opener. Skip updater for now."
Claude:
- Runs
cargo --version,rustc --version,node --version. Resolves latest stable Tauri 2.x, React, Vite, Tailwind versions via context7 / crates.io. - Reports chosen versions; waits for confirmation if anything looks off.
- Asks the Step-2 questions for any inputs not already provided (tray? encryption? first command?).
- Generates the full project skeleton per Mode 1.
- Runs
npm install,cargo build,cargo test,npm run build— all must pass. - Reports the tree, the bundle identifier set, and next steps ("create your icon source and run
npx @tauri-apps/cli icon icons/icon.png", "runnpm run tauri dev").
Example 2: Add a command
User: "Add a get_disk_usage command to my Tauri app that returns the size of a directory."
Claude: Runs Mode 2. Adds #[tauri::command] pub async fn get_disk_usage(path: String) -> Result to commands/system.rs (or a new commands/disk.rs if it warrants its own module), wraps the recursive walk in tokio::task::spawn_blocking, registers it in lib.rs, adds a typed hook useDiskUsage to src/hooks/, and writes a unit test for the inner function. Builds and tests.
Examp
Related skills
Find why your productivity system keeps failing, then apply the smallest fix — capacity math, bottleneck routing, durable local notes.
Stores durable facts in a categorized, plain-markdown vault on disk, alongside your agent's built-in memory.
Write, debug, and tune Playwright specs with locator strategy, trace diagnosis, and CI-aware timeouts.
Fetch raw ad creative, app, ranking, and revenue data from AdMapix as structured JSON.
Join a video meeting as an AI bot with voice, avatar, and screenshare across four operating modes.
More from dennisrongo
Browse all skillsSurface architectural friction in a codebase and propose **deepening opportunities** — refactors that turn shallow modules into deep ones, informed by the project's `CONTEXT.md` glossary and `docs/adr/` decisions. Walks the codebase with an Explore sub-agent, applies the **deletion test** to suspected pass-through modules, presents a numbered list of candidates with files / problem / solution / benefits, and drops into a grilling loop once the user picks one — naming new concepts into `CONTEXT.md` inline and offering an ADR only when a rejection is load-bearing. Writes no production code. Use this skill whenever the user says "improve architecture", "improve the architecture", "architecture review", "find refactoring opportunities", "find deepening opportunities", "find shallow modules", "make this more testable", "this code is hard to navigate", or invokes `/improve-codebase-architecture` — even if they don't name the skill.
Write and run E2E tests for React Native / Expo apps using Maestro CLI — the open-source tool that drives the **real native app** on an Android emulator or iOS simulator, not a web build. Covers what browser-based e2e (Playwright, Expect, e2e-verify) cannot reach: native components, biometrics, secure storage, push notifications, platform-specific modules. Write YAML flows, run headed for authoring or headless for CI. Scripts currently automate the Android workflow end-to-end (Java + SDK detection, emulator boot, adb reverse); iOS simulator setup requires Xcode + `xcrun simctl` and is documented but not automated by the setup script. Use this skill whenever the user says "test my react native app", "e2e test the mobile app", "write maestro tests", "test the expo app", "automate emulator testing", or "/maestro-mobile-test" — even if they don't name the skill. Not for web apps (e2e-verify), unit/component tests (write-tests with RNTL + Jest), or API contract tests.
Verify a change end-to-end in a real browser, routed by one question — who needs this check to run again? Ephemeral checks run via Expect (millionco/expect) or browser-use (AI agent driving headless Chromium — references/browser-use.md) when installed, else Claude driving the browser directly; regression-critical flows (auth, money, signup, checkout, deletion) get durable Playwright tests under write-tests discipline. Evidence rule either way — an AI-walked flow yields "no issues found in the paths walked" with observations quoted, never "e2e passes". Safety gate: never production, never real user cookies. Use this skill whenever the user says "verify this in the browser", "test it end to end", "test my web app", "e2e test this", "write playwright tests", "run expect", "run browser-use", "smoke test the UI", or "/e2e-verify" — even if they don't name the skill. Not for native mobile (maestro-mobile-test), unit/integration authoring (write-tests), or debugging a failing e2e test (diagno
Pre-commit SQL code review for uncommitted database changes. Detects antipatterns that cause real production incidents — broken TRY/CATCH error handling, swallowed errors, retry patterns without loops, tables missing PK/indexes, type-mismatch truncation, dynamic-SQL injection, NOLOCK in write paths, UPDATE/DELETE without WHERE, hardcoded env values, cross-DB refs, and more (full catalog in the skill body). Reports BLOCKER/WARN/INFO findings with file:line citations and fixes. Never edits SQL. Use this skill whenever the user says "/sql-review", "review my SQL", "review the SQL diff", "lint the SQL", "check my SQL changes", "SQL pre-commit check", "audit my stored proc", or asks about SQL antipatterns in a diff — even if they don't name the skill. Distinct from code-review (general review) — this carries SQL-specific incident patterns.
Turn a feature idea, conversation context, or rough notes into researched, detailed, dependency-ordered checkbox tasks appended to the project's roadmap/backlog file — exactly the format goal-runner consumes, so a fresh unattended session can execute them. Grounds every task in the actual codebase before writing it (grep/read or Explore scouts; think-like-fable rigor if installed), puts what + done-when on the checkbox line with verified files, acceptance criteria, and a verification command in plain sub-bullets, and labels assumptions. Appends to the existing ROADMAP.md/BACKLOG.md/TODO.md; when none exists it asks once and defaults to ROADMAP.md. Use this skill whenever the user says "add this to the backlog", "add it to the roadmap", "plan these tasks", "break this down into tasks", "capture this as roadmap tasks", "turn these notes into a task list", or "/backlog-planner" — even if they don't name the skill. Not for executing tasks (goal-runner, task-executor, autopilot) — this writ
Produce a durable onboarding artifact for a codebase — writes ONBOARDING.md covering a "read this first" minimum, system overview, dependency map, startup flow, auth flow, and 5-15 important files, every claim backed by file:line citations; refreshes an existing onboarding doc instead of rewriting it. Use this skill whenever the user says "explain this codebase", "explain the codebase", "onboard me", "give me a tour", "tour this repo", "what does this repo do", "where do I start", "codebase overview", "read this first", "I haven't looked at this in months", or "/codebase-explainer" — even if they don't name the skill.