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

> Securely implement file system access in Tauri using the fs plugin, strict path glob patterns, and runtime permissions with explicit user consent. Learn how in our complete guide from tauri-apps/tauri.

- Repository: [Tauri/tauri](https://github.com/tauri-apps/tauri)
- Tags: how-to-guide
- Published: 2026-02-26

---

**To implement file system access in Tauri securely, enable the `fs` plugin, define strict path glob patterns in [`tauri.conf.json`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/tauri.conf.json) that declares baseline `allowedPaths` and `forbiddenPaths` using glob patterns.
- **`Scope` struct** – Defined in [`crates/tauri/src/scope/fs.rs`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/tauri.conf.json) into a `tauri_utils::config::FsScope` value. The `Scope::new` constructor (lines 75-88 of [`fs.rs`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/tauri.conf.json) before the application launches:

```json
{
  "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:

```rust
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`](https://github.com/tauri-apps/tauri/blob/main/Cargo.toml):

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

```

### 2. Register the Plugin

Initialize the plugin in [`src/main.rs`](https://github.com/tauri-apps/tauri/blob/main/src/main.rs):

```rust
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`](https://github.com/tauri-apps/tauri/blob/main/tauri.conf.json). Use environment variables like `$APPDATA` and `$HOME` for cross-platform compatibility:

```json
{
  "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:

```typescript
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:

```rust
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`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri/src/scope/fs.rs) | Implements `Scope::is_allowed`, pattern matching, and the `allow_*`/`forbid_*` methods. |
| [`crates/tauri/src/lib.rs`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri/src/lib.rs) | Provides `AppHandle::fs_scope()` accessor at line 767. |
| [`crates/tauri-utils/src/config.rs`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri-utils/src/config.rs) | Defines `FsScope` configuration structure. |
| [`examples/file-associations/src-tauri/src/main.rs`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/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`](https://github.com/tauri-apps/tauri/blob/main/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.

### Why does Tauri resolve symlinks before checking file permissions?

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`](https://github.com/tauri-apps/tauri/blob/main/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.