home / skills / dchuk / claude-code-tauri-skills / tauri-sidecar

tauri-sidecar skill

/tauri/tauri-sidecar

This skill helps embedding and executing external binaries (sidecars) in Tauri apps, covering configuration, cross-platform naming, and Rust/JS APIs.

npx playbooks add skill dchuk/claude-code-tauri-skills --skill tauri-sidecar

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
10.9 KB
---
name: embedding-tauri-sidecars
description: Teaches the assistant how to embed and execute external binaries (sidecars) in Tauri applications, including configuration, cross-platform executable naming, and Rust/JavaScript APIs for spawning sidecar processes.
---

# Tauri Sidecars: Embedding External Binaries

This skill covers embedding and executing external binaries (sidecars) in Tauri applications, including configuration, cross-platform considerations, and execution from Rust and JavaScript.

## Overview

Sidecars are external binaries embedded within Tauri applications to extend functionality or eliminate the need for users to install dependencies. They can be executables written in any programming language.

**Common Use Cases:**
- Python CLI applications packaged with PyInstaller
- Go or Rust compiled binaries for specific tasks
- Node.js applications bundled as executables
- API servers or background services

## Plugin Dependency

Sidecars require the shell plugin:

**Cargo.toml:**
```toml
[dependencies]
tauri-plugin-shell = "2"
```

**Register in main.rs:**
```rust
fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

**Frontend package:**
```bash
npm install @tauri-apps/plugin-shell
```

## Configuration

### Registering Sidecars

Configure sidecars in `tauri.conf.json` under `bundle.externalBin`. Paths are relative to `src-tauri`:

```json
{
  "bundle": {
    "externalBin": [
      "binaries/my-sidecar",
      "../external/processor"
    ]
  }
}
```

**Important:** The path is a stem. Tauri appends the target triple suffix at build time.

### Cross-Platform Binary Naming

Each sidecar requires platform-specific variants with target triple suffixes:

| Platform | Architecture | Required Filename |
|----------|--------------|-------------------|
| Linux | x86_64 | `my-sidecar-x86_64-unknown-linux-gnu` |
| Linux | ARM64 | `my-sidecar-aarch64-unknown-linux-gnu` |
| macOS | Intel | `my-sidecar-x86_64-apple-darwin` |
| macOS | Apple Silicon | `my-sidecar-aarch64-apple-darwin` |
| Windows | x86_64 | `my-sidecar-x86_64-pc-windows-msvc.exe` |

**Determine your target triple:**
```bash
rustc --print host-tuple    # Rust 1.84.0+
rustc -Vv | grep host       # Older versions
```

### Directory Structure

```
src-tauri/
  binaries/
    my-sidecar-x86_64-unknown-linux-gnu
    my-sidecar-aarch64-apple-darwin
    my-sidecar-x86_64-apple-darwin
    my-sidecar-x86_64-pc-windows-msvc.exe
  tauri.conf.json
  src/main.rs
```

## Executing Sidecars from Rust

### Basic Execution

```rust
use tauri_plugin_shell::ShellExt;

