Introduction to the Shell API
Understand what the Tauri Shell API is, why it exists, and how to set it up securely in a React Vite project with Tauri v2.
The Shell API lets your Tauri application interact with the operating system’s command-line environment and open external resources using the default system applications. It is the bridge between your frontend JavaScript and native processes — spawning child processes, launching URLs in the browser, opening files with their associated programs, and running sidecar binaries.
What the Shell API Provides
The API is split into two main capabilities:
- Opening a path or URL with the system’s default handler. For example, opening a web link in the browser, launching an email client with a
mailto:link, or opening a file with whatever program the user has associated with that file type. - Spawning child processes — running system commands, shell scripts, or sidecar binaries. This includes streaming stdout/stderr back to the frontend, writing to stdin, and controlling the child process lifecycle. These operations run outside the webview sandbox, so Tauri wraps them in a strict permission and scope system. Without that, a compromised frontend could execute arbitrary commands on the user’s machine.
Shell access is powerful — secure it early:
Every command your app is allowed to run, and every URL pattern you allow to be opened, must be explicitly declared in a capability file. Never grant blanket "execute all" permission unless your app genuinely requires it and you fully understand the risk.
Why the Shell API Exists
A Tauri app’s frontend runs inside a sandboxed webview, with no direct access to the filesystem or operating system. The Shell API provides a controlled escape hatch for those times when you need to:
- Run a CLI tool that processes data (e.g.,
ffmpegfor media conversion,gitfor version control operations, oropensslfor certificate handling). - Launch a third-party application installed on the system.
- Open an external link in the default browser without relying on an anchor tag’s
target="_blank". - Execute a sidecar — a binary you bundle with your app — to perform heavy computation in a native process. In each case, the API ensures that the frontend can only invoke the exact commands and argument patterns you have approved, and that any sensitive data passed to those processes is controlled.
How It Works
The Shell API is a Tauri plugin. Plugins in Tauri v2 are Rust crates that expose functionality through an IPC bridge to the JavaScript side.
- Rust layer (
tauri-plugin-shell) — responsible for validating permissions, executing system calls, managing child process handles, and returning results. - JavaScript layer (
@tauri-apps/plugin-shell) — provides ergonomic functions likeopen()andCommand.create()that invoke Tauri commands under the hood. When your frontend callsopen('https://example.com'), the following sequence unfolds:
- The JavaScript function invokes a Tauri IPC command.
- The Rust side checks the capability file: does the app have permission to open this URL pattern?
- If permitted, the plugin delegates to the OS’s default handler (
openon macOS,xdg-openon Linux,ShellExecuteWon Windows) or uses the specified application. - The result (success or error) is sent back to the frontend.
A similar flow occurs for
Command.create()— the Rust backend validates the command name and arguments against the scope defined in the capability before spawning the process.
Required Plugin Installation
The Shell API is not bundled into every Tauri app by default — you must add it explicitly.
Step 1: Add the Rust crate
Open src-tauri/Cargo.toml and add the plugin as a dependency:
[dependencies]
tauri-plugin-shell = "2"
The precise version can be found on crates.io.
Step 2: Register the plugin
In src-tauri/src/lib.rs (or main.rs if your project still uses that), call .plugin() during the builder setup:
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Without this step, the plugin will not be loaded and all shell-related functions will fail.
Step 3: Install the JavaScript package
In your React project root, install the guest bindings:
npm add @tauri-apps/plugin-shell
Version alignment:
The npm package version should be compatible with the Rust crate version. Both follow 2.x releases. The latest versions are recommended.
After these steps, the plugin is available to your frontend code, but no operations are allowed until you configure permissions. Attempting to call open() or Command.create() without the proper capability will result in a denied permission error.
Required Permissions
Tauri v2 moved away from the allowlist system of v1. Permissions are now declared in capability files — JSON documents inside src-tauri/capabilities/. Each capability grants specific permissions and can be restricted to particular windows or platforms.
Creating a Shell Capability
Create a new file src-tauri/capabilities/shell.json (or merge into the existing default.json):
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "shell-capability",
"description": "Grants access to the Shell API",
"windows": ["main"],
"permissions": [
"shell:allow-open",
"shell:allow-execute",
"shell:allow-spawn",
"shell:allow-stdin-write"
]
}
This grants the app permission to use the open, execute, spawn, and stdin-write operations. However, without a scope, it does not define which commands or URL patterns are allowed. For the open API, a default URL pattern is applied unless you override it. For command execution, you must explicitly define a scope array.
Permissions alone do not enable arbitrary commands:
Simply having shell:allow-execute in your permissions list does not mean you can run rm -rf /. The plugin will still check the command name and arguments against the scope. If no scope is defined, execution requests are denied.
Defining the Open Scope
The open API validates the path or URL against a regular expression. The default pattern is ^((mailto:\w+)\|(tel:\w+)\|(https?://\w+)).+, which allows mailto:, tel:, and standard HTTP/HTTPS URLs. If you need to open file paths, you must override this with a custom pattern.
To allow opening any URL and certain file schemes, you can define a custom scope inside your capability file by adding a scope property:
{
"identifier": "shell-capability",
"description": "Grants access to the Shell API with custom open scope",
"windows": ["main"],
"permissions": [
{
"identifier": "shell:allow-open",
"allow": [
{ "url": "^https://" }
]
},
"shell:allow-execute",
"shell:allow-spawn",
"shell:allow-stdin-write"
]
}
Now open() will only accept URLs starting with https://. This is the recommended practice: never allow unbounded .* patterns unless absolutely necessary.
Defining the Command Scope
To allow your app to execute specific system commands, add a scope array within the permissions block for shell:allow-execute (or as a top-level scope in the capability — check plugin documentation for exact syntax in v2). The recommended approach in Tauri v2 is to use the "scope" field within the permission object.
Here is an example that allows calling git commit -m "message" with a validated message:
{
"identifier": "shell-capability",
"description": "Grants access to the Shell API with command scope",
"windows": ["main"],
"permissions": [
"shell:allow-open",
{
"identifier": "shell:allow-execute",
"scope": [
{
"name": "run-git-commit",
"cmd": "git",
"args": [
"commit",
"-m",
{ "validator": "\\S+" }
]
}
]
},
"shell:allow-spawn",
"shell:allow-stdin-write"
]
}
name: a unique identifier you will use in your frontend code when callingCommand.create('run-git-commit', ...). This decouples the frontend from the actual binary path, which is a security feature.cmd: the actual program or binary to execute.args: eithertrueto allow any arguments,falseto allow none, or an array where each item is either a fixed string ("commit") or a validator object that specifies a regex the argument must match.
Command naming is a security layer:
By using a descriptive name like run-git-commit instead of directly passing "git" to the frontend, you prevent the user interface from specifying arbitrary binaries even if some other security measure fails. The frontend code never sees the real executable path — that mapping lives safely in the capability file on the backend.
Opening a URL — A Minimal Example
With the plugin installed and a capability that allows shell:allow-open with the default URL pattern, you can open a link from a React component.
import { useState } from "react";
import { open } from "@tauri-apps/plugin-shell";
function App() {
const [status, setStatus] = useState<string>("");
const handleOpenUrl = async () => {
try {
await open("https://github.com");
setStatus("Opened successfully");
} catch (error) {
setStatus(`Failed to open: ${error}`);
}
};
return (
<div>
<button onClick={handleOpenUrl}>
Open GitHub
</button>
<p>{status}</p>
</div>
);
}
export default App;
When the button is clicked, the default browser will launch (or switch to an existing tab) with GitHub. The open function returns a promise that resolves on success or rejects if the URL is not permitted by the scope or if the system handler fails.
The error case is important: if the capability file does not include shell:allow-open or the URL pattern does not match the scope, the call will throw a permission error. This is the mechanism that prevents a malicious script from opening arbitrary dangerous URIs.
Running a Scoped Command — Quick Look
Although the deep dive into command execution belongs to later sections, here is a taste of how a scoped command works. Given the run-git-commit scope defined earlier, the frontend can run:
import { Command } from "@tauri-apps/plugin-shell";
const output = await Command.create("run-git-commit", [
"commit",
"-m",
"Initial commit"
]).execute();
console.log(output.stdout);
Command.create takes the scope name as the first argument. The Rust backend looks up that name, verifies the arguments match the validator, and then executes git commit -m "Initial commit". Any deviation from the allowed argument structure is blocked.
Common Mistakes
Forgetting to register the plugin:
A frequent error is adding the npm package and writing frontend code while skipping the .plugin(tauri_plugin_shell::init()) call in lib.rs. Without that registration, the shell plugin is never loaded, and all API calls will fail with an opaque error about missing commands. Always check that the plugin is registered before debugging permission issues.
Using v1 API patterns in v2:
Tauri v2 changed the JavaScript import path from @tauri-apps/api/shell to @tauri-apps/plugin-shell, and the constructor from new Command(...) to Command.create(...). If you copy-paste code from older tutorials, these differences will cause runtime failures. Always verify the imports and method signatures against the latest official plugin documentation.
Summary
The Shell API is Tauri’s controlled interface to the operating system’s native process and file-handling capabilities. By requiring explicit permission declarations and scope definitions, it allows your app to run external commands and open resources safely. The installation involves adding a Rust crate, registering it, installing the JavaScript bindings, and configuring a capability file that spells out exactly what your app is allowed to do. When you move to For now, ensure your capability is correctly scoped and that the plugin is initialized; this prevents a whole category of problems before they occur.