SDK reference

vos.fs — Filesystem

Read, write, organise and watch files in the user's Home directory. Access is directory-scoped by the permissions your app declares, and re-validated by the host on every single call.

window.vos.fs9 methodsdeleteDir · rename · move · onChange · watch

The path model

Paths are Home-relative strings, always starting with home. There are no drive letters, no leading slash, and no .. segments.

PathMeaning
"home"The root of the user's Home directory.
"home/Documents"A folder inside Home.
"home/Documents/Journal"Nested folders — separate each level with a forward slash.
"home/.apps/<appId>"Your app's private sandbox. Read it from app.getInfo().sandboxPath rather than hard-coding it.

File-level methods take the directory and the file name as two separate arguments — readFile(dir, name) — while directory-level methods (readDir, deleteDir) take a single path. rename and move are the exception: they take two full paths including the file name.

Always-blocked paths

Three areas are reserved by the operating system and rejected even if they somehow appear in a grant: home/.system, home/.Trash, and the whole home/.apps tree — meaning you can reach your own sandbox but never another app's. Any path containing .. is rejected as path traversal.

Your private sandbox

Every app gets a private directory at home/.apps/<appId>/. It needs no user-facing directory approval: declaring "read": [] and "write": [] in your manifest grants the sandbox and nothing else. This is the right default for app data — settings, caches, saved documents your app owns.

javascript
const info = await window.vos.app.getInfo();
const dir = info.sandboxPath;            // "home/.apps/my-app"
await window.vos.fs.writeFile(dir, "settings.json", JSON.stringify(settings));

Least privilege passes review faster

Only request shared folders such as home/Documents when the user is expected to see and manage the files with other apps. Sandbox-only apps install with zero friction. See permissions.

Reading

fs.readDir

requires read
readDir(path: string): Promise<{ name: string; type: string }[]>

returns an array of directory entries

Lists the contents of a directory. type is "folder" for subdirectories, otherwise the file's type (e.g. "text"). Order is not guaranteed — sort it yourself if the order matters to your UI.

javascript
const entries = await window.vos.fs.readDir("home/Documents");
// → [ { name: "note.md", type: "text" },
//     { name: "Archive",  type: "folder" } ]

const folders = entries.filter((e) => e.type === "folder");
const files = entries.filter((e) => e.type !== "folder");

fs.readFile

requires read
readFile(path: string, name: string): Promise<string | null>

returns the file contents, or null when the file does not exist

Reads a file as text. A missing file resolves to null rather than rejecting, so treat null as “first run” and reserve catch for real errors such as a denied path.

javascript
const text = await window.vos.fs.readFile("home/Documents", "note.md");
if (text === null) {
  // The directory is readable but the file does not exist.
  // This is the normal first-run case — start with empty state.
}

Binary files

readFile returns text. To store binary data, encode it yourself — base64 in a .txt/.json wrapper or a data URI string — and decode on read.

Writing & deleting

fs.writeFile

requires write
writeFile(path: string, name: string, content: string): Promise<boolean>

returns true on success

Writes text to a file, overwriting it if it already exists — there is no append mode, so read-modify-write to append. Missing parent folders are created automatically.

javascript
// Overwrites if the file exists; creates missing parent folders.
await window.vos.fs.writeFile(
  "home/Documents/Journal",
  "2026-09-15.md",
  "# Today\n\nShipped the docs.",
);

fs.deleteFile

requires write
deleteFile(path: string, name: string): Promise<boolean>

returns true on success

Deletes a single file. Deleting a file that does not exist is not an error worth surfacing to users — catch and ignore.

javascript
await window.vos.fs.deleteFile("home/Documents", "draft.md");

fs.deleteDir

Newrequires write
deleteDir(path: string): Promise<boolean>

returns true on success

Recursively deletes a directory and everything inside it. The host refuses to delete the Home root or your sandbox root and rejects with Refusing to delete root or sandbox root.

javascript
// Recursive: removes the folder and everything inside it.
await window.vos.fs.deleteDir("home/Documents/Archive");

Destructive and irreversible

Deleted content does not go to the Trash. Confirm with the user before calling this on anything outside your own sandbox.

Renaming & moving

fs.rename / fs.move

Newrequires write on both paths
rename(fromPath: string, toPath: string): Promise<boolean>
move(fromPath: string, toPath: string): Promise<boolean>   // alias

returns true on success

Renames or moves a file. Both endpoints take a full path including the file name, and both must fall inside your write scope. Moves within the same volume are metadata-only, so even large files move instantly.

javascript
// Rename inside the same folder
await window.vos.fs.rename(
  "home/Documents/note.md",
  "home/Documents/note-final.md",
);

// Move to another folder (move is an alias of rename)
await window.vos.fs.move(
  "home/Documents/note-final.md",
  "home/Documents/Archive/note-final.md",
);

To rename a folder, move its files into a new folder one by one (walk it with readDir), then deleteDir the old one.

Watching for changes