#[tauri::command]
async fn run_sidecar(app: tauri::AppHandle) -> Result<String, String> {
    let output = app
        .shell()
        .sidecar("my-sidecar")
        .map_err(|e| e.to_string())?
        .output()
        .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())
    }
}
```

**Note:** Pass only the filename to `sidecar()`, not the full path from configuration.

### With Arguments

```rust
#[tauri::command]
async fn process_file(app: tauri::AppHandle, file_path: String) -> Result<String, String> {
    let output = app
        .shell()
        .sidecar("processor")
        .map_err(|e| e.to_string())?
        .args(["--input", &file_path, "--format", "json"])
        .output()
        .await
        .map_err(|e| e.to_string())?;

    Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
```

### Spawning Long-Running Processes

For sidecars that run continuously (API servers, watchers):

```rust
use tauri_plugin_shell::{ShellExt, process::CommandEvent};

#[tauri::command]
async fn start_server(app: tauri::AppHandle) -> Result<u32, String> {
    let (mut rx, child) = app
        .shell()
        .sidecar("api-server")
        .map_err(|e| e.to_string())?
        .args(["--port", "8080"])
        .spawn()
        .map_err(|e| e.to_string())?;

    let pid = child.pid();

    tauri::async_runtime::spawn(async move {
        while let Some(event) = rx.recv().await {
            match event {
                CommandEvent::Stdout(line) => println!("{}", String::from_utf8_lossy(&line)),
                CommandEvent::Stderr(line) => eprintln!("{}", String::from_utf8_lossy(&line)),
                CommandEvent::Terminated(payload) => {
                    println!("Terminated: {:?}", payload.code);
                    break;
                }
                _ => {}
            }
        }
    });

    Ok(pid)
}
```

### Managing Sidecar Lifecycle

```rust
use std::sync::Mutex;
use tauri::State;
use tauri_plugin_shell::{ShellExt, process::CommandChild};

struct SidecarState {
    child: Mutex<Option<CommandChild>>,
}

#[tauri::command]
async fn start_sidecar(app: tauri::AppHandle, state: State<'_, SidecarState>) -> Result<(), String> {
    let (_, child) = app.shell().sidecar("service").map_err(|e| e.to_string())?
        .spawn().map_err(|e| e.to_string())?;
    *state.child.lock().unwrap() = Some(child);
    Ok(())
}

#[tauri::command]
async fn stop_sidecar(state: State<'_, SidecarState>) -> Result<(), String> {
    if let Some(child) = state.child.lock().unwrap().take() {
        child.kill().map_err(|e| e.to_string())?;
    }
    Ok(())
}
```

## Executing Sidecars from JavaScript

### Permission Configuration

Grant shell execution permissions in `src-tauri/capabilities/default.json`:

```json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "core:default",
    {
      "identifier": "shell:allow-execute",
      "allow": [{ "name": "binaries/my-sidecar", "sidecar": true }]
    }
  ]
}
```

### Basic Execution

```typescript
import { Command } from '@tauri-apps/plugin-shell';

async function runSidecar(): Promise<string> {
  const command = Command.sidecar('binaries/my-sidecar');
  const output = await command.execute();
  if (output.code === 0) return output.stdout;
  throw new Error(output.stderr);
}
```

### With Arguments

```typescript
async function processFile(filePath: string): Promise<string> {
  const command = Command.sidecar('binaries/processor', [
    '--input', filePath, '--format', 'json'
  ]);
  const output = await command.execute();
  return output.stdout;
}
```

### Handling Streaming Output

```typescript
import { Command, Child } from '@tauri-apps/plugin-shell';

async function runWithStreaming(): Promise<Child> {
  const command = Command.sidecar('binaries/long-task');

  command.on('close', (data) => console.log(`Finished: ${data.code}`));
  command.on('error', (error) => console.error(error));
  command.stdout.on('data', (line) => console.log(line));
  command.stderr.on('data', (line) => console.error(line));

  return await command.spawn();
}
```

### Managing Long-Running Processes

```typescript
let serverProcess: Child | null = null;

async function startServer(): Promise<number> {
  const command = Command.sidecar('binaries/api-server', ['--port', '8080']);
  command.stdout.on('data', console.log);
  serverProcess = await command.spawn();
  return serverProcess.pid;
}

async function stopServer(): Promise<void> {
  if (serverProcess) {
    await serverProcess.kill();
    serverProcess = null;
  }
}
```

## Argument Validation

Configure argument validation in capabilities:

```json
{
  "identifier": "shell:allow-execute",
  "allow": [{
    "name": "binaries/my-sidecar",
    "sidecar": true,
    "args": [
      "-o",
      "--verbose",
      { "validator": "\\S+" }
    ]
  }]
}
```

**Argument types:**
- **Static string**: Exact match required (`-o`, `--verbose`)
- **Validator object**: Regex pattern for dynamic values
- **`true`**: Allow any argument (use with caution)

## Cross-Platform Considerations

### Building Platform-Specific Binaries

**Rust sidecars:**
```bash
cargo build --release --target x86_64-unknown-linux-gnu
cp target/x86_64-unknown-linux-gnu/release/my-tool \
   src-tauri/binaries/my-tool-x86_64-unknown-linux-gnu
```

**Python with PyInstaller:**
```bash
pyinstaller --onefile my_script.py
mv dist/my_script dist/my_script-x86_64-unknown-linux-gnu
```

### Platform Notes

**Windows:**
- Executables must have `.exe` extension
- Handle line endings in text file processing

**macOS:**
- Use `lipo` for universal binaries (Intel + Apple Silicon)
- Code signing may be required for distribution
- Gatekeeper may block unsigned sidecars

**Linux:**
- Mark binaries as executable (`chmod +x`)
- Consider glibc version compatibility
- Static linking reduces dependency issues

## Complete Example

**tauri.conf.json:**
```json
{
  "productName": "My App",
  "version": "1.0.0",
  "identifier": "com.example.myapp",
  "bundle": {
    "externalBin": ["binaries/data-processor"]
  }
}
```

**capabilities/default.json:**
```json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "core:default",
    {
      "identifier": "shell:allow-execute",
      "allow": [{
        "name": "binaries/data-processor",
        "sidecar": true,
        "args": [
          "--input", { "validator": "^[a-zA-Z0-9_\\-./]+$" },
          "--output", { "validator": "^[a-zA-Z0-9_\\-./]+$" }
        ]
      }]
    }
  ]
}
```

**src/main.rs:**
```rust
use tauri_plugin_shell::ShellExt;

#[tauri::command]
async fn process_data(app: tauri::AppHandle, input: String, output: String) -> Result<String, String> {
    let result = app.shell().sidecar("data-processor").map_err(|e| e.to_string())?
        .args(["--input", &input, "--output", &output])
        .output().await.map_err(|e| e.to_string())?;

    if result.status.success() {
        Ok(String::from_utf8_lossy(&result.stdout).to_string())
    } else {
        Err(String::from_utf8_lossy(&result.stderr).to_string())
    }
}

fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![process_data])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

**Frontend (App.tsx):**
```tsx
import { invoke } from '@tauri-apps/api/core';

function App() {
  const handleProcess = async () => {
    try {
      const result = await invoke('process_data', {
        input: '/path/to/input.txt',
        output: '/path/to/output.txt'
      });
      console.log('Result:', result);
    } catch (error) {
      console.error('Error:', error);
    }
  };

  return <button onClick={handleProcess}>Process Data</button>;
}
```

## Best Practices

1. **Validate all sidecar paths**: Never pass untrusted paths to sidecars
2. **Use argument validators**: Restrict allowed arguments in capabilities
3. **Handle errors gracefully**: Sidecars may fail or be missing
4. **Clean up processes**: Kill spawned processes on app exit
5. **Test on all platforms**: Binary naming and execution varies
6. **Consider binary size**: Sidecars increase bundle size significantly

Overview

This skill teaches how to embed and run external binaries (sidecars) inside Tauri v2 applications. It covers configuration, cross-platform executable naming, and the Rust and JavaScript APIs for launching, streaming, and managing sidecar processes. The goal is to let you bundle auxiliary tools so users do not need separate installations.

How this skill works

Register sidecars in tauri.conf.json under bundle.externalBin and include platform-specific filenames (target-triple suffixes) in src-tauri/binaries. Use the tauri-plugin-shell in Rust and @tauri-apps/plugin-shell in the frontend to execute sidecars, pass arguments, capture output, stream logs, and manage long-running processes. Capabilities must grant explicit shell permissions and optional argument validators to restrict allowed arguments.

When to use it

  • You need to bundle a CLI tool, small server, or native helper with your app so users do not install it separately.
  • A platform-specific compiled binary (Rust, Go, Python packaged with PyInstaller) provides functionality not available in the web layer.
  • You require streaming stdout/stderr or background processes (API server, file watcher) managed by the app.
  • You want to minimize runtime dependencies by shipping self-contained executables with your installer.
  • You must validate and restrict command arguments for security-sensitive operations.

Best practices

  • Add tauri-plugin-shell and register it in main.rs before using sidecars.
  • Place platform variants in src-tauri/binaries with target triple suffixes and list stems in bundle.externalBin.
  • Define shell permissions and argument validators in capabilities to limit execution and allowed args.
  • Handle errors and missing binaries gracefully; check process exit status and return stderr for failures.
  • Manage lifecycle: spawn long-running sidecars, capture logs, and kill processes on app exit.
  • Test builds on every target (Windows, macOS Intel/Apple Silicon, Linux x86_64/ARM) and ensure executables have proper permissions.

Example use cases

  • Bundle a Rust or Go data-processor binary and invoke it from frontend via an invoke handler.
  • Package a Python CLI with PyInstaller as a sidecar for repeated local data conversions.
  • Spawn a local API server sidecar and stream logs to the app UI while keeping it running in background.
  • Use argument validators in capabilities to safely allow file paths and restrict dangerous flags.
  • Provide platform-specific binaries to avoid runtime dependency issues across user machines.

FAQ

Do sidecar filenames need extensions or specific suffixes?

Yes. Provide platform-specific files with the target triple suffix and .exe for Windows. In tauri.conf.json list the stem; Tauri resolves the correct platform file at build time.

How do I restrict what arguments a sidecar can receive?

Configure argument validators in your capabilities JSON. You can list static allowed strings, regex validators, or use true to allow any argument (use sparingly).