How PI-Desktop File Scope Configuration Works: A Complete Guide to Plugin Permissions

PI-Desktop enforces fine-grained file access through a dual-layer system combining activation scopes (where extensions run) and manifest-declared permission scopes (what files they can access).

The PI-Desktop project (vastsa/PI-Desktop) implements a rigorous permission model that ensures plugins, MCP servers, and user skills operate strictly within defined boundaries. Understanding how file scope configuration works is essential for both extension developers and users managing workspace security.

Understanding Activation Scopes

Activation scopes determine the contextual boundaries where an extension is permitted to execute. This mechanism prevents plugins from activating in inappropriate project contexts.

Global vs. Project-Specific Activation

Each activatable record in the system stores an enabled flag alongside an optional scope field of type ActivationScope, defined in [packages/shared/src/activation.ts](https://github.com/vastsa/PI-Desktop/blob/main/packages/shared/src/activation.ts) (lines 13-22). The default configuration uses GLOBAL_SCOPE, meaning the extension remains active across every project without restriction.

When operating in "projects" mode, the projects array contains absolute project paths that the extension may access. These paths undergo normalization through normalizeProjectPath to ensure consistent comparison across different operating systems and path formats.

Core Helper Functions

The activation system relies on two primary utilities:

  • resolveScope (lines 81-96) normalizes potentially missing scope configurations, ensuring consistent data structures for downstream validation
  • isActiveInProject (lines 101-115) performs the actual membership test, determining whether a specific extension should execute for a given project path
import {
  isActiveInProject,
  GLOBAL_SCOPE,
  resolveScope,
} from "./activation";

const plugin = { 
  enabled: true, 
  scope: { 
    mode: "projects", 
    projects: ["/home/user/myproject"] 
  } 
};

const active = isActiveInProject(plugin, "/home/user/myproject/src"); // → true

Declaring File Scope in Plugin Manifests

While activation scopes control where plugins run, permission scopes (file scopes) govern what files the extension may read, write, or delete.

The fs Block Structure

In a plugin's manifest.json, the fs block contains granular declarations for each filesystem operation. Each permission category—read, write, and delete—accepts a scope array containing glob patterns that define accessible paths:

{
  "permissions": ["fs.read", "fs.write"],
  "fs": {
    "read": { 
      "scope": ["src/**/*.ts", "README.md"] 
    },
    "write": { 
      "scope": ["src/generated/**"] 
    },
    "delete": { 
      "own": true, 
      "scope": ["dist/**"] 
    }
  }
}

Validation Rules

The Plugin SDK validates scope consistency during manifest parsing. A plugin requesting fs.read must declare a non-empty read.scope array; otherwise, the SDK rejects the manifest as invalid. This validation logic appears in [packages/plugin-sdk/src/index.test.ts](https://github.com/vastsa/PI-Desktop/blob/main/packages/plugin-sdk/src/index.test.ts) (lines 363-387), which provides comprehensive test coverage for acceptance and rejection scenarios.

Runtime Enforcement Mechanisms

File scope configuration transitions from declaration to enforcement when the Electron main process handles IPC calls.

Permission Assertions

When the host receives an IPC call such as fs.read, the runtime first verifies the calling plugin possesses the requisite permission through assertPermission. This check occurs before any file system interaction begins.

Path Validation Against Scope

Following permission verification, the runtime resolves the target path against the plugin's declared file scope. If the requested path falls outside the permitted glob set, the operation terminates with an access denial. This security-critical logic resides in [apps/desktop/electron/main/plugin-runtime.ts](https://github.com/vastsa/PI-Desktop/blob/main/apps/desktop/electron/main/plugin-runtime.ts):

function handleFsRead(request) {
  const { pluginId, path } = request;
  const plugin = getLoadedPlugin(pluginId);
  
  assertPermission(plugin, "fs.read"); // Permission check
  
  if (!matchesScope(plugin.manifest.fs.read.scope, path)) {
    throw new Error("Path outside declared file scope");
  }
  
  return fs.readFile(path, "utf8");
}

Legacy Compatibility and Migration

Older PI-Desktop manifests utilized the legacy permission fs.read.workspace. The current SDK maintains backward compatibility by accepting this deprecated format but internally mapping it to the modern fs.read model. This migration path enforces identical scope rules to ensure no security degradation during upgrades. Test cases covering these legacy mappings appear around lines 399-410 in [packages/plugin-sdk/src/index.test.ts](https://github.com/vastsa/PI-Desktop/blob/main/packages/plugin-sdk/src/index.test.ts).

User Interface Integration

The file scope configuration surface extends into the user experience layer. According to [packages/shared/src/changelog.ts](https://github.com/vastsa/PI-Desktop/blob/main/packages/shared/src/changelog.ts) (lines 453-454), the interface displays each plugin's declared file scope adjacent to its permission summary, providing transparency about extension capabilities.

Users adjust activation scopes through the Settings studio, toggling between "global" and "project-specific" modes. Despite UI variations, the underlying data model remains consistent, preserving a single source of truth for both activation and permission enforcement.

Summary

  • Activation scopes in activation.ts determine project-level execution rights using isActiveInProject and resolveScope
  • File scopes declared in manifest.json use glob patterns within the fs block to restrict read, write, and delete operations
  • Runtime enforcement in plugin-runtime.ts validates paths against declared scopes before executing IPC calls
  • Manifest validation requires non-empty scope arrays for requested permissions, rejecting inconsistent configurations
  • Legacy formats automatically migrate to the current scope model while maintaining security boundaries

Frequently Asked Questions

What happens if a plugin tries to access a file outside its declared scope?

The runtime throws an error indicating the path falls outside the declared file scope. This check occurs in the Electron main process after permission verification but before filesystem access, effectively sandboxing the plugin to its permitted directories.

How do I configure a plugin to work across all projects?

Set the activation scope to GLOBAL_SCOPE (the default) in the extension configuration. This eliminates project restrictions, allowing the plugin to activate regardless of the current workspace. You can verify this status using the isActiveInProject helper from [packages/shared/src/activation.ts](https://github.com/vastsa/PI-Desktop/blob/main/packages/shared/src/activation.ts).

Can a plugin have different read and write scopes?

Yes. The fs block in manifest.json supports independent scope arrays for each operation type. For example, you might grant read access to ["src/**/*.ts"] while restricting write access to ["src/generated/**"], ensuring the plugin can read source files but only modify specific output directories.

What is the difference between activation scope and file scope?

Activation scope controls where (in which projects) the extension loads and executes, managed through the ActivationScope type in activation.ts. File scope controls what files the extension may access within those projects, declared in the manifest's fs block and enforced at runtime during IPC calls.

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 →