VibeOS is a multi-app desktop: the user can edit, move or delete your files from the Files app or another app while your window is open. Subscribe to changes so your UI never shows stale content.

fs.watch

Newrequires read
watch(
  path: string,
  cb: (detail: { affectedPath?: string }) => void,
  opts?: { debounceMs?: number },
): () => void

returns an unsubscribe function

The recommended way to stay in sync. Only fires for changes inside path, and debounces bursts (default 300 ms) so a batch of writes triggers one callback.

javascript
// Only fires for changes inside the given path, debounced.
const stop = window.vos.fs.watch(
  "home/Documents/Journal",
  () => refreshList(),
  { debounceMs: 500 }, // default 300
);

fs.onChange

Newrequires read
onChange(cb: (detail: { affectedPath?: string }) => void): () => void

returns an unsubscribe function

The unfiltered stream: fires on every filesystem change the host observes, with the affected path when the host knows it. Use it when you care about the whole tree; prefer watch otherwise.

javascript
// Fires on every filesystem change the host observes.
const stop = window.vos.fs.onChange((detail) => {
  console.log("something changed:", detail.affectedPath);
  reloadCurrentFile();
});

// Later, when your view unmounts:
stop();

affectedPath can be absent

Some host operations report a change without naming a path. Treat a missing affectedPath as “something changed, re-read what I care about” rather than ignoring the event.

Ignoring your own writes

Your own writeFile also triggers a change event. Without a guard, an editor will reload the file it just saved and can fight the user's cursor. Set a short-lived flag around saves:

javascript
let selfWrite = false;

async function save(text) {
  selfWrite = true;
  try {
    await window.vos.fs.writeFile(dir, file, text);
  } finally {
    // Release after the change event has had time to arrive.
    setTimeout(() => { selfWrite = false; }, 600);
  }
}

window.vos.fs.watch(dir, () => {
  if (selfWrite) return; // ignore the echo of our own save
  reloadFromDisk();
});

Recipes

Persist app state in one JSON file

The pattern behind most VibeOS apps: load once on boot, debounce saves, never block the UI.

javascript
// A tiny typed store on top of a single JSON file.
async function load(dir, file, fallback) {
  try {
    const raw = await window.vos.fs.readFile(dir, file);
    return raw ? JSON.parse(raw) : fallback;
  } catch (err) {
    console.warn("load failed, using defaults:", err.message);
    return fallback;
  }
}

let saveTimer = null;
function save(dir, file, data) {
  clearTimeout(saveTimer);            // debounce rapid edits
  saveTimer = setTimeout(() => {
    window.vos.fs
      .writeFile(dir, file, JSON.stringify(data, null, 2))
      .catch((err) => setStatus("Save failed: " + err.message));
  }, 250);
}

Create a folder

javascript
// There is no mkdir(). Writing into a new path creates every
// missing parent folder, so drop a placeholder to materialise an
// otherwise-empty directory.
await window.vos.fs.writeFile("home/Documents/Archive", ".keep", "");

Walk a directory tree

javascript
// Recursively collect every file under a directory.
async function walk(path, out = []) {
  const entries = await window.vos.fs.readDir(path);
  for (const entry of entries) {
    if (entry.type === "folder") await walk(path + "/" + entry.name, out);
    else out.push({ dir: path, name: entry.name });
  }
  return out;
}

const all = await walk("home/Documents");

Copy a file

There is no copyFile — read and write. Use rename when you want a move rather than a copy.

javascript
async function copyFile(fromDir, name, toDir, newName) {
  const data = await window.vos.fs.readFile(fromDir, name);
  if (data === null) throw new Error("source file not found");
  await window.vos.fs.writeFile(toDir, newName || name, data);
}

// Duplicate a file in place: note.md → note copy.md
await copyFile("home/Documents", "note.md", "home/Documents", "note copy.md");

// Copy into another folder, keeping the same name
await copyFile("home/Documents", "note.md", "home/Documents/Archive");

// Copy your app's sandbox state as a one-click backup
const info = await window.vos.app.getInfo();
await copyFile(info.sandboxPath, "state.json", info.sandboxPath, "state.backup.json");

Failure modes

Filesystem rejections are plain Error objects; read err.message. The common ones:

MessageWhat went wrong
Access denied: app lacks read permissionYour manifest has no read key at all. Add it (even as []).
Access denied: app lacks write permissionSame for write.
Access denied: path is outside the app's allowed directoriesThe path is valid but not covered by the grant the user approved.
Access denied: path must be inside Home directoryThe path did not start with home.
Access denied: path is reservedYou touched .system, .Trash or another app's sandbox.
Access denied: path traversal is not allowedThe path contained .. or . segments.
Refusing to delete root or sandbox rootdeleteDir was called on Home or your sandbox root.
  • Never surface a raw access-denied message to users — explain what the app cannot do and, if relevant, that permissions are managed in Web Apps → Permissions.
  • Every call also has a 30-second timeout; see errors & limits.