Limiting Tauri Windows with Capability Scopes: A Practical Engineering Decision
Learn how to restrict which native commands each Tauri window can call by using capability scopes, reducing attack surface while keeping the app functional.
27 Sept 2026, 13:38 UTC

The problem: over‑privileged windows in a Tauri app
When you build a desktop app with Tauri, the frontend runs in the OS‑provided WebView while the Rust backend holds all native privileges. By default, any window can invoke every command you expose from Rust. If a compromised or buggy frontend gains the ability to call a privileged command (for example, fs::writeFile or shell::open), it can act with the full rights of the user. This widens the attack surface unnecessarily, especially in apps that load third‑party content or plugins.
Thesis: capability scopes let you apply least‑privilege at the window level
Tauri’s capability system lets you declare which commands and resources each window may use. By binding a capability file to a specific window, you enforce a explicit allow‑list. The frontend remains untrusted; only the commands listed in its capability can cross the IPC bridge. This decision reduces the impact of a frontend vulnerability without requiring a redesign of the Rust backend.
How capability scopes work
In a Tauri project you create JSON capability files under src-capabilities/. Each file lists:
identifier– a unique string used to reference the capability.description– human‑readable purpose.allow– arrays ofplugin,event, orcommandentries that are permitted.
When you define a window in tauri.conf.json you add a capabilities array that contains the identifiers of the files you want that window to inherit. The Rust runtime checks each IPC call against the window’s capability set and rejects any call that is not explicitly allowed.
Worked example: restricting a preview window
Suppose you have a markdown editor with two windows: a main editor that needs to save files, and a preview pane that only renders HTML and should never touch the filesystem.
1. Create a capability for the preview window (src-capabilities/preview.json):
{
"identifier": "preview-capability",
"description": "Allow only safe rendering commands for the preview pane",
"allow": [
{
"plugin": "tauri-plugin-html",
"event": ["html:rendered"]
}
]
}
2. Expose a safe command in Rust that the preview can use (e.g., a command that returns sanitized HTML):
#[tauri::command]
fn render_markdown(markdown: String) -> Result {
// simple sanitization placeholder
let html = markdown.replace("<", "<").replace(">", ">");
Ok(html)
}
3. Register the command in src-tauri/src/main.rs (omitting boilerplate):
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![render_markdown])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
4. Configure the windows in tauri.conf.json:
{
"windows": [
{
"label": "main",
"url": "/index.html",
"capabilities": ["default"]
},
{
"label": "preview",
"url": "/preview.html",
"capabilities": ["preview-capability"]
}
]
}
Now the preview window can only call render_markdown (if you add it to its allow list) and any events from the HTML plugin. Attempts to call a filesystem command such as fs::readFile will be blocked by the runtime, producing an error like:
[tauri] IPC call denied: command 'fs::readFile' is not allowed for window 'preview'
Trade‑off and limitation
The main trade‑off is added configuration overhead: every window that needs a distinct set of privileges requires its own capability file and explicit listing in tauri.conf.json. For small apps with a single window this can feel like boilerplate. Additionally, capability checking happens at runtime, so a mis‑configured window will fail only when the offending IPC call is made; static analysis tools cannot yet guarantee that all possible calls are covered.
Practical verification: after building the app, launch it and open the developer tools for the preview window. In the console, try to invoke a disallowed command via window.__TAURI__.invoke('fs::readFile', { path: 'test.txt' }). You should see the denial message above. Conversely, call an allowed command (e.g., render_markdown) and confirm it returns the expected result.
Actionable closing
If your Tauri app uses multiple windows or loads untrusted content, start by drawing a privilege matrix: list each window and the native operations it truly needs. Create a capability file for each row, bind them in the configuration, and test the denial of any operation outside the matrix. This disciplined approach keeps the attack surface small while preserving the flexibility that makes Tauri attractive for desktop development.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.