How to Implement File System Access in Tauri Securely: A Complete Guide

To implement file system access in Tauri securely, enable the fs plugin, define strict path glob patterns in tauri.conf.json, and extend permissions at runtime only after explicit user consent using AppHandle::fs_scope().

Tauri isolates the frontend from the operating system by default, requiring explicit opt-in for any file operations. When you implement file system access in Tauri securely, you configure the fs plugin with a Scope that defines exactly which paths are permitted or forbidden. According to the tauri-apps/tauri source code, all access checks are performed in Rust before any filesystem operation executes, ensuring the frontend can only touch explicitly whitelisted paths.

Understanding the Security Architecture

Tauri's filesystem security relies on a multi-layered approach that combines static configuration with runtime enforcement. The system ensures that forbidden patterns always override allowed patterns, providing defense-in-depth protection.

Core Security Components

  • FsScope configuration – The JSON configuration in tauri.conf.json that declares baseline allowedPaths and forbiddenPaths using glob patterns.
  • Scope struct – Defined in crates/tauri/src/scope/fs.rs, this holds compiled glob patterns, resolves symlinks, and determines if a path is permitted.
  • tauri-plugin-fs – Exposes JavaScript APIs like readFile and writeFile that forward calls to Rust where scope verification occurs.
  • AppHandle::fs_scope() – Located in crates/tauri/src/lib.rs, this accessor allows runtime extension of the whitelist after user interaction.

How Permission Checks Work

When the application starts, Tauri parses the fs block from tauri.conf.json into a tauri_utils::config::FsScope value. The Scope::new constructor (lines 75-88 of fs.rs) processes each path through push_pattern, building three glob variants: the literal path, an escaped version, and a canonicalized parent.

During runtime, the Scope::is_allowed method (lines 58-71 of fs.rs) performs the following checks:

  1. Resolves symlinks and canonicalizes the incoming path using try_resolve_symlink_and_canonicalize.
  2. Tests against forbidden_patterns first; any match immediately denies access.
  3. If not forbidden, tests against allowed_patterns; only matches permit access.

This logic guarantees that explicit forbid entries always win, even if a path matches both lists.

Static Configuration vs. Runtime Permissions

Secure file system access requires configuring both baseline restrictions and dynamic extensions. The static configuration establishes a minimal attack surface at startup, while runtime permissions grant temporary access based on user actions.

Defining the Baseline Scope

Configure strict defaults in tauri.conf.json before the application launches:

{
  "tauri": {
    "security": { "csp": "default-src 'self'" },
    "allowlist": { "fs": { "all": false } },
    "fs": {
      "allowedPaths": [
        "$APPDATA/**",
        "$HOME/Documents/**"
      ],
      "forbiddenPaths": [
        "$HOME/**/.ssh/**",
        "$HOME/**/.gnupg/**"
      ]
    }
  }
}

Critical: Keep allowlist.fs.all set to false to prevent blanket access. Only the glob patterns listed in allowedPaths will be reachable at startup.

Extending Permissions at Runtime

When users select folders through native dialogs, grant access programmatically using the Scope API:

use tauri::Manager;

#[tauri::command]
async fn add_user_folder(app: tauri::AppHandle, folder: String) -> Result<(), String> {
    let path = std::path::PathBuf::from(folder);
    app.fs_scope()
        .map_err(|e| e.to_string())?
        .allow_directory(path, true)
        .map_err(|e| e.to_string())
}

The second parameter (true in the example) enables recursive access to subdirectories. This approach ensures you implement file system access in Tauri securely by tying permissions to explicit user consent.

Step-by-Step Implementation

Follow these steps to enable secure file operations in your Tauri application.

1. Enable the FS Plugin

Add the required dependencies to your Cargo.toml:

[dependencies]
tauri = { version = "1", features = ["fs"] }
tauri-plugin-fs = "1"

2. Register the Plugin

Initialize the plugin in src/main.rs:

