Skip to main content

File System API

The File System API lets extensions read, write, create, delete, rename, move, copy, and list files within the editor. All operations go through the editor.workspace.fs namespace and work transparently with both local file paths (file://) and Android SAF URIs (content://).

Overview

MethodDescription
workspace.fs.readRead file content as a string
workspace.fs.writeWrite string content to a file
workspace.fs.createCreate a new file in a directory
workspace.fs.deleteDelete a file or directory
workspace.fs.renameRename a file or directory
workspace.fs.moveMove a file or directory to a different location
workspace.fs.copyCopy a file to a different location
workspace.fs.listList directory contents with optional filters
workspace.fs.existsCheck if a file or directory exists
workspace.fs.createDirectoryCreate a new directory
workspace.fs.zipCreate a ZIP archive of a folder
workspace.fs.shareShare a file using the system share sheet
workspace.fs.openExternalOpen a file with the device's default app
workspace.openFileOpen a file in an editor tab
workspace.getActiveFileGet the currently active file/tab

RPC Methods

workspace.fs.read

Read the contents of a file as a UTF-8 string.

SDK signature:

editor.workspace.fs.read(uri);

Parameters:

NameTypeRequiredDescription
uristringYesFile URI (local file:// path or SAF content:// URI)

Returns: Promise<string> -- The file content as a string.

RPC method name: workspace.fs.read

RPC params: { uri: string }

Example:

const content = await editor.workspace.fs.read(fileUri);
console.log("File content:", content);

workspace.fs.write

Write string content to a file. If the file exists, its content is overwritten. If it does not exist yet, it is created.

SDK signature:

editor.workspace.fs.write(uri, content);

Parameters:

NameTypeRequiredDescription
uristringYesFile URI to write to
contentstringYesNew file content

Returns: Promise<void>

RPC method name: workspace.fs.write

RPC params: { uri: string, content: string }

Example:

await editor.workspace.fs.write(fileUri, "Hello, world!\n");

workspace.fs.create

Create a new empty file inside a directory. The file name must not contain path separators or parent traversal sequences.

SDK signature:

editor.workspace.fs.create(parentUri, name);

Parameters:

NameTypeRequiredDescription
parentUristringYesURI of the parent directory
namestringYesName for the new file (e.g. "style.css")

Returns: Promise<string> -- The URI of the newly created file.

RPC method name: workspace.fs.create

RPC params: { parentUri: string, name: string }

Validation rules for name:

  • Must not contain ..
  • Must not contain / or \

Example:

const newUri = await editor.workspace.fs.create(folderUri, "index.html");
console.log("Created:", newUri);

// Write initial content into the new file
await editor.workspace.fs.write(newUri, "<!DOCTYPE html>\n<html></html>");

workspace.fs.delete

Delete a file or directory.

SDK signature:

editor.workspace.fs.delete(uri);

Parameters:

NameTypeRequiredDescription
uristringYesURI of the file or directory to delete

Returns: Promise<void>

RPC method name: workspace.fs.delete

RPC params: { uri: string }

Example:

const confirmed = await editor.window.showConfirm({
title: "Delete File",
message: "Are you sure you want to delete this file?",
});

if (confirmed) {
await editor.workspace.fs.delete(fileUri);
editor.window.showToast("File deleted");
}

workspace.fs.rename

Rename a file or directory. The new name must be a simple file name without path separators.

SDK signature:

editor.workspace.fs.rename(uri, newName);

Parameters:

NameTypeRequiredDescription
uristringYesURI of the file or directory to rename
newNamestringYesNew name (e.g. "app.js")

Returns: Promise<string> -- The URI of the renamed file.

RPC method name: workspace.fs.rename

RPC params: { uri: string, newName: string }

Validation rules for newName:

  • Must not contain ..
  • Must not contain / or \

Example:

const newUri = await editor.workspace.fs.rename(fileUri, "main.js");
console.log("Renamed to:", newUri);

workspace.fs.move

Move a file or directory to a different target directory.

SDK signature:

editor.workspace.fs.move(uri, destinationUri);

Parameters:

NameTypeRequiredDescription
uristringYesURI of the file or directory to move
destinationUristringYesURI of the target directory

Returns: Promise<string> -- The URI of the file at its new location.

RPC method name: workspace.fs.move

RPC params: { uri: string, destinationUri: string }

Permission note: Both the source and destination paths are checked independently. If either is outside the current project, the fileSystem permission is required for that path.

Example:

const newUri = await editor.workspace.fs.move(
"/project/src/old/utils.js",
"/project/src/lib/"
);
console.log("Moved to:", newUri);

workspace.fs.copy

Copy a file to a target directory. The copy retains the original file name.

SDK signature:

editor.workspace.fs.copy(uri, destinationUri);

Parameters:

NameTypeRequiredDescription
uristringYesURI of the file to copy
destinationUristringYesURI of the target directory

Returns: Promise<string> -- The URI of the copied file (constructed as destinationUri + "/" + originalFileName).

RPC method name: workspace.fs.copy

RPC params: { uri: string, destinationUri: string }

Permission note: Both the source and destination paths are checked independently, same as move.

Example:

const copiedUri = await editor.workspace.fs.copy(
"/project/config/base.json",
"/project/config/env/"
);
console.log("Copied to:", copiedUri);

workspace.fs.list

List the contents of a directory. Supports optional filtering by MIME type, file extension, name substring, and recursive traversal.

SDK signature:

editor.workspace.fs.list(uri, options?)

Parameters:

NameTypeRequiredDescription
uristringYesURI of the directory to list
optionsobjectNoFilter and traversal options (see below)

Options object:

NameTypeDefaultDescription
mimeTypesstring[][]Only include files matching these MIME types (e.g. ["text/javascript"])
extensionsstring[][]Only include files with these extensions (e.g. [".js", ".ts"])
nameContainsstringnullOnly include entries whose name contains this substring
excludeDirsstring[][]Directory names to exclude from results (e.g. ["node_modules", ".git"])
recursivebooleanfalseIf true, list all entries recursively through subdirectories

Returns: Promise<Array<FileEntry>> where each FileEntry has:

PropertyTypeDescription
uristringURI of the file or directory
namestringDisplay name
isDirectorybooleantrue if the entry is a directory
sizenumberFile size in bytes (0 for directories)

RPC method name: workspace.fs.list

RPC params: { uri: string, mimeTypes?: string[], extensions?: string[], nameContains?: string, excludeDirs?: string[], recursive?: boolean }

Example -- basic listing:

const entries = await editor.workspace.fs.list(folderUri);
entries.forEach((entry) => {
const type = entry.isDirectory ? "DIR " : "FILE";
console.log(`${type} ${entry.name} (${entry.size} bytes)`);
});

Example -- filtered and recursive:

const jsFiles = await editor.workspace.fs.list(projectUri, {
extensions: [".js", ".mjs"],
excludeDirs: ["node_modules", ".git", "dist"],
recursive: true,
});

console.log(`Found ${jsFiles.length} JavaScript files`);

workspace.fs.exists

Check whether a file or directory exists at the given URI.

SDK signature:

editor.workspace.fs.exists(uri);

Parameters:

NameTypeRequiredDescription
uristringYesFile or directory URI to check

Returns: Promise<boolean> -- true if the file or directory exists, false otherwise.

RPC method name: workspace.fs.exists

RPC params: { uri: string }

Example:

const configExists = await editor.workspace.fs.exists(configUri);
if (!configExists) {
// Create default config
const uri = await editor.workspace.fs.create(projectRoot, "config.json");
await editor.workspace.fs.write(uri, JSON.stringify({ version: 1 }));
}

workspace.fs.createDirectory

Create a new empty directory inside a parent directory.

SDK signature:

editor.workspace.fs.createDirectory(parentUri, name);

Parameters:

NameTypeRequiredDescription
parentUristringYesURI of the parent directory
namestringYesName for the new directory (e.g. "src")

Returns: Promise<string> -- The URI of the newly created directory.

RPC method name: workspace.fs.createDirectory

RPC params: { parentUri: string, name: string }

Validation rules for name:

  • Must not contain ..
  • Must not contain / or \

Example:

const srcUri = await editor.workspace.fs.createDirectory(projectRoot, "src");
const libUri = await editor.workspace.fs.createDirectory(srcUri, "lib");

// Create a file inside the new directory
const fileUri = await editor.workspace.fs.create(libUri, "index.js");
await editor.workspace.fs.write(fileUri, 'export default {};');

workspace.fs.zip

Create a ZIP archive from a folder. The ZIP is saved as a new file.

SDK signature:

editor.workspace.fs.zip(uri, options?)

Parameters:

NameTypeRequiredDescription
uristringYesURI of the folder to zip
optionsobjectNoZIP options (see below)

Options object:

NameTypeDefaultDescription
destinationUristringParent of folderURI of the directory where the ZIP file will be saved
namestring<folderName>.zipName for the ZIP file
excludeDirsstring[][]Directory names to skip (e.g. ["node_modules", ".git"])

Returns: Promise<string> -- The URI of the created ZIP file.

RPC method name: workspace.fs.zip

RPC params: { uri: string, destinationUri?: string, name?: string, excludeDirs?: string[] }

Example:

// Zip the current project's src folder
const zipUri = await editor.workspace.fs.zip(srcFolderUri, {
excludeDirs: ["test"],
name: "source-backup.zip",
});
editor.window.showToast("ZIP created: " + zipUri);

workspace.fs.share

Share a file using the device's system share sheet. Only files are supported (not directories).

SDK signature:

editor.workspace.fs.share(uri, options?)

Parameters:

NameTypeRequiredDescription
uristringYesURI of the file to share
optionsobjectNoShare options (see below)

Options object:

NameTypeDefaultDescription
titlestringnullTitle for the share dialog (shown on Android Intent chooser; may not appear on all devices)

Returns: Promise<boolean> -- true if the file was shared successfully.

RPC method name: workspace.fs.share

RPC params: { uri: string, title?: string }

Example:

const shared = await editor.workspace.fs.share(fileUri, {
title: "Share source file",
});
if (shared) {
editor.window.showToast("File shared!");
}

workspace.fs.openExternal

Open a file with the device's default application (e.g. an image viewer for .png, a PDF reader for .pdf). Only files are supported (not directories).

SDK signature:

editor.workspace.fs.openExternal(uri, options?)

Parameters:

NameTypeRequiredDescription
uristringYesURI of the file to open
optionsobjectNoOpen options (see below)

Options object:

NameTypeDefaultDescription
titlestringnullTitle for the app chooser dialog (shown on Android Intent chooser; may not appear on all devices)

Returns: Promise<boolean> -- true if the file was opened successfully.

RPC method name: workspace.fs.openExternal

RPC params: { uri: string, title?: string }

Example:

const opened = await editor.workspace.fs.openExternal(imageUri);
if (!opened) {
editor.window.showToast("No app found to open this file");
}

workspace.openFile

Open a file in the editor. If the file is already open in a tab, the editor switches to that tab. If the editor screen is not active, the app navigates to it.

SDK signature:

editor.workspace.openFile(uri);

Parameters:

NameTypeRequiredDescription
uristringYesURI of the file to open

Returns: Promise<void>

RPC method name: workspace.openFile

RPC params: { uri: string }

Example:

// Create a file and open it in the editor
const newUri = await editor.workspace.fs.create(srcFolder, "app.js");
await editor.workspace.fs.write(newUri, 'console.log("Hello!");');
await editor.workspace.openFile(newUri);

workspace.getActiveFile

Get information about the currently active tab.

SDK signature:

editor.workspace.getActiveFile();

Parameters: None.

Returns: Promise<ActiveFile | null> -- Returns null if no tab is active.

PropertyTypeDescription
keystringUnique tab key
namestringDisplay name of the tab
uristring?File URI (present only for file-type tabs)
typestring?Tab type: "file", "terminal", "webview", "info", or "untracked"

RPC method name: workspace.getActiveFile

Example:

const active = await editor.workspace.getActiveFile();
if (active && active.uri) {
const content = await editor.workspace.fs.read(active.uri);
console.log(`${active.name} has ${content.length} characters`);
}

workspace.getEngineVersion

Get the current extension engine version string.

SDK signature:

editor.workspace.getEngineVersion();

Parameters: None.

Returns: Promise<string> -- The engine version string (e.g. "0.1.0").

RPC method name: workspace.getEngineVersion

Example:

const version = await editor.workspace.getEngineVersion();
console.log("Engine version:", version);

workspace.getAppVersion

Get the app version string.

SDK signature:

editor.workspace.getAppVersion();

Parameters: None.

Returns: Promise<string> -- The app version string (e.g. "1.2.3").

RPC method name: workspace.getAppVersion


workspace.project.create

Create a new project with optional initial files. Requires the projectCreate permission in your manifest.

SDK signature:

editor.workspace.project.create(options);

Parameters:

NameTypeRequiredDescription
namestringYesProject name.
parentUristringNoURI of the parent directory where the project will be created.
filesArray<{ path: string, content: string }>NoInitial files to create inside the project.

Returns: Promise<{ id: string, name: string, uri: string }> -- The created project's ID, name, and URI.

RPC method name: workspace.project.create

Example:

const project = await editor.workspace.project.create({
name: "my-app",
files: [
{ path: "index.html", content: "<!DOCTYPE html>\n<html></html>" },
{ path: "style.css", content: "body { margin: 0; }" },
],
});

await editor.window.showToast(`Created project: ${project.name}`);

Permission Model

In-project access (no permission required)

Files and directories located within the currently open project can be accessed freely. The editor determines whether a path is within the project by normalizing both the file path and the project root, resolving symlinks, and checking that the file path starts with the project root prefix.

Out-of-project access (requires fileSystem permission)

To access files outside the current project directory, the extension must declare the fileSystem permission in its manifest.json:

{
"id": "my-extension",
"name": "My Extension",
"version": "1.0.0",
"permissions": ["fileSystem"]
}

When an extension attempts to access a path outside the project, the editor checks for an existing permission grant. If no grant exists, the user is prompted with a permission dialog.

Grant scopes

Users can choose from three grant scopes when approving access:

ScopeDurationStorage
One-timeSingle operationNot stored
SessionUntil app restartIn-memory only
PermanentSurvives restartsPersisted to database

Granular directory grants

Instead of granting blanket file system access, users can restrict approval to a specific directory. The grant then covers that directory and all of its descendants. For example, granting access to /home/user/projects/ allows the extension to access any path under that directory.

Methods that check both source and destination

workspace.fs.move and workspace.fs.copy validate permissions on both the source URI and the target URI independently. If the source is inside the project but the target is outside, the extension needs a grant for the target path (and vice versa).


Security Considerations

Path traversal prevention

All file URIs that resolve to local file paths are normalized. If the normalized path differs from the original, the request is rejected with a Path traversal not allowed error. This prevents attacks using .. segments to escape allowed directories.

For create and rename, the file name parameter is additionally validated to reject names containing .., /, or \.

SAF URI handling

Android Storage Access Framework (SAF) URIs (content://) bypass the local path security checks because they are governed by Android's own permission model. SAF URIs are always considered "within project" for the purpose of the permission gate.

RPC timeout

File system operations have a 90-second RPC timeout (longer than the default 30-second timeout for most RPC calls) to accommodate permission dialogs that require user interaction.

Rate limiting

Each extension is limited to 50 concurrent RPC calls. If an extension exceeds this limit, subsequent calls are rejected until in-flight calls complete.

Error responses

When a permission is denied (either because the extension lacks the fileSystem permission in its manifest, or the user declines the prompt), the RPC response contains the error string PERMISSION_DENIED: fileSystem. Extensions should handle this gracefully:

try {
const content = await editor.workspace.fs.read(externalPath);
} catch (err) {
if (err.message && err.message.includes("PERMISSION_DENIED")) {
editor.window.showToast("File access was denied by the user");
} else {
editor.window.showToast("Failed to read file: " + err.message);
}
}

File Events

The editor emits events when files are created, deleted, renamed, or moved. These events fire regardless of whether the change was made by your extension, another extension, or the user directly.

editor.events.onFileCreated((data) => {
console.log("Created:", data.name, data.uri, "in", data.parentUri);
});

editor.events.onFileDeleted((data) => {
console.log("Deleted:", data.name, data.uri);
});

editor.events.onFileRenamed((data) => {
console.log(
"Renamed:",
data.oldUri,
"->",
data.newUri,
"new name:",
data.newName
);
});

editor.events.onFileMoved((data) => {
console.log(
"Moved:",
data.oldUri,
"->",
data.newUri,
"target dir:",
data.targetUri
);
});

editor.events.onFileSave((data) => {
console.log("Saved:", data.name, data.uri);
});

All event listeners return an unsubscribe function:

const unsub = editor.events.onFileSave((data) => {
console.log("Saved:", data.name);
});

// Later, when no longer needed:
unsub();

Complete Example

The following example demonstrates a command that finds all TODO comments across JavaScript files in the current project and writes a summary report.

import editor from "@srcnexus/ext-sdk";

editor.onLoad(() => {
editor.commands.registerCommand(
"todo-finder.scan",
async () => {
// Get the project root directory
const projectRoot = await editor.workspace.getProjectRoot();
if (!projectRoot) {
editor.window.showToast("No project is open");
return;
}

// Recursively find all .js files, excluding node_modules
const jsFiles = await editor.workspace.fs.list(projectRoot, {
extensions: [".js"],
excludeDirs: ["node_modules", ".git", "dist"],
recursive: true,
});

const todos = [];

for (const file of jsFiles) {
if (file.isDirectory) continue;
const content = await editor.workspace.fs.read(file.uri);
const lines = content.split("\n");
lines.forEach((line, index) => {
if (line.includes("TODO")) {
todos.push({
file: file.name,
line: index + 1,
text: line.trim(),
});
}
});
}

// Build a report
let report = `# TODO Report\n\nFound ${todos.length} TODOs:\n\n`;
todos.forEach((todo) => {
report += `- **${todo.file}:${todo.line}** ${todo.text}\n`;
});

// Write the report to a file in the project root
const reportUri = await editor.workspace.fs.create(
projectRoot,
"TODO-REPORT.md"
);
await editor.workspace.fs.write(reportUri, report);
await editor.workspace.openFile(reportUri);

editor.window.showToast(`Found ${todos.length} TODOs`);
},
{ description: "Scan project for TODO comments" }
);
});