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
- Remove `..` segments and symlinks-to-outside from the vault path referenced by the URL, or reference the real path inside the vault.
- Resolve the symlink into the vault (copy or move the target under the vault root) instead of linking out.
- Re-read `vault://` (e.g. `vault://_/`) to refresh the cached vault root/active path so it matches the on-disk vault location, then retry.
- 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
- Never build vault:// paths with `..` segments; normalize and strip them first
- Avoid symlinking files outside the vault into notes you will resolve via vault://
- Prefer URL-building helpers (encodeRelativePath-style) over string concatenation
- Refresh the cached vault root after moving the vault on disk
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
- Shared-folder destination escapes its configured root
- memory:// URL escapes memory root
- Absolute paths are not allowed in skill:// URLs
- Path traversal (..) is not allowed in skill:// URLs
- The managed-skills root is a symlink; refusing to operate ou
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/12e1d2e8c801788d.
Report an issue: GitHub.