fn main() {
  tauri::Builder::default()
    .plugin(tauri_plugin_fs::init())
    .setup(|app| {
      // Example: pre-grant a specific directory during setup
      let selected = std::path::PathBuf::from("C:/temp/user_data");
      app.fs_scope()?.allow_directory(selected, true)?;
      Ok(())
    })
    .run(tauri::generate_context!())
    .expect("failed to run Tauri app");
}

3. Configure Strict Baselines

Define minimal permissions in tauri.conf.json. Use environment variables like $APPDATA and $HOME for cross-platform compatibility:

{
  "tauri": {
    "fs": {
      "allowedPaths": [
        "$APPDATA/**"
      ],
      "forbiddenPaths": [
        "$HOME/**/.ssh/**"
      ]
    }
  }
}

4. Implement Frontend Operations

Use the TypeScript API to read and write files. Operations automatically fail if the path violates the current scope:

import { readTextFile, writeTextFile } from '@tauri-apps/api/fs';

async function loadConfig() {
  // Succeeds only if path matches allowed patterns
  const content = await readTextFile('$APPDATA/config.json');
  return content;
}

async function saveUserData(data: string) {
  // Requires runtime permission grant first
  await writeTextFile('$APPDATA/user/data.txt', data);
}

Monitoring Scope Changes

For auditing or UI updates, attach listeners to scope modifications using the Scope::listen method:

let id = app.fs_scope()?.listen(Box::new(|event| {
    match event {
        tauri::scope::Event::PathAllowed(p) => 
            println!("Allowed: {}", p.display()),
        tauri::scope::Event::PathForbidden(p) => 
            println!("Forbidden: {}", p.display()),
    }
}));

Store the returned id to call unlisten later if you need to remove the observer.

Key Source Files Reference

Understanding these implementation details helps debug permission issues:

File Purpose
crates/tauri/src/scope/fs.rs Implements Scope::is_allowed, pattern matching, and the allow_*/forbid_* methods.
crates/tauri/src/lib.rs Provides AppHandle::fs_scope() accessor at line 767.
crates/tauri-utils/src/config.rs Defines FsScope configuration structure.
examples/file-associations/src-tauri/src/main.rs Demonstrates real-world usage of runtime scope extension.

Summary

  • Never enable unrestricted access – Keep allowlist.fs.all disabled to prevent unauthorized file operations.
  • Define narrow static patterns – Use tauri.conf.json to whitelist only essential directories like $APPDATA.
  • Extend scope only after user consent – Call app.fs_scope()?.allow_directory() following dialog selection or explicit user action.
  • Leverage forbidden paths – Explicitly blacklist sensitive directories (.ssh, .gnupg) to ensure they remain inaccessible even if allowed patterns are overly broad.
  • Audit scope changes – Use Scope::listen to track which paths become accessible during runtime.

Frequently Asked Questions

What's the difference between allowedPaths and forbiddenPaths in Tauri?

allowedPaths defines glob patterns that the frontend can access, while forbiddenPaths defines patterns that are explicitly blocked. According to the logic in crates/tauri/src/scope/fs.rs, the Scope::is_allowed method checks forbidden patterns first, meaning forbidden entries always override allowed ones even if a path matches both lists.

How do I grant file access after a user selects a folder in a dialog?

Use the AppHandle::fs_scope() method to extend permissions at runtime. After obtaining the path from a file dialog, call app.fs_scope()?.allow_directory(path, true) to grant recursive access. This pattern ensures you only implement file system access in Tauri securely when the user explicitly chooses the location.

The Scope::is_allowed method calls try_resolve_symlink_and_canonicalize before pattern matching to prevent symlink attacks. Without this canonicalization, an attacker could create a symlink in an allowed directory pointing to a forbidden location (like /etc/passwd), bypassing security restrictions. The canonicalization ensures the actual resolved path is checked against your scope.

Can I restrict access to specific file extensions only?

The built-in Scope in fs.rs operates on path patterns rather than file extensions. To restrict by extension, either structure your allowedPaths globs to include extensions (e.g., $APPDATA/**/*.json) or implement additional validation in your Rust commands before calling filesystem operations. For granular extension control, combine scope patterns with explicit checks in your #[tauri::command] functions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →