Tauri Apps skill for Codex

Small binaries, smallest possible permissions. Codex writes Tauri 2 commands that stay off the main thread and return serializable errors, then grants each window only the plugin permissions and file scopes its features need instead of widening scopes when invoke says not allowed. It sets a CSP, wires the updater with a backed-up signing key, builds every bundle on its own OS runner, and confirms before publishing an update manifest.

  • Mobile & Desktop
  • $tauri-apps
  • 192 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- tauri-apps

Installs to ~/.agents/skills/tauri-apps/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $tauri-apps, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds Tauri 2 desktop and mobile apps - Rust commands with typed errors and managed state, invoke and channels from the frontend, capabilities and permission scopes for core and plugin APIs, CSP, tauri.conf.json, plugins such as fs, dialog, shell and updater, signed updater artifacts, and bundling to dmg, msi, nsis, deb, rpm and AppImage plus Android and iOS. Use when creating or extending a Tauri app, migrating from Tauri 1, fixing permission-denied errors on invoke, or setting up bundling, updates and CI builds.

Use it when

  • Creating or extending a Tauri 2 desktop or mobile app
  • invoke or plugin calls fail with permission or scope errors
  • Migrating from Tauri 1 allowlists
  • Setting up bundling, CI builds or the updater

Use something else when

  • The app depends on Node.js APIs and a bundled Chromium: use electron-apps
  • The question is general Rust async design outside the Tauri boundary: use rust-tokio-async

How to check the result

clippy and cargo test pass, every frontend feature runs in tauri dev without not-allowed errors while out-of-scope paths are rejected, and an N to N+1 update installs from the test endpoint.

The mistake it prevents

