ErrLookup › Background 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
- Dot-dot segments in the supplied path. The path contains '..', from leading chains like '../../etc/passwd' to interior segments like 'a/../b'. Lexical guards test the raw string before resolution, so even a semantically harmless interior segment is rejected.
- Absolute path where a relative one is required. A leading '/' or a Windows drive prefix ('C:\x') makes the joined path diverge from its base: openapi-generator prints an absoluteTarget that no longer shares a prefix with outDir, GitNexus's path.relative yields an absolute result, and mise rejects rooted cask tokens outright.
- Symlink inside the root pointing outside. The path looks contained until canonicalization resolves the link to a target beyond the root. Grav, zeroclaw, caveman, and CodeWhale reject this by design; ruflo applies the same rule to symlinked build inputs and worktree paths.
- Unvalidated external string becomes a path component. Record tags, form fields, spec-derived names, remote tree entries, tap metadata, and package names flow into path construction. fluentd's ${tag} in a path, openapi-generator filenames computed from schema or operation names, mise cask tokens and versions, and the deno package name field are all documented triggers.
- Environment changed under the guard. The containment base no longer matches reality: context7's skillPath prefix slicing mangles the relativePath after an upstream restructure, ruflo's registry breaks when the repository moves between phases, and a symlinked working directory makes resolve() disagree with the joined prefix.
- Full path sent where one segment was expected. Callers pass 'sounds/alert.mp3' or 'docs/readme' as a file name or a single path parameter. RocketChat rejects any name where path.basename(name) differs from the name; litellm's separator guard returns 'Invalid path parameter' for the same shape.
- Hostile or broken remote metadata. Crafted GitHub tree entries, tap cask JSON carrying '..' or NUL bytes in token or version, and absolute tarball entries in git dependencies. These are the cases the guard exists for; treat the source as hostile or broken rather than the guard as buggy.
What usually fixes it
- [object Object]
- [object Object]
- [object Object]
- [object Object]
- [object Object]
- [object Object]
Go deeper
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
Documented occurrences
- Skipping file with unsafe path: ${item.path} (upstash/context7)
- Backup location not allowed (outside site root): {$backup_root} (getgrav/grav)
- Target files must be generated within the output directory; absoluteTarget=%s outDir=%s (OpenAPITools/openapi-generator)
- editor::lang.filesystem.error_deleting_directory (octobercms/october)
- Invalid path name (Mintplex-Labs/anything-llm)
- Path parameter '{param_name}' cannot include '.' or '..' segments (BerriAI/litellm)
- localSingleEntryPageRank: sourceIndex ${src} out of range [0, ${n}) (ruvnet/ruflo)
- caveman-code: path escapes the workspace: ${candidate} (JuliusBrussee/caveman)
- Path parameter '{param_name}' must not contain path separators (BerriAI/litellm)
- brew-cask: invalid {kind} '{value}' (jdx/mise)
- Skill name "${skillName}" escapes the skills root (upstash/context7)
- Invalid key_alias (BerriAI/litellm)
- invalid agent runtime turn state (siyuan-note/siyuan)
- Slack outbound attachment path escapes workspace: {} (zeroclaw-labs/zeroclaw)
- refusing to write tarball with unsafe name derived from package: {} (denoland/deno)
- reviewed plugin MCP argument path escaped its staged root (Hmbown/CodeWhale)
- manifest exec binary escapes plugin directory: {} is not under {} (zeroclaw-labs/zeroclaw)
- registry path escapes owned worktree root: ${actual} (ruvnet/ruflo)
- Lark/Feishu marker target resolves outside workspace_dir (zeroclaw-labs/zeroclaw)
- error-invalid-file-name: error-invalid-file-name (RocketChat/Rocket.Chat)
…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.