Agent Wranglers

DSK-34Recommended

Give each Tauri window only the permissions it uses

In a Tauri app, list every capability in src-tauri/capabilities and in app.security.capabilities in tauri.conf.json. Each capability names its windows by label and grants named permissions, with file and URL scopes limited to the app's own folders and hosts.

How to do it

Tauri checks every call from a page against the capabilities that cover its window. A broad capability turns a cross-site scripting hole into file and shell access. That is what keep web content away from the operating system stops. When tauri.conf.json lists capability identifiers, only those are built in, so a stray file adds nothing. By default only code packaged with the app reaches the API. A remote.urls entry opens it to a website, so leave it out unless that site is yours and has its own content security policy. Give a settings window and a document viewer separate capabilities. Scope file permissions to paths such as $APPDATA/, and deny folders such as $HOME/.ssh/. Your own commands still check their arguments, as check the sender and content of every message to the main process says.

Examples

JSON example

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    { "identifier": "fs:allow-read-text-file", "allow": [{ "path": "$APPDATA/invoices/**" }] }
  ]
}

JavaScript example

// capabilities.test.js: fail the build on a broad or unscoped capability
const BROAD_SCOPES = ['$HOME/**', '/**'];

for (const file of readdirSync('src-tauri/capabilities')) {
  const capability = JSON.parse(readFileSync(`src-tauri/capabilities/${file}`, 'utf8'));
  assert.ok(capability.windows?.length, `${file} names no window`);
  const paths = capability.permissions.flatMap(permission => permission.allow ?? []).map(scope => scope.path);
  assert.ok(!paths.some(path => BROAD_SCOPES.includes(path)), `${file} grants a broad file scope`);
}

Plain text example

// build.rs: refuse to build with a capability that names no window or grants a broad file scope
const BROAD_SCOPES: [&str; 2] = ["$HOME/**", "/**"];

fn main() {
    for entry in std::fs::read_dir("capabilities").expect("capabilities folder") {
        let text = std::fs::read_to_string(entry.unwrap().path()).unwrap();
        let capability: serde_json::Value = serde_json::from_str(&text).unwrap();
        assert!(capability["windows"].as_array().is_some_and(|windows| !windows.is_empty()), "capability names no window");
        assert!(!BROAD_SCOPES.iter().any(|scope| text.contains(&format!("\"{scope}\""))), "broad file scope");
    }
    tauri_build::build();
}

How to check

A CI test reads every capability file. It fails on a capability without a windows list and on a remote.urls host outside an approved list. It also fails on an unlisted permission and on a file scope of $HOME/ or /.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps/tauri-capabilities">Agent Wranglers desktop apps and extensions playbook</a>. In code you write, add a comment next to it with the same link.