Writing heavy commands without async, which run on the main thread and freeze the window, or answering a scope error by granting $HOME/**.

~/.agents/skills/tauri-apps/SKILL.md

--- name: tauri-apps description: Builds Tauri 2 desktop and mobile apps - Rust commands with typed errors and managed state, invoke and channels from the frontend, capabilities and… license: MIT ---

Done well means: the webview can call only the commands and plugin APIs its capability files grant, with file and URL scopes as narrow as the feature needs; long work never runs on the main thread; errors cross the IPC boundary as typed values; and every platform bundle is built on its own OS and updates from a signed feed.

Before changing anything

  1. Read AGENTS.md, then src-tauri/tauri.conf.json, src-tauri/Cargo.toml, src-tauri/capabilities/*.json and the frontend package.json.
  2. Confirm Tauri 2: tauri = "2" in Cargo.toml and @tauri-apps/api 2.x. A tauri.conf.json with an allowlist key is Tauri 1; run npm run tauri migrate and review the generated capabilities rather than hand-porting.
  3. List registered commands (generate_handler![...] in src-tauri/src/lib.rs) and plugins (.plugin(...) calls).
  4. The first cargo build downloads crates and can take minutes; it needs network. Linux builds also need system packages (libwebkit2gtk-4.1-dev and friends); ask before installing anything system-wide.

Project shape

src/                     frontend (Vite, React, Svelte, ...)
src-tauri/
  tauri.conf.json        identifier, windows, CSP, bundle, plugin config
  capabilities/          which windows may use which permissions
  src/lib.rs             builder, commands, state (shared by desktop and mobile)
  src/main.rs            desktop entry calling lib::run()

Commands

use serde::Serialize;
use tauri::State;
use tokio::sync::Mutex;

#[derive(Debug, thiserror::Error)]
enum AppError {
    #[error("not found: {0}")] NotFound(String),
    #[error(transparent)] Io(#[from] std::io::Error),
}
impl Serialize for AppError {
    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
        s.serialize_str(&self.to_string())
    }
}

struct Db(Mutex<rusqlite::Connection>);

#[tauri::command]
async fn load_note(id: String, db: State<'_, Db>) -> Result<Note, AppError> {
    let conn = db.0.lock().await;
    query_note(&conn, &id)?.ok_or(AppError::NotFound(id))
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .manage(Db(Mutex::new(open_db())))
        .invoke_handler(tauri::generate_handler![load_note])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
const note = await invoke<Note>('load_note', { id });   // args are camelCase on the JS side

Rules:

  • Commands without async run on the main thread and freeze the UI while they work. Make I/O and CPU-heavy commands async (or #[tauri::command(async)]) and push CPU work to tokio::task::spawn_blocking.
  • Async commands take owned arguments (String, not &str).
  • Error types must implement Serialize; anyhow::Error does not. Use thiserror plus a manual Serialize as above.
  • Rust parameter note_id is passed from JS as noteId unless the command uses #[tauri::command(rename_all = "snake_case")].
  • Use std::sync::Mutex for short critical sections, tokio::sync::Mutex when holding the lock across .await.
  • For progress or streams, take a tauri::ipc::Channel<T> argument and send events rather than emitting global events.

Capabilities and permissions

Every plugin API the frontend uses must be granted to the window that calls it.

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "dialog:allow-open",
    {
      "identifier": "fs:allow-read-text-file",
      "allow": [{ "path": "$APPDATA/notes/**" }]
    },
    "updater:default",
    "process:allow-restart"
  ]
}
  • Start from nothing and add permissions per feature. Avoid fs:default plus broad scopes like $HOME/**, and never grant shell:allow-execute with arbitrary programs.
  • A “not allowed” error from invoke or a plugin call means a missing permission or a path outside the scope; fix the capability, do not widen it to **.
  • Your own app commands are callable from every window by default. To restrict them, declare them in build.rs with tauri_build::AppManifest::new().commands(&[...]) and grant the generated allow-<command> permissions per window.
  • Windows that load remote URLs need a separate capability with remote.urls and the smallest permission set; better, give them none.
  • Add plugins with npm run tauri add <plugin> so the Rust crate, JS package and default permission are wired together.

Security settings

  • Set app.security.csp in tauri.conf.json (for example "default-src 'self'; img-src 'self' asset: data:; connect-src 'self' ipc: http://ipc.localhost"); Tauri injects nonces for its own scripts at build time.
  • Keep withGlobalTauri false; import from @tauri-apps/api instead.
  • Validate every command argument in Rust; the frontend is not trusted, especially if it renders user or remote content.
  • Use the asset: protocol with assetProtocol.scope for local files instead of reading bytes over IPC.

Bundling and updates

npm run tauri dev
npm run tauri build                       # bundles for the host OS only
npm run tauri build -- --bundles dmg       # one format
npm run tauri android init && npm run tauri android build
npm run tauri ios init && npm run tauri ios build    # macOS with Xcode
  • Bundles land in src-tauri/target/release/bundle/. Cross-compiling desktop bundles is unreliable; build each OS on its own CI runner (the tauri-apps/tauri-action GitHub Action covers the matrix).
  • Updater: generate a key pair once with npm run tauri signer generate -- -w ~/.tauri/app.key, put the public key in plugins.updater.pubkey, set bundle.createUpdaterArtifacts: true, and provide TAURI_SIGNING_PRIVATE_KEY (and its password) only in CI secrets. Losing the private key means existing installs can never update; back it up.
  • The update check uses check() from @tauri-apps/plugin-updater, then downloadAndInstall() and relaunch() from the process plugin.
  • Publishing the latest.json manifest reaches every user; confirm version and channel before uploading. OS code signing and notarization are in desktop-app-distribution.
  • Windows: choose bundle.windows.webviewInstallMode (download bootstrapper by default; offline installer for air-gapped customers).

Gotchas

  • The webview differs per OS: WebView2 (Chromium) on Windows, WKWebView on macOS and iOS, WebKitGTK on Linux. Test CSS and newer JS APIs on all three; Safari-level support is the floor.
  • identifier in tauri.conf.json is the bundle ID and app data folder name; changing it after release orphans user data and breaks updates.
  • Mobile shares lib.rs but not every plugin supports Android and iOS; check the plugin’s platform table before relying on it.
  • Large binary payloads over invoke are serialized; return file paths or use channels with raw bytes (tauri::ipc::Response) for big data.

Verification

  1. cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings and cargo test --manifest-path src-tauri/Cargo.toml pass.
  2. Every frontend call path is exercised in tauri dev with no “not allowed” errors, and a call outside the scope (a path outside $APPDATA/notes) is rejected.
  3. Release bundles build on each target OS in CI.
  4. Version N updates to N+1 from the test endpoint and relaunches.

Output

**Commands:** <name -> args, async, error type>
**Capabilities:** <window -> permissions and scopes>
**Bundles:** <targets per OS, CI runners>
**Updater:** <endpoint, key storage, tested N -> N+1>

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Tauri Apps"
  short_description: "Small binaries, smallest possible permissions"
  default_prompt: "$tauri-apps Add a Tauri command that reads notes from app data and fix the not allowed error"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem