Plugin Setup
This guide walks you through integrating tauri-plugin-pilot into an existing Tauri v2 application.
1. Add the dependency
Section titled “1. Add the dependency”In your app’s src-tauri/Cargo.toml, add the plugin under [dependencies]:
[dependencies]tauri-plugin-pilot = { git = "https://github.com/mpiton/tauri-pilot" }2. Register the plugin
Section titled “2. Register the plugin”Register the plugin in your app entry point using a #[cfg(debug_assertions)] guard:
fn main() { let mut builder = tauri::Builder::default();
#[cfg(debug_assertions)] { builder = builder.plugin(tauri_plugin_pilot::init()); }
builder.run(tauri::generate_context!()).expect("error running app");}3. Debug-only compilation
Section titled “3. Debug-only compilation”The #[cfg(debug_assertions)] guard is intentional and important:
- The plugin is only compiled and included in debug builds
- Production builds (
cargo build --release) will not include the plugin - There is zero runtime overhead or binary size impact in production
- No need to strip or disable the plugin before shipping
On Unix (Android excepted), a debug build also watches SIGINT, SIGTERM and
SIGHUP. Those signals end the process before Tauri emits RunEvent::Exit, so
the plugin removes the socket file itself, restores the default handler and
re-raises the signal: the app still exits with the usual 128+n status. If your
app installs its own handler for those signals, it still runs, but the re-raise
can cut a long graceful shutdown short.
On Windows, a debug build watches the console control events instead: Ctrl+C,
Ctrl+Break and console close. The plugin removes
%LOCALAPPDATA%\tauri-pilot\instances\{identifier}.json, then exits with STATUS_CONTROL_C_EXIT
(0xC000013A), the status the default handler gives. This needs tokio 1.44
or newer.
Windows calls console handlers last registered first and stops at the first
one that claims the event, which the plugin’s does. A handler your app
registered before the plugin’s setup, for example ctrlc::set_handler in
main, is therefore not called in a debug build, so cleanup it does (killing
a sidecar, say) does not run. A handler registered later runs first. A
tokio::signal listener runs alongside the plugin’s, but the exit cuts it
short.
System shutdown and logoff still leave the instance file: Windows delivers
neither to a console handler once the process has loaded user32.dll, which
every Tauri app has. The CLI skips an entry whose process is gone.
Release builds never see any of this.
4. Socket path
Section titled “4. Socket path”Once your app starts in dev mode, the plugin creates a Unix socket at:
$XDG_RUNTIME_DIR/tauri-pilot-{identifier}.sockThe {identifier} value comes from the identifier field in your tauri.conf.json.
The plugin uses $XDG_RUNTIME_DIR only when it is a private directory: owned by you, with no group or other access. When it is unset, empty or not private, the socket goes to /tmp/tauri-pilot-{identifier}.sock instead. For a non-private directory the plugin also emits the warn-level event XDG_RUNTIME_DIR is not a private directory, falling back to /tmp. Most Linux desktops set a private $XDG_RUNTIME_DIR (/run/user/<uid>). macOS usually leaves it unset, so the socket lands in /tmp.
On Windows the plugin listens on the Named Pipe \\.\pipe\tauri-pilot-{identifier} and registers it in %LOCALAPPDATA%\tauri-pilot\instances\{identifier}.json.
Example: an app with identifier com.myapp.dev, run by user 1000 on a Linux desktop, creates the socket at:
/run/user/1000/tauri-pilot-com.myapp.dev.sockThe plugin reports the path in use through tracing events: the info-level tauri-pilot socket listening (tauri-pilot named pipe listening on Windows). They appear only if the app captures tauri_plugin_pilot tracing events, for example with a tracing-subscriber; a default Tauri app prints neither line. To check without a subscriber on Linux or macOS, list the sockets in both locations (no output means no socket):
find -H /tmp ${XDG_RUNTIME_DIR:+"$XDG_RUNTIME_DIR"} -maxdepth 1 -type s -name 'tauri-pilot-*.sock' 2>/dev/nullOn Windows, list the instance files instead (PowerShell):
Get-ChildItem "$env:LOCALAPPDATA\tauri-pilot\instances"The CLI auto-discovers the socket when you run commands; see Socket Auto-Detection for the order it searches in. To talk to it by hand:
echo '{"jsonrpc":"2.0","id":1,"method":"ping"}' | socat - UNIX-CONNECT:"$XDG_RUNTIME_DIR/tauri-pilot-com.myapp.dev.sock"# after the /tmp fallback:echo '{"jsonrpc":"2.0","id":1,"method":"ping"}' | socat - UNIX-CONNECT:/tmp/tauri-pilot-com.myapp.dev.sock5. Permissions
Section titled “5. Permissions”The plugin requires the pilot:default permission for its internal __callback IPC command. Add it to your capability file (e.g. src-tauri/capabilities/default.json):
{ "remote": { "urls": ["https://allowed.example/*"] }, "permissions": ["core:default", "pilot:default"]}Without this permission, eval commands will time out with “eval timed out after 10s”. List a foreign origin in remote.urls on the same capability if you need to drive that origin; otherwise commands there fail fast.
6. Verify the setup
Section titled “6. Verify the setup”Start your Tauri app in development mode, then test the connection from a second terminal:
# Terminal 1 — start your appcargo tauri dev
# Terminal 2 — verify the plugin is reachabletauri-pilot ping# Connected. Plugin and CLI both 0.8.1.ping reports the plugin version compiled into your app alongside the CLI version. When they match, you’re ready to start using the snapshot/action workflow.
A successful ping only means the socket answers. Right after launch it can answer about a second before the app’s first window exists. Commands without --window wait up to 3 s for that window, then fail with error.data.error = "NO_WEBVIEW", which a script can retry.
7. Keep the plugin and CLI in sync
Section titled “7. Keep the plugin and CLI in sync”The plugin is a Rust dependency compiled into your app. The CLI is a separate binary. They’re versioned independently, so they drift apart if you update one and not the other. ping surfaces a drift:
tauri-pilot ping# Connected. Plugin 0.8.0, CLI 0.8.1.# Plugin 0.8.0 and CLI 0.8.1 differ. Rebuild your app against tauri-plugin-pilot 0.8.1 ...If ping reports Plugin <= 0.7.0, or eval commands fail on macOS with native WebKit eval callback returned an error, the plugin baked into your app predates the unified eval path (removed in 0.7.1). Update it and rebuild:
# git dependency (the setup above): pull the latest commitcargo update -p tauri-plugin-pilot
# or pin a released version in src-tauri/Cargo.toml:# tauri-plugin-pilot = "0.8.1"
# then rebuildcargo tauri devAndroid via ADB
Section titled “Android via ADB”The computer running the CLI must use Linux or macOS. The Windows CLI uses named pipes and cannot use this Unix socket forwarding setup.
Before connecting:
- Complete Tauri’s Android prerequisites, including the SDK, NDK,
ANDROID_HOMEandNDK_HOME, and putadbonPATH. - Run
cargo tauri android initonce for the app. Use an emulator or a device with USB debugging enabled and the computer authorized. - Register the plugin in the mobile
run()entry point insrc-tauri/src/lib.rs, with the debug guard andpilot:defaultpermission shown above. - Configure the app’s logging to capture
tauri_plugin_pilottracing events at info level. The listening event contains the socket name needed for forwarding.
Disabling the desktop press backend on Android is recommended. Replace the shared plugin dependency with target-specific entries so desktop builds keep it enabled:
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]tauri-plugin-pilot = { git = "https://github.com/mpiton/tauri-pilot", default-features = false }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]tauri-plugin-pilot = { git = "https://github.com/mpiton/tauri-pilot" }Start a debug build with cargo tauri android dev. Select the device and read the tauri-pilot socket listening event from the intended app’s startup logs. For apps logging to Logcat, use the Android application ID to select its process:
adb devices -lpilot_device=YOUR_DEVICE_SERIALpilot_package=YOUR_ANDROID_APPLICATION_IDpilot_pid=$(adb -s "$pilot_device" shell pidof "$pilot_package")adb -s "$pilot_device" logcat -d --pid="$pilot_pid" | grep 'tauri-pilot socket listening'Copy the complete tauri-pilot-{identifier}-{random}.sock name into pilot_name below. The identifier is limited to 16 characters, followed by 16 random hexadecimal digits. The name changes each time the app starts, so update the forwarding after a restart. Once the page has loaded:
pilot_name=NAME_FROM_STARTUP_LOGpilot_dir=$(mktemp -d /tmp/tauri-pilot.XXXXXX)export TAURI_PILOT_SOCKET="$pilot_dir/pilot.sock"adb -s "$pilot_device" forward "localfilesystem:$TAURI_PILOT_SOCKET" \ "localabstract:$pilot_name"tauri-pilot pingtauri-pilot snapshotUse the same -s "$pilot_device" for all ADB commands when multiple devices are connected. TAURI_PILOT_SOCKET also applies to subsequent CLI commands and MCP processes launched from this shell.
press is unavailable on Android and screenshot_native is macOS-only. Use fill or type for text input. Window operations are limited to the mobile app’s single window.
If forwarding succeeds but the CLI cannot connect, check adb -s "$pilot_device" shell cat /proc/net/unix | grep tauri-pilot and compare the name with the current startup log. The plugin only listens in debug builds.
After the app restarts, the old forwarding still accepts connections but has no socket behind it, so every command fails with Server closed the connection and a hint about a restarted app. Read the new name from the tauri-pilot socket listening log line and run the adb forward command above again. adb forward --list shows the stale entry.
windows omits title on Android and iOS, where windows have no native title. Use tauri-pilot title for the page’s document.title.
Remove the forwarding created above when finished:
adb -s "$pilot_device" forward --remove "localfilesystem:$TAURI_PILOT_SOCKET"rm -f "$TAURI_PILOT_SOCKET"rmdir "$pilot_dir"unset TAURI_PILOT_SOCKETOn Linux, forwarding to a socket named tauri-pilot-{identifier}.sock inside your private $XDG_RUNTIME_DIR also enables CLI auto-discovery without TAURI_PILOT_SOCKET. For this alternative, remove only the forward and socket when finished, keeping the runtime directory:
adb -s "$pilot_device" forward --remove "localfilesystem:$XDG_RUNTIME_DIR/tauri-pilot-{identifier}.sock"rm -f "$XDG_RUNTIME_DIR/tauri-pilot-{identifier}.sock"Use a private directory rather than a bare /tmp socket, since ADB creates the host socket with its own permissions.
iOS Simulator
Section titled “iOS Simulator”The computer running the CLI must be a Mac, since building for iOS requires Xcode. A Simulator shares the Mac’s filesystem, so the plugin’s usual socket file is already reachable from the CLI and no forwarding is needed.
Before connecting:
- Complete Tauri’s iOS prerequisites,
including Xcode and the
aarch64-apple-ios-simRust target (x86_64-apple-ioson an Intel Mac). - Run
cargo tauri ios initonce for the app. - Register the plugin in the mobile
run()entry point insrc-tauri/src/lib.rs, with the debug guard andpilot:defaultpermission shown above. A capability that restrictsplatformsmust includeiOS.
Disabling the desktop press backend on iOS is recommended. Replace the shared
plugin dependency — or the combined pair from the Android section above — with these
target-specific entries, so desktop builds keep it enabled:
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]tauri-plugin-pilot = { git = "https://github.com/mpiton/tauri-pilot", default-features = false }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]tauri-plugin-pilot = { git = "https://github.com/mpiton/tauri-pilot" }Cargo unions features across dependency entries, so use this single combined
pair rather than one pair per platform. A leftover not(target_os = "android")
or a naive not(target_os = "ios") entry matches the other mobile OS and
re-enables press there.
press needs an OS keyboard backend that iOS does not provide, and
screenshot_native is macOS-only. Use fill or type for text input, and the
regular screenshot, which captures the webview. Window operations are limited
to the mobile app’s single window.
Start a debug build in a Simulator and use the CLI exactly as you would for a desktop app:
cargo tauri ios devtauri-pilot pingtauri-pilot snapshotA desktop build and every Simulator running the same app all resolve to the
same default socket path. Whichever starts first owns it; the others log a
warning, start without a pilot server, and the CLI then drives that first
instance instead. Quit the extra ones, or give a Simulator its own socket
directory. The app must already be installed on it (one cargo tauri ios dev
run against that Simulator does this):
mkdir -m 700 /tmp/pilot-my-simulatorSIMCTL_CHILD_XDG_RUNTIME_DIR=/tmp/pilot-my-simulator \ xcrun simctl launch --terminate-running-process SIMULATOR_UDID YOUR_BUNDLE_IDENTIFIERtauri-pilot --socket /tmp/pilot-my-simulator/tauri-pilot-YOUR_BUNDLE_IDENTIFIER.sock pingKeep that directory short, owned by you, and mode 0700 — the plugin ignores a
non-private XDG_RUNTIME_DIR and falls back to /tmp, and a long path (for
example, deep inside a Simulator’s data container) can exceed the length limit
on Unix socket addresses.
Physical iOS devices are not supported yet: an app sandbox on a device hides its socket file from the Mac, so the CLI has no path to open.
