sanitise-path
v1.0.1
Published
Zero-dependency, pure-JS file name and path sanitisation with configurable options.
Readme
sanitise-path
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*).
Install
bun add sanitise-pathAPI
import {
sanitiseFilename, // sanitizeFilename
sanitisePath, // sanitizePath
sanitise,
truncateUtf8Bytes,
} from "sanitise-path";sanitiseFilename(name, opts?) → string
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)sanitisePath(path, opts?) → string
Sanitises a file path (no filename). Pure-JS, deterministic POSIX output on every OS.
sanitisePath('../etc/passwd') // 'etc/passwd' (no traversal)
sanitisePath('a/../b') // 'b' (canonical resolution)
sanitisePath('%2e%2e%2fetc') // 'etc' (decodes %2e/%2f/%5c)
sanitisePath('/var/app/..') // 'var' (trailing '..' resolves)
sanitisePath("/path/$dir!/file|name.txt") // 'path/dir/filename.txt'sanitise(filePath, fileName, opts?) → { path, name }
Convenience wrapper — one call for both. Passes the same opts to each.
sanitise("dir/../up", "report?.pdf")
// => { path: "up", name: "report.pdf" }truncateUtf8Bytes(str, maxBytes) → string
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 pairOptions
Shared by all three functions. Each is optional.
| 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 the file name only — never to paths. No truncation by default — the old hardcoded 245 budget is gone. |
| reserveWindowsNames | boolean | true | Strip reserved device names: con prn aux nul com0-9 lpt0-9 (± extension, case-insensitive). Set false to keep them. |
| emptyFallback | string | (none) | Returned when the result would be '' (whitespace/dots-only, illegal-only, or a reserved name) — for both the file name and the path. Guards the empty-filename footgun. |
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.
Behaviour contract
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:
..resolves canonically wherever it appears, clamped at the relative root —a/../b→b,a/b/..→a,a/..→"",../../etc→etc(pops that would climb above the root are dropped). A./..component is never emitted; a whole dot-only result leads to''.- Only
%2e,%2f,%5care decoded (lowercase only); other sequences stay literal. - Forbidden path chars
: $ ! ' " @ + \| =` are removed. - Reserved-name matching happens after trailing-trim, so
"prn "→''. emptyFallbackapplies to the path as well as the name:sanitisePath('', { emptyFallback: 'x' })→'x'.
Browser vs Node
Pure JS — no node:path, no Buffer. Bundlers (Vite/webpack/ESBuild/Bun) will
tree-shake it cleanly.
Security notes
- Output never contains a leading
/, a./..component, or a Windows reserved device name (unless disabled). - Path output is POSIX-normalised and host-independent, so the same input sanitises identically on Windows/macOS/Linux.
- This is a sanitisation helper, not a substitute for a proper allow-list or permission check when paths hit the filesystem.
Benchmark
Run with bun bench (powered by mitata).
Lower is better — avg time per iteration over the shared realistic input
suites in benchmarks/bench.ts.

Development
bun install
bun test # 142 tests
bunx tsc --noEmit # typecheck
bun run build # dist/ (ESM + .d.ts) via scripts/build.tsLicense
MIT
