can1357/oh-my-pi · error

vault:// URL escapes vault root

Error message

vault:// URL escapes vault root

What it means

The vault:// protocol handler throws this when a resolved or canonicalized filesystem path is not the vault root itself and does not sit under it (prefix `root + path.sep`). It is a path-traversal containment check: `ensureWithinRoot` runs after `path.resolve` and after `fs.realpathSync`, so symlinked or `..`-laden targets that land outside the vault are rejected. It guards both file reads/directory listings and the ancestor-walk used to canonicalize not-yet-existing paths.

Source

Thrown at packages/coding-agent/src/internal-urls/vault-protocol.ts:126

let cachedVaultDirectory: Map<string, string> | undefined;
let cachedActiveVaultPath: string | undefined;
const cachedVaultInfo = new Map<string, string>();

function toVaultValidationError(error: unknown): Error {
	const message = error instanceof Error ? error.message : String(error);
	return new Error(message.replace("skill://", "vault://"));
}

function getContentType(filePath: string): ContentType {
	if (isMarkdownPath(filePath)) return "text/markdown";
	const ext = path.extname(filePath).toLowerCase();
	if (ext === ".json") return "application/json";
	return "text/plain";
}

function ensureWithinRoot(targetPath: string, rootPath: string): void {
	if (targetPath !== rootPath && !targetPath.startsWith(`${rootPath}${path.sep}`)) {
		throw new Error("vault:// URL escapes vault root");
	}
}

function encodePathComponent(component: string): string {
	return encodeURIComponent(component).replaceAll("%2F", "/");
}

function encodeRelativePath(relativePath: string): string {
	return relativePath
		.split("/")
		.filter(segment => segment.length > 0)
		.map(encodeURIComponent)
		.join("/");
}

function decodeVaultPath(url: InternalUrl): {
	rawPathname: string;
	relativePath: string;

View on GitHub (pinned to 9690622007)

Solutions

  1. Remove `..` segments and symlinks-to-outside from the vault path referenced by the URL, or reference the real path inside the vault.
  2. Resolve the symlink into the vault (copy or move the target under the vault root) instead of linking out.
  3. Re-read `vault://` (e.g. `vault://_/`) to refresh the cached vault root/active path so it matches the on-disk vault location, then retry.
  4. Verify the vault root itself is a real directory (not a symlink chain) if the error occurs on every URL.

Example fix

// before
const url = "vault://_/../secrets/api-keys.md";
await handler.resolve(parseInternalUrl(url)); // throws: escapes vault root
// after
const url = "vault://_/notes/api-keys.md"; // keep target inside the vault
await handler.resolve(parseInternalUrl(url));
Defensive patterns

Strategy: validation

Validate before calling

import * as path from "node:path";
function staysInsideVault(relativePath: string, vaultRoot: string): boolean {
  const abs = path.resolve(vaultRoot, relativePath);
  return abs === vaultRoot || abs.startsWith(vaultRoot + path.sep);
}
// call before resolving: staysInsideVault("notes/a.md", vaultRoot)

Type guard

function isWithinRoot(target: string, root: string): boolean {
  return target === root || target.startsWith(`${root}${path.sep}`);
}

Try / catch

try {
  const p = resolveVaultUrlToPath(url);
} catch (err) {
  if (err instanceof Error && err.message === "vault:// URL escapes vault root") {
    // reject the link or re-anchor it inside the vault; do not retry as-is
  }
  throw err;
}

Prevention

When it happens

Trigger: resolveVaultUrlToPath, #readFile, #listDir, #resolveFsTarget, findExistingAncestor(Sync) are called with a vault:// URL whose decoded relativePath contains `..` segments surviving validation, or whose target is a symlink pointing outside the vault root, or whose cached vault root resolves to a different real path than the target's real ancestor (e.g. vault root itself is a symlink and realpathSync of the target escapes it).

Common situations: Constructing URLs by hand with `..` segments; note-taking setups where a note inside the vault symlinks to a folder outside (common for dotfiles or shared asset dirs); moving/renaming the vault so the cached root realpath no longer contains the target's realpath.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/12e1d2e8c801788d. Report an issue: GitHub.