Practical Examples
Step-by-step examples for bundling FFmpeg, Python scripts, Git, ImageMagick, and custom CLI tools as Tauri sidecars.
The sidecar feature shines when your Tauri app needs a well-known external tool without asking the user to install it manually. This section walks through five real-world sidecar integrations, each demonstrating the full lifecycle: obtaining the binary, configuring externalBin, granting capabilities, and calling the sidecar from both Rust and JavaScript.
General Workflow
Every sidecar follows the same rough shape. Understanding this pattern before diving into the specific tools makes the examples easier to absorb.
- Obtain a self-contained binary for your target platform.
- Append the Rust target triple to its filename (e.g.,
my-tool-x86_64-unknown-linux-gnu). Tauri’s bundler uses this to pick the correct binary for the user’s OS and architecture. - Place the binary inside
src-tauri/binaries/(or a subfolder you prefer). - Add the relative path to
bundle.externalBinintauri.conf.json. - Register a shell permission for the binary in a capability file so the frontend can execute it.
- Invoke the sidecar from Rust with
app.shell().sidecar(...)or from JavaScript withCommand.sidecar(...).
Target Triple Naming:
Without the triple suffix, Tauri will not find your binary during bundling. Run rustc --print host-tuple to see your current platform’s triple, but remember that for distribution you’ll need one binary per supported platform.
Example 1: Bundling FFmpeg
FFmpeg is the go-to tool for converting, trimming, and inspecting media files. Bundling it as a sidecar lets your app process video or audio without any system dependency.
Preparing the Binary
Download a static build
Get a portable FFmpeg binary from the official site or a trusted build like BtbN/FFmpeg-Builds. Choose the smallest package that suits your needs — usually the ffmpeg executable alone is enough.
# Example: downloading a Linux static build
wget https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
tar -xf ffmpeg-release-amd64-static.tar.xz
Rename with the target triple
The binary name must include the Rust target triple. Use rustc --print host-tuple to get it.
mv ffmpeg ffmpeg-$(rustc --print host-tuple)
# Example result: ffmpeg-x86_64-unknown-linux-gnu
Place it in the binaries folder
Move the renamed binary into src-tauri/binaries/. Create the folder if it doesn’t exist.
mkdir -p src-tauri/binaries
mv ffmpeg-x86_64-unknown-linux-gnu src-tauri/binaries/
Configuration
Add the path to externalBin:
{
"bundle": {
"externalBin": ["binaries/ffmpeg"]
}
}
Notice that the path does not include the target triple — Tauri appends it automatically based on the platform.
Granting Permission
Create or update a capability file to allow executing the sidecar with arguments:
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "binaries/ffmpeg",
"sidecar": true,
"args": [
{
"validator": "\\S+"
}
]
}
]
}
]
}
The args field with a regex validator lets you pass any non‑whitespace argument. In a production app you’d tighten this to accepted flags and file paths.
Invocation
The Rust side spawns the sidecar and reads its standard output line by line. This is useful when you need to process the output on the backend before sending results to the frontend.
use tauri::Emitter;
use tauri_plugin_shell::process::CommandEvent;
use tauri_plugin_shell::ShellExt;
#[tauri::command]
async fn convert_video(app: tauri::AppHandle, input: String, output: String) -> Result<(), String> {
let sidecar_command = app.shell()
.sidecar("binaries/ffmpeg")
.map_err(|e| format!("Failed to create sidecar command: {}", e))?
.args(["-i", &input, "-c:v", "libx264", &output]);
let (mut rx, _child) = sidecar_command
.spawn()
.map_err(|e| format!("Failed to spawn FFmpeg: {}", e))?;
tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await {
if let CommandEvent::Stderr(line_bytes) = event {
let line = String::from_utf8_lossy(&line_bytes);
// FFmpeg writes progress to stderr
app.emit("ffmpeg-progress", line.to_string())
.expect("failed to emit progress");
}
}
});
Ok(())
}
FFmpeg writes to stderr:
FFmpeg’s progress updates go to stderr, not stdout. If you only listen to CommandEvent::Stdout, you’ll miss everything. This trips up nearly everyone on their first FFmpeg sidecar.
From the frontend, calling the sidecar directly is a one‑liner:
import { Command } from "@tauri-apps/plugin-shell";
async function runFFmpeg(inputPath: string, outputPath: string) {
const command = Command.sidecar("binaries/ffmpeg", [
"-i",
inputPath,
"-c:v",
"libx264",
outputPath,
]);
const output = await command.execute();
console.log("FFmpeg finished with code", output.code);
}
If your output.code is 0:
The sidecar ran successfully. Any non‑zero code means an error, and output.stderr will contain the diagnostic information from FFmpeg.
Example 2: Bundling a Python Script
Python is a common choice for data processing, automation, or leveraging libraries like Pandas. Since end users shouldn’t have to install Python, you compile your script into a single executable with PyInstaller and bundle it as a sidecar.
Preparing the Binary
Write your Python script
Keep it self-contained. In this example, process_csv.py reads an input file, performs some transformation, and writes the result.
import sys
import pandas as pd
input_path = sys.argv[1]
output_path = sys.argv[2]
df = pd.read_csv(input_path)
df["processed"] = df["value"] * 2
df.to_csv(output_path, index=False)
Install PyInstaller
pip install pyinstaller
Compile with --onefile and the target triple
Use the --onefile flag to produce a single executable that bundles the Python interpreter and all dependencies. Name the binary with the target triple so Tauri can find it.
pyinstaller --onefile --name "process_csv-$(rustc --print host-tuple)" process_csv.py
This creates a single binary inside the dist/ directory.
Place the binary in the project
mkdir -p src-tauri/binaries
cp dist/process_csv-* src-tauri/binaries/
Always use --onefile with PyInstaller:
Without --onefile, PyInstaller creates a folder full of shared libraries. Tauri’s sidecar mechanism expects a single executable. Attempting to run a multi‑file build will result in errors like Failed to load Python shared library.
If your script needs extra data files (CSVs, YAML configs, etc.), use the --add-data flag and place those files in the Tauri resources bundle. The sidecar’s working directory will be next to the executable, so relative paths usually work.
Configuration
{
"bundle": {
"externalBin": ["binaries/process_csv"],
"resources": ["binaries/references/*"]
}
}
The resources entry ensures any data files land next to the binary at runtime.
Granting Permission
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "binaries/process_csv",
"sidecar": true,
"args": [
{ "validator": "\\S+" },
{ "validator": "\\S+" }
]
}
]
}
]
}
Two positional arguments are expected: input and output file paths.
Invocation
use tauri_plugin_shell::ShellExt;
#[tauri::command]
async fn process_csv(app: tauri::AppHandle, input: String, output: String) -> Result<(), String> {
let sidecar = app.shell()
.sidecar("binaries/process_csv")
.map_err(|e| e.to_string())?
.args([input, output]);
let (mut rx, _child) = sidecar.spawn().map_err(|e| e.to_string())?;
tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(line) => {
println!("Python stdout: {}", String::from_utf8_lossy(&line));
}
tauri_plugin_shell::process::CommandEvent::Stderr(line) => {
eprintln!("Python stderr: {}", String::from_utf8_lossy(&line));
}
_ => {}
}
}
});
Ok(())
}
import { Command } from "@tauri-apps/plugin-shell";
async function runPythonProcessing(inputPath: string, outputPath: string) {
const command = Command.sidecar("binaries/process_csv", [inputPath, outputPath]);
const output = await command.execute();
if (output.code !== 0) {
console.error("Python sidecar failed:", output.stderr);
}
}
Working directory and resources:
When the sidecar launches, its working directory is the folder containing the binary. If you bundled additional files via resources, they will be next to the executable, so your Python script can open them with relative paths like "./references/config.yaml".
Example 3: Bundling Git
A portable Git binary gives your app version‑control capabilities without requiring the user to install Git. Use cases include showing commit history, performing git status, or bundling a lightweight Git server.
Preparing the Binary
Obtain a portable Git distribution
For Windows, use Git for Windows Portable. For macOS and Linux, static builds are available from git‑portable or you can compile a statically‑linked Git yourself.
A minimal portable Git includes the git executable and any required shared libraries, all placed next to the binary.
Rename the main executable
Rename the git binary to include the target triple. If you have a folder with dependencies, Tauri still expects a single named binary, so either use --onefile‑style bundling (like git-static) or place the entire folder in binaries/ and point externalBin to the binary inside it.
mv git git-$(rustc --print host-tuple)
Bundle dependencies (if any)
If the Git build is not fully static, copy the required .dylib or .dll files into the same folder and add them to resources so they land next to the binary at runtime.
mkdir -p src-tauri/binaries/git-portable
mv git-* git-portable/
cp /path/to/lib*.dylib git-portable/ # macOS example
Configuration
{
"bundle": {
"externalBin": ["binaries/git-portable/git"],
"resources": ["binaries/git-portable/*"]
}
}
Granting Permission
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "binaries/git-portable/git",
"sidecar": true,
"args": [
{ "validator": "\\S+" },
{ "validator": ".*" }
]
}
]
}
]
}
The regex .* allows arbitrarily long arguments, which is often needed for commit messages or paths.
Invocation
use tauri_plugin_shell::ShellExt;
use tauri_plugin_shell::process::CommandEvent;
#[tauri::command]
async fn git_status(app: tauri::AppHandle, repo_path: String) -> Result<String, String> {
let output = app.shell()
.sidecar("binaries/git-portable/git")
.map_err(|e| e.to_string())?
.args(["-C", &repo_path, "status", "--short"])
.execute()
.await
.map_err(|e| e.to_string())?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
Err(String::from_utf8_lossy(&output.stderr).to_string())
}
}
import { Command } from "@tauri-apps/plugin-shell";
async function getGitLog(repoPath: string) {
const command = Command.sidecar("binaries/git-portable/git", [
"-C",
repoPath,
"log",
"--oneline",
"-5",
]);
const output = await command.execute();
console.log(output.stdout);
}
Portable Git dependencies:
A non‑static Git build will fail with “library not loaded” if the required shared libraries aren’t next to the binary. Always test on a machine that doesn’t have Git installed to confirm the sidecar is self‑contained.
Example 4: Bundling ImageMagick
ImageMagick provides a vast set of image manipulation commands (convert, resize, composite) accessible via the magick or convert CLI. It’s a classic candidate for a sidecar because the CLI is stable and the functionality is hard to replicate from scratch.
Preparing the Binary
Download a static build
ImageMagick’s site offers portable binaries. For all platforms, you can also build a static version from source. A static build contains everything in one file, avoiding shared‑library headaches.
# Example: Linux AppImage or static binary from official site
wget https://download.imagemagick.org/ImageMagick/download/binaries/magick
chmod +x magick
Rename with the target triple
mv magick magick-$(rustc --print host-tuple)
Place in binaries
mv magick-* src-tauri/binaries/
Configuration
{
"bundle": {
"externalBin": ["binaries/magick"]
}
}
Granting Permission
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "binaries/magick",
"sidecar": true,
"args": [
{ "validator": "\\S+" },
{ "validator": "\\S+" },
{ "validator": "\\S+" },
{ "validator": "\\S+" }
]
}
]
}
]
}
ImageMagick commands often need several arguments; adjust the number of argument slots to the maximum your app requires.
Invocation
use tauri_plugin_shell::ShellExt;
#[tauri::command]
async fn resize_image(app: tauri::AppHandle, input: String, output: String, width: u32, height: u32) -> Result<(), String> {
let size = format!("{}x{}", width, height);
let sidecar = app.shell()
.sidecar("binaries/magick")
.map_err(|e| e.to_string())?
.args([&input, "-resize", &size, &output]);
let output = sidecar.execute().await.map_err(|e| e.to_string())?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).to_string());
}
Ok(())
}
import { Command } from "@tauri-apps/plugin-shell";
async function convertToWebP(inputPath: string, outputPath: string) {
const command = Command.sidecar("binaries/magick", [
inputPath,
"-quality",
"80",
outputPath,
]);
const output = await command.execute();
if (output.code !== 0) {
throw new Error(output.stderr);
}
}
Validating the output:
After calling the sidecar, check output.code === 0 to confirm success. If the command fails, stderr will carry ImageMagick’s detailed error message (e.g., missing delegate library).
Example 5: Custom CLI Tool (Rust or Go)
Sometimes the tool you need doesn’t exist yet, or you want to ship a piece of your business logic as a separate, independently testable binary. Writing a small command‑line utility in Rust, Go, or any compiled language and bundling it as a sidecar is a clean pattern.
Preparing the Binary
Build a standalone CLI
Write your tool in Rust (or Go, C, Zig). Ensure you produce a static binary — x86_64-unknown-linux-musl target for Linux, or CGO_ENABLED=0 for Go.
# Rust
cargo build --release --target x86_64-unknown-linux-musl
# Go
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o my-tool
Name it with the target triple
mv my-tool my-tool-$(rustc --print host-tuple)
Place it in binaries
cp my-tool-* src-tauri/binaries/
Configuration
{
"bundle": {
"externalBin": ["binaries/my-tool"]
}
}
Granting Permission
{
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "binaries/my-tool",
"sidecar": true,
"args": true
}
]
}
]
}
Setting "args": true allows any argument to be passed. Use this only when you fully trust the caller, or tighten it with a regex.
Invocation
use tauri_plugin_shell::ShellExt;
#[tauri::command]
async fn run_custom_tool(app: tauri::AppHandle, input: String) -> Result<String, String> {
let output = app.shell()
.sidecar("binaries/my-tool")
.map_err(|e| e.to_string())?
.args(["--input", &input])
.execute()
.await
.map_err(|e| e.to_string())?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
Err(String::from_utf8_lossy(&output.stderr).to_string())
}
}
import { Command } from "@tauri-apps/plugin-shell";
async function callTool(payload: string) {
const command = Command.sidecar("binaries/my-tool", ["--payload", payload]);
const output = await command.execute();
console.log("Tool output:", output.stdout);
}
Static linking is mandatory:
A dynamically‑linked custom binary will break on systems that lack the required libraries. On Linux, target x86_64-unknown-linux-musl to statically link musl. On macOS, set MACOSX_DEPLOYMENT_TARGET and consider -target x86_64-apple-darwin with static‑libc++ if you need maximum compatibility.
What You Should Take Away
The five examples cover the spectrum of sidecar use: off‑the‑shelf tools (FFmpeg, Git, ImageMagick), interpreted language runtimes (Python via PyInstaller), and custom utilities you control. The pattern is the same in every case: a single executable named with a target triple, a line in externalBin, a shell permission that names the binary exactly, and a spawn or execute call.
The most frequent mistakes are forgetting the target triple suffix, not using --onefile with PyInstaller, and pointing externalBin at a folder instead of a binary. If something doesn’t work, check that the binary exists at src-tauri/binaries/<name>-<triple>, that it’s executable, and that the capability permission matches the path used in Command.sidecar().