Deterministic path identity across runtimes. Root-bound filesystem access where real I/O begins.
English | Language coverage | Targets | Verification | Security
Path+FS = authoritative root-bound filesystem boundary; Path+Client = lexical identity plus daemon/client helpers; Transport = daemon wrapper only; Path only = lexical identity only.
Every runtime has a path library. CanonicalPath is for the moment where a path leaves one runtime and enters another.
A Unity editor tool may pass a path to a TypeScript MCP gateway, a Go daemon, a PowerShell script, a browser UI, WSL, Windows, and back again. Each layer can interpret .., slashes, drive-relative paths, UNC paths, symlinks, device names, and missing files differently. That is how path bugs become security bugs, broken artifacts, and hard-to-debug agent behavior.
The key idea:
CanonicalPathsays what path identity the tool means.CanonicalFSdecides whether real filesystem I/O may touch that path under a root.- Non-Go runtimes share lexical identity and delegate security-sensitive I/O to the Go daemon.
| Layer | Purpose | Filesystem access | Security claim |
|---|---|---|---|
CanonicalPath |
Deterministic lexical identity, comparison, serialization, aliases, and shared test vectors. | No | Stable identity contract, not an I/O boundary. |
CanonicalFS |
Root-bound filesystem access for real reads/writes. | Yes | Go implementation is the authoritative filesystem security boundary. |
| Go daemon | Capability-style HTTP/RPC boundary for non-Go clients. | Yes | Other languages delegate security-sensitive I/O to this daemon. |
| Client/runtime ports | Lexical helpers, wrappers, transports, Unity bridge adapters. | Usually no | Lexical/client-only unless explicitly documented otherwise. |
The split is intentional: path identity is not file access.
Use standard libraries for ordinary local path manipulation. Use CanonicalPath when a path must cross process, language, operating-system, editor, or agent boundaries without changing meaning.
CanonicalPath is designed for:
- agent tools and MCP servers that must validate scoped paths before touching a project;
- build systems and generators that reference files before they exist;
- Unity/editor pipelines that need scoped paths such as
Assets/...,Packages/..., artifacts, caches, and temp sessions; - Windows/WSL/macOS/Linux workflows where host paths and project-relative paths are not the same thing;
- archive extraction and file tools where untrusted filenames must not escape a root;
- shared test vectors so Go, TypeScript, JavaScript, Python, Dart, C#, Swift, Kotlin, C, Rust, C++, Haxe, GDScript, PowerShell, Unity, and wrappers stay aligned.
| Group | Current surface | Filesystem security boundary |
|---|---|---|
| Go | canonicalpath, authoritative canonicalfs, daemon |
Yes: Go CanonicalFS / daemon |
| TypeScript | canonicalpath, best-effort canonicalfs helpers, RPC/HTTP clients |
Delegates to Go daemon for adversarial I/O |
| JavaScript standalone/browser | lexical canonicalpath |
No filesystem access |
| PowerShell 5.1 / 7 | lexical module, typed HTTP client helpers, daemon transport | Delegates to Go daemon |
| Bash / Windows CMD-BAT | daemon transport wrappers | Delegates to Go daemon |
| Unity C# | managed lexical helpers, PathGuard/scoped validation, daemon transport, Burst-oriented surface | Delegates to Go daemon |
| Python, Dart/Flutter, C#/.NET, Swift, Kotlin, C, Rust, C++, Haxe, GDScript/Godot | vector-checked lexical canonicalpath surfaces |
Lexical/client-only until native root-bound FS or daemon transport is reviewed |
See docs/language-coverage.md and docs/language-targets.md for the detailed matrix.
For security-sensitive filesystem operations, do not build paths with string concatenation, Join, normalize, or realpath and then open them later.
Use the root-bound filesystem layer:
- In Go, use
CanonicalFSdirectly. - In TypeScript, Unity, C#, Python, PowerShell, Bash, CMD/BAT, or other runtimes, validate and serialize with the local CanonicalPath surface, then delegate real I/O to the Go daemon unless that runtime has a separately reviewed root-bound implementation.
- Path traversal through
.., absolute paths, encoded separators, or mixed separators. - Windows-specific surprises: drive-relative paths like
C:foo, UNC paths, reserved device names, alternate data streams, trailing dots/spaces, and NUL bytes. - Symlink and reparse-point escape from a trusted project root.
- Time-of-check/time-of-use bugs from validating a path string and opening it later.
- Zip Slip style archive writes outside the destination directory.
- Agent/MCP tools writing into the wrong project, cache, artifact, package, or temp root.
- Broken identity when one runtime stores a path and another runtime interprets it differently.
- Canonical repository:
https://github.com/romanilyin/canonicalpath. - TypeScript/npm package:
@romanilyin/canonicalpath. - JavaScript standalone npm package:
@romanilyin/canonicalpath-standalone. - Go module:
github.com/romanilyin/canonicalpath/packages/go. - Unity UPM package:
com.romanilyin.canonicalpath. - Unity npmjs scoped-registry release candidate:
com.romanilyin.canonicalpath@2026.6.19-1. - Release version:
2026.6.19-1. - License model:
- Repository default:
MIT. @romanilyin/canonicalpath:MIT.@romanilyin/canonicalpath-standalone:MIT.- Go
canonicalpath/canonicalfs:MITunless a package directory says otherwise. - Unity UPM package
com.romanilyin.canonicalpath:LicenseRef-Stinger-Royalty-Free-EULA-1.0.
- Repository default:
The earlier full source release plan is tracked in docs/release-2026.5.18-2.md; Unity registry publication history is tracked in docs/release-unity-2026.6.14-1.md. The Go module tag is packages/go/v0.2026.5-18.2; see docs/release-process.md for the release tag policy.
- Planned language targets and allocation-check gates are tracked in
spec/language-targets.jsonand summarized indocs/language-targets.md. - Planned package directories are skeleton/not implemented placeholders unless listed as supported or as an early bridge target.
- Go
canonicalfs.Renameis intentionally unsupported on Go 1.24 becauseos.Rootdoes not expose a root-bound rename method there. Do not replace it withfilepath.Join(root, rel)plusos.Rename.
After installing dependencies, run:
pnpm install
pnpm verifyFor Go race-sensitive filesystem tests, also run:
pnpm go:raceFor active allocation and memory smoke gates, run:
pnpm allocIf the pnpm shim is not available, use corepack pnpm for the same commands. Detailed verification commands live in docs/verification.md.
CanonicalPath is not meant to replace normal path utilities for simple, trusted, single-language applications.
You probably do not need it if:
- paths never cross a language/process/OS boundary;
- filenames are fully trusted and intentionally allowed to point anywhere;
- you only need local display formatting;
- your OS sandbox/container already handles the security boundary and you do not need cross-runtime identity.
This is not an attempt to invent a universal filesystem standard. It is a practical contract for codebases where path strings are already crossing too many boundaries.
Image: xkcd “Standards”. The point is taken seriously: CanonicalPath should be used as a narrow contract at tool boundaries, not as a reason to rewrite every path helper in every program.
This repository uses package-level licensing.
The repository default and the non-Unity CanonicalPath / CanonicalFS packages are
licensed under the MIT License. See LICENSE.md and LICENSES/MIT.txt.
The Unity UPM/npm package com.romanilyin.canonicalpath under packages/unity
is licensed separately under Stinger Royalty-Free EULA 1.0. See
packages/unity/LICENSE.md and packages/unity/LICENSE.ru.md.
The MIT License grants rights to use the code. It does not grant trademark rights to use the CanonicalPath name, logo, or branding to imply official endorsement.
