Zero-dependency, pure-JS sanitisation for file names and paths, optimised for performance. Works in
Node, Bun, Deno and the browser. No node:path, no buffers, no runtime deps.
British spelling exports (sanitise*) with US aliases (sanitize*).
bun add sanitise-pathimport {
sanitiseFilename, // sanitizeFilename
sanitisePath, // sanitizePath
sanitise,
truncateUtf8Bytes,
} from "sanitise-path";Sanitises a single file name (not a path). Returns string.
sanitiseFilename('hello?.txt') // 'hello.txt' (illegal chars stripped)
sanitiseFilename('con.txt') // '' (reserved device name)
sanitiseFilename('myfile. ') // 'myfile' (trailing dot/space trimmed)Sanitises a file path as path text. It does not resolve, normalise, or canonicalise paths. Pure-JS, deterministic POSIX-style output on every OS.
sanitisePath('../etc/passwd') // 'etc/passwd' (`..` segment dropped)
sanitisePath('a/../b') // 'a/b' (no resolution)
sanitisePath('a/../b', { resolve: true }) // 'b' (opt-in resolution)
sanitisePath('%2e%2e%2fetc') // '%2e%2e%2fetc' (percent stays literal)
sanitisePath('/var/app/..') // 'var/app' (trailing '..' dropped)
sanitisePath("/path/$dir!/file|name.txt") // 'path/$dir!/filename.txt'Convenience wrapper — one call for both. Passes the same opts to each.
sanitise("dir/../up", "report?.pdf")
// => { path: "dir/up", name: "report.pdf" }Truncates to at most maxBytes UTF-8 bytes without splitting a surrogate pair.
Returns the input unchanged when it already fits.
truncateUtf8Bytes("a".repeat(300), 245) // 245 chars
truncateUtf8Bytes("😀".repeat(100), 245) // 61 emoji (244 bytes), never a partial pairEach option is optional. sanitiseFilename supports all options.
sanitisePath supports replacement, reserveWindowsNames, and
resolve, and emptyFallback; it does not apply maxBytes to paths or path segments.
| Option | Type | Default | Description |
|---|---|---|---|
replacement |
string |
(none) | Inserted per removed char instead of stripping. E.g. replacement: "_" turns a/b into a_b. |
maxBytes |
number |
(none) | Truncate the file name to at most this many UTF-8 bytes (safe around surrogate pairs). Applies to names only. No truncation by default. |
reserveWindowsNames |
boolean |
true |
Strip reserved device names: con prn aux nul com0-9 lpt0-9 (± extension, case-insensitive). For paths, this applies per segment. Set false to keep them. |
resolve |
boolean |
false |
Path only. Lexically resolve .. against previous emitted segments. Percent sequences still stay literal. |
emptyFallback |
string |
(none) | Returned when the final result would be ''. For paths, this applies only to the whole final path, not to individual dropped segments. |
sanitiseFilename("a/b", { replacement: "_" }) // "a_b"
sanitiseFilename("a".repeat(300), { maxBytes: 245 }) // truncated to 245
sanitiseFilename("con", { reserveWindowsNames: false }) // "con"
sanitiseFilename(" ", { emptyFallback: "untitled" }) // "untitled"
replacement: ""— an empty-string replacement is equivalent to stripping (chars are simply removed). It is not inserted literally.
Name pipeline (in order): 1) strip/replace illegal + control chars
(\ / ? < > : * | " + C0/C1 controls), 2) trim trailing dots/spaces,
3) truncate to maxBytes, 4) reserved-name check (if enabled) → '',
5) emptyFallback if result is ''.
Path rules:
- Backslashes are treated as separators and emitted as
/. - Empty segments from leading, trailing, or repeated slashes are dropped.
- Each path segment is sanitised with the filename rules, excluding
maxBytes. - Empty,
.,.., illegal-only, and reserved-name segments are dropped. - Segments are never resolved:
a/../b→a/b,a/b/..→a/b,../../etc→etc. - Set
resolve: trueto lexically resolve..against previous emitted segments:a/../b→b,a/b/..→a,a/..→'',../../etc→etc. - Percent sequences stay literal:
%2e%2e/%2fis not decoded before sanitising. emptyFallbackapplies to the final whole path only:sanitisePath('', { emptyFallback: 'x' })→'x', whilesanitisePath('a/con/b', { emptyFallback: 'x' })→'a/b'.
Pure JS — no node:path, no Buffer. Bundlers (Vite/webpack/ESBuild/Bun) will
tree-shake it cleanly.
- Output never contains a leading
/, a./..component, or a Windows reserved device name segment (unless disabled). - Path output is POSIX-style and host-independent, so the same input sanitises identically on Windows/macOS/Linux.
- This sanitises path text only. It is not a substitute for
node:path, path resolution, filesystem containment checks, an allow-list, or permission checks. resolve: trueis lexical segment resolution only. It does not prove that a filesystem path remains contained under a directory after symlinks or platform filesystem semantics are applied.
Run with bun bench (powered by mitata).
Lower is better — avg time per iteration over the shared realistic input
suites in benchmarks/bench.ts.
This package is intentionally small: zero runtime dependencies, pure JS output, and deterministic POSIX-style path text on every platform. Contributions should preserve that shape unless there is a clear reason to change the public contract.
Use Bun for package management, tests, and builds:
bun install
bun run typecheck
bun test
bun run buildbun run build regenerates dist/ by bundling src/index.ts to ESM and
emitting TypeScript declarations.
- Keep runtime code dependency-free. Do not import
node:*,Buffer, or a runtime package fromsrc/. - Keep behaviour deterministic across operating systems. Path output should stay
POSIX-style (
/) and must not depend on the host filesystem. - Update tests for every behaviour change. The behaviour contract above should
match
src/sanitise.tsandtest/sanitise.spec.ts. - Update this README and CHANGELOG when public API, options, security notes, or observable behaviour changes.
- Run
bun run typecheck && bun test. Runbun run buildwhen preparing a publishable package.
src/sanitise.ts is optimised for usage in hot paths. Prefer small, direct changes over new
abstractions. The implementation deliberately uses precomputed character lookup
tables and direct UTF-16 code-unit scanning to avoid regex and Buffer overhead in
runtime code.
src/index.ts is the package entrypoint. British exports (sanitise*) are the
canonical API; US spelling is provided only as explicit alias bindings.
MIT
