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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- tauri-apps Installs to ~/.agents/skills/tauri-apps/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install tauri-apps The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project tauri-apps Installs to .agents/skills/tauri-apps/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/tauri-apps.zipunzip -o tauri-apps.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) tauri-apps PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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/**.
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
- Read
AGENTS.md, thensrc-tauri/tauri.conf.json,src-tauri/Cargo.toml,src-tauri/capabilities/*.jsonand the frontendpackage.json. - Confirm Tauri 2:
tauri = "2"in Cargo.toml and@tauri-apps/api2.x. Atauri.conf.jsonwith anallowlistkey is Tauri 1; runnpm run tauri migrateand review the generated capabilities rather than hand-porting. - List registered commands (
generate_handler![...]insrc-tauri/src/lib.rs) and plugins (.plugin(...)calls). - The first
cargo builddownloads crates and can take minutes; it needs network. Linux builds also need system packages (libwebkit2gtk-4.1-devand 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
asyncrun on the main thread and freeze the UI while they work. Make I/O and CPU-heavy commandsasync(or#[tauri::command(async)]) and push CPU work totokio::task::spawn_blocking. - Async commands take owned arguments (
String, not&str). - Error types must implement
Serialize;anyhow::Errordoes not. Usethiserrorplus a manualSerializeas above. - Rust parameter
note_idis passed from JS asnoteIdunless the command uses#[tauri::command(rename_all = "snake_case")]. - Use
std::sync::Mutexfor short critical sections,tokio::sync::Mutexwhen holding the lock across.await. - For progress or streams, take a
tauri::ipc::Channel<T>argument andsendevents 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:defaultplus broad scopes like$HOME/**, and never grantshell:allow-executewith arbitrary programs. - A “not allowed” error from
invokeor 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.rswithtauri_build::AppManifest::new().commands(&[...])and grant the generatedallow-<command>permissions per window. - Windows that load remote URLs need a separate capability with
remote.urlsand 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.cspintauri.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
withGlobalTaurifalse; import from@tauri-apps/apiinstead. - Validate every command argument in Rust; the frontend is not trusted, especially if it renders user or remote content.
- Use the
asset:protocol withassetProtocol.scopefor 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 (thetauri-apps/tauri-actionGitHub Action covers the matrix). - Updater: generate a key pair once with
npm run tauri signer generate -- -w ~/.tauri/app.key, put the public key inplugins.updater.pubkey, setbundle.createUpdaterArtifacts: true, and provideTAURI_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, thendownloadAndInstall()andrelaunch()from the process plugin. - Publishing the
latest.jsonmanifest 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.
identifierin 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.rsbut not every plugin supports Android and iOS; check the plugin’s platform table before relying on it. - Large binary payloads over
invokeare serialized; return file paths or use channels with raw bytes (tauri::ipc::Response) for big data.
Verification
cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warningsandcargo test --manifest-path src-tauri/Cargo.tomlpass.- Every frontend call path is exercised in
tauri devwith no “not allowed” errors, and a call outside the scope (a path outside$APPDATA/notes) is rejected. - Release bundles build on each target OS in CI.
- 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.yaml
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.