Running Sidecars
How to execute external binaries from Tauri v2, pass arguments, capture output, handle errors, and manage sidecar processes with React and Vite.
After you have added a binary to the externalBin array, the next step is to actually launch it from your application. Running a sidecar involves granting the right permissions, choosing between a one-shot call and a long‑running process, capturing output, and cleaning up when the sidecar is no longer needed.
This section covers the complete workflow: setting up capabilities, spawning the sidecar from Rust, calling it from React, passing arguments, handling errors, and managing the process lifecycle.
Complete Execution Flow (Rust‑Based)
The steps below assume you want maximum control – streaming output in real time, sending data to the sidecar’s stdin, and being able to kill the process from your application. If you only need to run a short‑lived command and collect its result, you can skip the Rust command entirely and call the sidecar directly from JavaScript (covered later).
Step 1: Grant shell permissions to the sidecar
Open src-tauri/capabilities/default.json (or the capability file for your main window). Add an entry to the permissions array that allows spawning the sidecar. The sidecar name here must match the relative path you used in tauri.conf.json’s externalBin. The Shell API is the plugin that actually executes it.
// src-tauri/capabilities/default.json
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "shell:allow-spawn",
"allow": [
{
"name": "binaries/my-sidecar",
"sidecar": true
}
]
}
]
}
Permission is mandatory:
Without this permission, any attempt to spawn the sidecar will fail with an error like "sidecar not allowed". The name must exactly match the value you listed in externalBin (without the target‑triple suffix).
If you later plan to pass arguments, you will extend this permission block – that is explained in the Passing Arguments section.
Step 2: Add the shell plugin to your Rust backend
The sidecar functionality lives in the tauri-plugin-shell crate. Add it to src-tauri/Cargo.toml and register the plugin when building the Tauri app.
// src-tauri/Cargo.toml (partial)
[dependencies]
tauri = "2"
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
In your main setup (either src-tauri/src/lib.rs or src-tauri/src/main.rs), initialise the plugin:
// src-tauri/src/lib.rs
use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
// ... other plugins, invoke handlers
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Plugin is ready:
If your project was created with npm create tauri-app, the shell plugin is already included. You can verify it under [dependencies] in Cargo.toml and in the .plugin() chain.
Step 3: Create a Tauri command that spawns the sidecar
A Tauri command gives you access to the AppHandle, which is needed to spawn the sidecar and emit events back to the frontend. This example spawns a sidecar named binaries/my-sidecar, reads its stdout line by line, and sends each line to the React UI via an event.
// src-tauri/src/lib.rs (inside the run() function or a separate commands module)
use tauri::Emitter;
use tauri_plugin_shell::ShellExt;
use tauri_plugin_shell::process::CommandEvent;
#[tauri::command]
async fn start_sidecar(app: tauri::AppHandle) -> Result<String, String> {
let sidecar_command = app
.shell()
.sidecar("binaries/my-sidecar")
.map_err(|e| e.to_string())?;
let (mut rx, child) = sidecar_command
.spawn()
.map_err(|e| e.to_string())?;
// Store the child handle so we can kill the process later.
app.manage(std::sync::Mutex::new(Some(child)));
// Background task that continuously reads sidecar output.
tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line_bytes) => {
let line = String::from_utf8_lossy(&line_bytes);
let _ = app.emit("sidecar-stdout", line.to_string());
}
CommandEvent::Stderr(line_bytes) => {
let line = String::from_utf8_lossy(&line_bytes);
let _ = app.emit("sidecar-stderr", line.to_string());
}
CommandEvent::Terminated(payload) => {
let _ = app.emit("sidecar-terminated", payload.code);
}
_ => {}
}
}
});
Ok("Sidecar spawned".into())
}
Register the command in the builder:
.invoke_handler(tauri::generate_handler![start_sidecar])
The CommandEvent::Terminated variant tells you when the sidecar exits and what exit code it returned. The CommandChild stored in managed state can later be used in a stop_sidecar command to cleanly kill the process.
Step 4: Call the command from your React frontend
In a React component, import invoke from @tauri-apps/api/core and call the command.
// src/App.tsx
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useEffect, useState } from "react";
function App() {
const [output, setOutput] = useState<string[]>([]);
useEffect(() => {
const unlisten = listen<string>("sidecar-stdout", (event) => {
setOutput((prev) => [...prev, event.payload]);
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
const handleStart = () => {
invoke("start_sidecar").catch(console.error);
};
return (
<div>
<button onClick={handleStart}>Start Sidecar</button>
<pre>{output.join("\n")}</pre>
</div>
);
}
export default App;
The listen call subscribes to the sidecar-stdout event. Every time the sidecar prints a line, it appears in the <pre> block. The cleanup function returned by listen unsubscribes when the component unmounts.
Step 5: Gracefully stop the sidecar
If the user closes your app while the sidecar is running, you risk leaving an orphan process. Add a command that kills the sidecar and call it from a React cleanup effect or a button.
#[tauri::command]
async fn stop_sidecar(app: tauri::AppHandle) -> Result<(), String> {
let state = app.state::<std::sync::Mutex<Option<tauri_plugin_shell::process::CommandChild>>>();
let mut child = state.lock().map_err(|e| e.to_string())?;
if let Some(c) = child.take() {
c.kill().map_err(|e| e.to_string())?;
}
Ok(())
}
Register it alongside start_sidecar. In React, you can call invoke("stop_sidecar") when needed.
Orphan processes:
If you never kill a spawned sidecar, it will continue running even after the Tauri window is closed. Always store the CommandChild handle and terminate it during app shutdown or when the user explicitly stops the feature.
Running a Sidecar Directly from JavaScript
If you do not need a persistent Rust‑managed stream, you can call the sidecar entirely from the frontend. This is useful for one‑off commands where you only care about the final output.
First, ensure the permission allows shell:allow-execute (or shell:allow-spawn if you prefer a spawn‑based approach). Then, use the Command class from @tauri-apps/plugin-shell.
import { Command } from "@tauri-apps/plugin-shell";
async function runSidecar() {
try {
const command = Command.sidecar("binaries/my-sidecar");
const output = await command.execute();
console.log("stdout:", output.stdout);
console.log("stderr:", output.stderr);
console.log("exit code:", output.code);
} catch (error) {
console.error("Sidecar execution failed:", error);
}
}
The execute() method waits for the process to finish and returns an object containing stdout, stderr, and code. There is no persistent child handle, so you cannot send data to stdin or kill the process mid‑execution.
Streaming output in JavaScript:
The @tauri-apps/plugin-shell package also supports spawn() which returns a Child object with stdout and stderr streams. However, for long‑running processes that must communicate bidirectionally, the Rust‑based approach (shown in the stepper) gives you more reliable control and is the recommended pattern in the official documentation.
Passing Arguments
Many sidecars need runtime parameters – file paths, port numbers, configuration flags. Tauri v2 requires you to declare all allowed arguments inside the capability file so that only permitted inputs reach the sidecar.
Declaring Allowed Arguments
Extend the shell permission in src-tauri/capabilities/default.json with an args array. Each element can be a static string (a fixed flag) or a validator object with a regular expression that matches dynamic values.
{
"identifier": "shell:allow-spawn",
"allow": [
{
"name": "binaries/my-sidecar",
"sidecar": true,
"args": [
"--output",
"-v",
{
"validator": "\\d+"
},
{
"validator": "\\S+\\.csv"
}
]
}
]
}
This definition permits exactly four arguments in order: the static flag --output, the static flag -v, a sequence of digits, and a string ending in .csv. Any deviation will cause the sidecar spawn to fail.
Sending Arguments from Rust
let sidecar_command = app.shell()
.sidecar("binaries/my-sidecar")
.map_err(|e| e.to_string())?
.args(["--output", "-v", "5", "data.csv"]);
let (mut rx, child) = sidecar_command.spawn().map_err(|e| e.to_string())?;
The args method accepts any type that implements IntoIterator<Item = impl Into<String>>, so you can pass a vector built dynamically.
Sending Arguments from JavaScript
The Command.sidecar static method accepts an optional second argument – an array of strings that must match the capability definition exactly.
const command = Command.sidecar("binaries/my-sidecar", [
"--output",
"-v",
"5",
"data.csv",
]);
const output = await command.execute();
Argument order and count must match:
The capability file enforces the arguments in the exact order they are listed. If you need to accept a variable number of a certain argument type, define a single validator that repeats, or use multiple sidecar entries with different argument patterns.
Receiving Output and Real‑Time Communication
Understanding the difference between execute and spawn is central to designing your sidecar interaction.
execute()runs the sidecar, waits for it to finish, and returns all stdout/stderr as strings. Use this for short‑lived utilities like image converters or data import scripts.spawn()returns immediately with a child process handle and a receiver channel (rx). It lets you read stdout/stderr line by line, write to stdin, and be notified when the process terminates.
The Rust example in the stepper uses spawn() and listens for CommandEvent::Stdout, CommandEvent::Stderr, and CommandEvent::Terminated. Each Stdout event carries a Vec<u8> that you can decode with String::from_utf8_lossy.
If the sidecar writes large chunks of binary data, avoid accumulating all output in memory. Stream the bytes directly to a file or process them incrementally.
Error Handling and Debugging
Sidecars can fail for many reasons, and the error messages are not always obvious at first.
Common Failure Scenarios
- Sidecar not found – The binary with the correct target‑triple suffix is missing from the directory declared in
externalBin. Double‑check that the file exists at, for example,src-tauri/binaries/my-sidecar-x86_64-unknown-linux-gnu. - Permission denied – The capability file does not list the sidecar name, or it uses
shell:allow-executewhile your code callsspawn()(or vice versa). Align the permission with the method you use. - Sidecar crashes immediately –
spawn()succeeding does not mean the process is healthy; the OS only confirms that the binary launched. Always monitorStderrandTerminatedevents. A common symptom is a singleTerminatedevent arriving before anyStdout. - Missing shared libraries – If the sidecar depends on DLLs or dylibs, they must be placed next to the binary or added as Tauri resources. A sidecar that fails to load libraries often prints an error like
error while loading shared librariesto stderr.
Capturing Stderr Separately
In Rust, route stderr to a dedicated event so the frontend can display errors distinctly:
CommandEvent::Stderr(line_bytes) => {
let line = String::from_utf8_lossy(&line_bytes);
let _ = app.emit("sidecar-stderr", line.to_string());
}
On the React side, listen for sidecar-stderr and show the messages in red or a collapsible debug panel.
Verifying the Sidecar is Actually Working
For long‑running sidecars (API servers, daemons), do not rely solely on the spawn success. Implement a health check. For example, if the sidecar exposes an HTTP endpoint, poll it after spawning:
// After spawn, poll http://localhost:PORT/health with exponential backoff.
// Set a maximum timeout; if no healthy response, emit an error state.
Silent failures:
A sidecar that starts but immediately exits will not raise a Rust panic – it will simply emit a Terminated event with a non‑zero exit code. Always listen for Terminated and handle unexpected exits.
Lifecycle Management
A spawned sidecar will continue running after the Tauri window closes, leaving an orphan process that may hold ports or files. Tauri provides a on_window_event hook to perform cleanup when the user exits the app.
// src-tauri/src/lib.rs (inside the builder)
.on_window_event(|window, event| {
if let tauri::WindowEvent::Destroyed = event {
let app = window.app_handle();
let state = app.state::<std::sync::Mutex<Option<tauri_plugin_shell::process::CommandChild>>>();
if let Ok(mut child) = state.lock() {
if let Some(c) = child.take() {
let _ = c.kill();
}
}
}
})
This ensures every spawned sidecar is terminated when the main window is destroyed. If your application has multiple windows, adjust the logic to match the appropriate lifecycle event.
Working Directory and Dependencies
By default, a sidecar spawned via shell().sidecar() sets its working directory to the folder that contains the binary. This is important when your sidecar relies on relative paths to find configuration files, DLLs, or other assets.
If the sidecar needs extra files at runtime, place them next to the binary and add them to the resources array in tauri.conf.json so they are bundled alongside:
{
"bundle": {
"externalBin": ["binaries/my-sidecar"],
"resources": {
"binaries/*.dll": "binaries/",
"binaries/config.yaml": "binaries/"
}
}
}
The resources will be copied into the same output directory as the sidecar binary, preserving the relative structure.
Verify with a test:
If you are unsure about the working directory, have the sidecar print std::env::current_dir() (in Rust) or os.getcwd() (in Python) as its first output line. You can then compare that path with your asset layout.
Summary
Running a sidecar in Tauri v2 boils down to three precise actions: declare it in externalBin, grant execution permission with an exact name match, and choose the right spawning strategy for your use case.
- For one‑shot utilities, use
Command.sidecar().execute()from JavaScript – it is the simplest path. - For long‑running processes, spawn from Rust so you can stream output, write to stdin, and forcefully kill the process when it is no longer needed.
- Always handle errors by monitoring stderr and the termination event, and never assume that a successful
spawn()means the sidecar is functioning correctly. - Clean up processes during window destruction to avoid orphan sidecars consuming system resources.