Useful? A star is how other developers find it — ★ GitHub · letools.dev/tools/paths-le
A path in a config file is a promise about the filesystem, and nothing
checks it. The import still compiles because the bundler resolves it
differently. The asset still loads because the symlink is still there.
The .. in the middle of that path means something other than it looks
like. paths-le reads the paths out of your files and then goes and looks:
does it exist, does it leave the tree, is it written the way it resolves,
and is anything on the way a link.
It is the second frontend of
Paths-LE, the VS Code
extension — one product, two frontends, one repository, so the two can
never read a document differently. The corpus both build against lives at
crate/fixtures/,
and CI fails on drift.
The other four ways to run it
Same engine, four other front doors. Pick the one that fits where you are; nothing here is a lesser version of anything else.
| Where | What you get | Install |
|---|---|---|
| VS Code | The extraction, in your editor, on a keystroke | Marketplace |
| Cursor, VSCodium, Windsurf | The same extension | Open VSX |
| Any MCP agent, via Node | extract_paths over stdio — the same tool this binary offers |
npx paths-le-mcp · npm |
| Zed | The MCP server as a context server | add it by hand (no listing yet) |
Which MCP server should you run? They answer identically —
fixtures/mcp-extract-paths.json runs against both and CI fails if they
diverge. Take npx paths-le-mcp if Node is already there and you only
want extract_paths. Take paths-le mcp if you want one static binary
with no runtime, or if you want paths_le_audit too — resolving paths
against the filesystem is this binary's half, and the npm server
deliberately touches no files.
All ten LE tools are on letools.dev.
Sixty seconds
|
./pkg.json:3:12 ./docs//guide.md [non-canonical — contains a duplicate separator]
./src/app.ts:2:16 ./gone.ts [missing — no such file or directory]
./src/app.ts:3:16 ../../escape/out [escapes root — resolves outside /repo]
5 paths in 3 files — 3 findings
The report is JSON on stdout — one object per line, one line per file — the summary above is stderr, and the exit code is the answer: 0 clear · 1 findings · 2 the question was malformed.
Every verdict is checkable by hand against the same filesystem. That is the rule the tool is built to: if a claim cannot be verified that way, it does not get made.
Install
| Route | Command | Worth knowing |
|---|---|---|
| cargo | cargo install paths-le |
Any platform, needs Rust 1.88+. |
| From source | git clone https://github.com/nolindnaidoo/paths-lecd paths-le/crate && cargo build --release |
The same build CI runs. |
No runtime, no browser, no network — it reads files and asks the filesystem about them, and that is all it ever does.
What it reads
Eight formats, the same eight the extension supports: JSON/JSONC,
TOML, CSV, dotenv, JavaScript, TypeScript, HTML, CSS/SCSS/LESS. A
directory is walked the way ripgrep walks one — .gitignore honoured,
hidden files skipped — so what it looks at is the answer you already have
in your head. A file named explicitly is always read, ignore rules
included.
Files in other formats are skipped silently when they turn up in a walk, and refused loudly when you name one: a repository is full of files this has nothing to say about, and naming one means you expected otherwise.
The verdicts
| verdict | meaning | counts as a finding |
|---|---|---|
ok |
exists, canonical, inside the root | no |
symlinked |
exists; the path or a component is a link, target reported | with --deny-symlinks |
non-canonical |
exists, but the written form is not how it resolves | yes |
missing |
does not exist | yes |
escapes-root |
a relative path that resolves above the root | yes |
unresolved |
not checked, or not a filesystem path | no |
Five rules keep this usable rather than noisy — each one came from running it over real repositories and throwing out the answers that were technically true and practically useless:
- An import written without an extension resolves to the file it
names.
./dedupefindsdedupe.ts, and says so in the verdict. The candidate list is fixed —.ts .tsx .js .jsx .mjs .cjs .json— and every answer is a file you canls.tsconfigpath maps and bundler aliases stay out of scope. missingrequires the value to commit to being a path. It earns that two ways: explicit syntax (./x,../x,/x,C:\x), or a file extension after a separator (src/app.ts). Everything else —image/png,@heroui/styles,io.github.you/tool,^1.101.0,example.com— still resolves tookwhen something is there, and comes backunresolvedwhen it is not. Its absence is not evidence it was ever a path.- A leading
./or../is idiomatic, not a finding. A check that fires on every relative import in every codebase is a check nobody reads.non-canonicalmeans genuinely ambiguous: a duplicate separator, a trailing slash, mixed separators, or a..in the middle of a path. - An absolute path never "escapes". It is absolute by intent; it is judged on existence alone.
- A symlink is a fact by default, and a finding when you ask. The
extension treats resolving a link as an ordinary step, so that is the
default here;
--deny-symlinksis for the audits that exist to catch an unexpected one. - Canonicalisation counts without being asked.
normalizePathin the extension defines canonical form, so a path that deviates is one it would have rewritten — an audit that stayed quiet about that would be withholding the thing it was asked for. - The root is the enclosing git repository. Rooting at the directory you named instead makes every cross-package import in a monorepo an escape.
Run over the eleven repositories these rules were developed against,
six report zero findings and the rest report one or two — each of which
is a genuinely absent path. The cost of that quiet is stated plainly
above: an extensionless path written without a leading ./, like
docs/api, is no longer reported when it goes missing, because that
shape is also how bare module specifiers are written.
Options
--no-resolve report paths as written; skip the filesystem entirely
--root <dir> the boundary a relative path may not escape
(default: the enclosing git repository)
--deny-symlinks treat a symlink as a finding too
--format <format> force a format instead of inferring it from the name
--stdin read one document from stdin
--follow-symlinks resolve through symlinks when walking a tree
--hidden walk hidden files and directories too
--no-ignore walk files that .gitignore excludes
A relative path resolves against the directory of the file it was found in, never the working directory — that is what it means to the code that contains it.
In CI
- name: No broken paths
run: paths-le .
Exit 1 fails the step on a real finding. Exit 2 means the tool could not answer — an unreadable file, a directory it cannot enter — and fails it too, because an audit that silently skipped something is worse than no audit.
As an MCP server
Two tools, both returning { ok, data, diagnostics, meta }:
extract_paths— content in, paths out. Touches no filesystem. The npm server ships the same tool with byte-identical output; one corpus runs against both.paths_le_audit— files or directories in, the same reports the CLI writes to stdout.
ok means the check ran, never that the answer was yes. A file full of
broken paths is a result, not an error.
What it will not do
- It does not rewrite files. There is no
--fix. - It has no style opinions. Where you put your paths is your business; whether they point at anything is not an opinion.
- It does not learn your bundler. It appends a known extension and
looks — that is a filesystem question.
tsconfigpath maps, bundler aliases andnode_modulesresolution need a config file to be right about, and half-guessing them would produce confident wrong answers. - It never touches the network. An
https://path is classified and left alone.
Full behaviour, including what is ported from the extension bug-for-bug and why, is in SPEC.md; the engineering standard this crate is held to is in AGENTS.md, and what changed is in CHANGELOG.md.
Also by nolindnaidoo
Rust
- pixelcoords — Freeze your screen, mark regions, get pixel-exact coordinates and crops pixelcoords.dev · crates.io · docs.rs
- pixelactions — Consume human-verified coordinates, perform the interaction, confirm it landed pixelactions.dev · crates.io · docs.rs
- secrets-le — Find hardcoded credentials, and never print one crates.io
- urls-le — Extract every URL from a codebase, with its protocol and exact position crates.io
- regex-le — Find every regex in a codebase and report which can be driven into catastrophic backtracking crates.io
- scrape-le — Check whether a page is scrapeable before the scraper is written crates.io
LE Tools — ten editor extensions, each also an MCP server: letools.dev
Contact Developer — nolindnaidoo.com · GitHub · LinkedIn
License
MIT — see LICENSE.