ErrLookupBackground articles › Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory

Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory

Path traversal blocked — the "path escapes the workspace", "outside site root", and "path traversal denied" errors a library raises when a path it was asked to use resolves outside the directory the library is confined to. A developer meets this family when a supplied path contains '..' segments, an absolute path where a relative one is required, or a symlink whose target sits outside the root, and it surfaces across file APIs, backup tools, code generators, plugin loaders, and package managers. The guard is deliberate security behavior: fix the path or its source, never the guard.

Distilled from 84 documented records across 26 repositories.

Background

These errors do not come from the filesystem; they come from a containment guard inside the library that runs before any file is opened, and the records show two dominant implementations. Lexical guards inspect the raw string before any join: fluentd matches a ../ or leading-slash pattern against buffer tags, mise requires brew-cask tokens and versions to be single normal path components with no NUL bytes, litellm parses MCP path parameters with PurePosixPath and refuses '.' and '..' segments, and ruflo's memory-path guard rejects any '..' followed by a slash anywhere in the string, before resolve() ever runs. Canonical guards resolve or realpath both the candidate and the root first, then require the result to sit inside the root under a separator-aware prefix comparison: Grav checks the backup root against GRAV_ROOT after realpath(), GitNexus computes path.relative(repoRoot, fullPath) and denies results that start with '..' or are absolute, zeroclaw canonicalizes marker targets and plugin binaries before comparing prefixes, and openapi-generator requires the resolved target to start with the intended output directory. Several libraries stack both, with the resolve-and-contain check documented as defense-in-depth that should be unreachable through public input (context7's skill-name guard, ruflo's session-path check).

The guard exists because the string is untrusted and is about to become a filesystem path used for read, write, delete, or execute. The untrusted sources in the records are varied: GitHub tree API responses fetched during skill install (context7), plugin manifests and agent-generated file markers (zeroclaw, CodeWhale), third-party tap and cask metadata (mise), the name field of deno.json (deno), tarball and git-tree entries (pnpm), fluentd record tags interpolated into output paths, filenames computed from OpenAPI spec names (openapi-generator), and plain form fields (anything-llm). Some records tie the check to a specific vulnerability fix: Grav's containment closed GHSA-fch7-cpv4-w7hg, whose deny-list predecessor let non-blocklisted external directories such as /opt and /mnt be archived, and siyuan describes its plugin-storage check as sandbox-escape prevention. Context7 notes that Git itself forbids '..' in tree paths, so traversal-shaped entries in fetched data usually point to a tampered or mangled API response rather than a normal repository.

From the caller's side the same underlying rejection wears very different clothes, which is why a single error page is not enough for this family. It can be a console.warn and a skipped file (context7's downloader), a RuntimeException (Grav), an HTTP 403 (GitNexus), an HTTP 500 that is really a client-input problem (anything-llm converts a failed folder-name check into 500 'Failed to create folder'), a ValueError surfaced as a failed tool-result string on generated MCP tools (litellm), a refused plugin manifest or registry record (zeroclaw, ruflo), or an UnrecoverableError at flush time that sends the buffered chunk to secondary storage or discards it (fluentd). Side effects differ too: octobercms completes a copy and then refuses to delete the source, leaving content in both places, while pnpm and deno abort the whole operation.

One axis where the libraries genuinely disagree is whether symlinks count as an escape. A lexical prefix check passes a symlink that lives inside the root but points elsewhere; a canonicalizing guard catches it because realpath resolves the link first, a contrast caveman's record states explicitly. Grav, zeroclaw, caveman, and CodeWhale reject symlink escapes by design, while the same canonicalization produces environment-shaped false positives in ruflo when the working directory is itself a symlink (macOS /tmp to /private/tmp) or the repository moved between run phases so resolved paths no longer sit under the recorded base. Input shapes also vary by platform and encoding: drive prefixes and backslashes are rejected explicitly by several guards (mise, anything-llm, and litellm normalizes backslashes to slashes), GitNexus catches percent-encoded traversal because express decodes it before the check, and ruflo's GCS guard rejects shell metacharacters as well, since the object path is interpolated into a gcloud argument.

Common causes

What usually fixes it

Go deeper

Documented occurrences

…and 64 more across the corpus — use search.

Honest provenance: generated on 2026-08-24 from AI-assisted analysis of the linked records. See how records are made.