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.

  1. Obtain a self-contained binary for your target platform.
  2. 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.
  3. Place the binary inside src-tauri/binaries/ (or a subfolder you prefer).
  4. Add the relative path to bundle.externalBin in tauri.conf.json.
  5. Register a shell permission for the binary in a capability file so the frontend can execute it.
  6. Invoke the sidecar from Rust with app.shell().sidecar(...) or from JavaScript with Command.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

1

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
2

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
3

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:

src-tauri/tauri.conf.json
{
  "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:

src-tauri/capabilities/default.json
{
  "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.

src-tauri/src/main.rs
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:

src/App.tsx
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

1

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.

process_csv.py
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)
2

Install PyInstaller

pip install pyinstaller
3

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.

4

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

src-tauri/tauri.conf.json
{
  "bundle": {
    "externalBin": ["binaries/process_csv"],
    "resources": ["binaries/references/*"]
  }
}

The resources entry ensures any data files land next to the binary at runtime.

Granting Permission

src-tauri/capabilities/default.json
{
  "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

src-tauri/src/main.rs
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(())
}
src/App.tsx
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

1

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.

2

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)
3

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

src-tauri/tauri.conf.json
{
  "bundle": {
    "externalBin": ["binaries/git-portable/git"],
    "resources": ["binaries/git-portable/*"]
  }
}

Granting Permission

src-tauri/capabilities/default.json
{
  "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

src-tauri/src/main.rs
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())
    }
}
src/App.tsx
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

1

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
2

Rename with the target triple

mv magick magick-$(rustc --print host-tuple)
3

Place in binaries

mv magick-* src-tauri/binaries/

Configuration

src-tauri/tauri.conf.json
{
  "bundle": {
    "externalBin": ["binaries/magick"]
  }
}

Granting Permission

src-tauri/capabilities/default.json
{
  "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

src-tauri/src/main.rs
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(())
}
src/App.tsx
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

1

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
2

Name it with the target triple

mv my-tool my-tool-$(rustc --print host-tuple)
3

Place it in binaries

cp my-tool-* src-tauri/binaries/

Configuration

src-tauri/tauri.conf.json
{
  "bundle": {
    "externalBin": ["binaries/my-tool"]
  }
}

Granting Permission

src-tauri/capabilities/default.json
{
  "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

src-tauri/src/main.rs
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())
    }
}
src/App.tsx
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().