Part of Ferramenta, a family of Rust tools.
Ferralk finds files. Glob matching and parallel filesystem walking, byte-first, with an unsafe-free matcher.
Explore Ferralk or start with the Rust guide.
Give it a glob such as {src,packages}/**/*.{ts,tsx} and it walks the tree in
parallel, applies .gitignore the way Git does, and opens only the directories
the pattern can reach. Filenames stay raw bytes and native Path values from
the first directory read to the last match, so nothing is lost to a UTF-8
conversion.
On the repository-shaped fixture in the
benchmark snapshot below it is the fastest arm
measured: ahead of ignore, jwalk, walkdir, globwalk and wax, and
level with or ahead of the Zig library it learned from. The same comparison on
Linux CI puts the margin wider — 2.1x over the next-fastest arm on the
unscoped query and 3.4x on the scoped one — and there Ferralk walking
serially still beats four-thread jwalk. One tree shape and a warm cache,
which the snapshot states: a reproducible measurement rather than a claim to
be the fastest walker everywhere.
It is published as two crates, so a consumer that only needs matching does not pay for traversal dependencies:
ferralk-globcompiles a glob once and matches it against arbitrary bytes.ferralkwalks filesystems with root-relative include and exclude patterns, and re-exportsferralk-glob.
Ferralk is independently developed and inspired by zlob 1.6.3. Its matcher and walker behavior are checked against a frozen zlob reference, but it is not source compatible with zlob and deliberately has no C ABI.
- The pattern prunes the walk.
{src,packages}/**/*.{ts,tsx}names the only roots that can match, sonode_modulesis never opened. Ferralk works that out from the pattern. Withignore, the same saving needs a hand-writtenfilter_entry, and passing the globs as overrides alone still reads the whole tree. On the 53,601-file fixture in the benchmark snapshot below, that is 4.32 ms against 7.61 ms for hand-prunedignore— and Ferralk walking serially still beats parallelignorewith the globs as overrides, because not opening a directory beats opening it on four threads. - Sized for the machine, not for the core count. A warm walk is filesystem
metadata work: profiled on macOS, 95% of it is
openat,getdirentries64, andclose, and a raw C walker issuing the same syscalls is no faster. What actually limits it is how much concurrency the kernel's namespace layer rewards, which on Apple Silicon is one performance cluster rather than every core. Ferralk defaults to that measured ceiling instead ofavailable_parallelism, which is worth 1.7–1.9x out of the box on the test host;Walker::threadsoverrides it. The evidence records the controls, the one workload shape that wants more threads, and the hosts this was not measured on. - Git semantics come from Git. Ignore verdicts are replayed against
git check-ignorein CI, on Linux and on Windows, instead of being reimplemented from memory. Nested.gitignorechains,.ignorefiles,.git/info/exclude, linked worktrees, andcore.ignoreCaseare covered. - Nothing surprising is on by default. Hidden files, symlink following, Git ignores, metadata, sorting, and error handling are explicit switches, and the usage guide documents each one with its default.
- Safe Rust, with a named exception list. The matcher crate forbids
unsafeoutright; the workspace denies it, so every remaining occurrence sits under an explicitallowin a file CI names with the reason it is there: the two opt-in native backends and their parity fixtures, onesysctlbynamequery that sizes the default worker budget on Apple Silicon, and two counting allocators that exist only in test targets. A file that starts carryingunsafefails CI, and so does a list entry that no longer needs to be on it. - Git is the oracle, not a spec someone read once. The 994 checked-in
behavioral cases are a machine-readable corpus rather than a side effect of
the tests, and the ignore cases among them are replayed against
git check-ignoreitself, on Linux and on Windows, pinned to Git 2.52.0. If Git disagrees, CI fails; where Ferralk diverges on purpose, the case carries an ADR reference or a recorded oracle defect, and all 76 of them do. - Verification beyond a test suite. Seven fuzz targets, differential checks
against
fast-globand the frozen zlob reference, AddressSanitizer, Miri, and loom models of the scheduler protocol, across Linux, macOS, and Windows. Verification depth counts what a pull request actually runs, and how the count was taken. Twenty ADRs record the decisions rather than leaving them to be rediscovered. Performance is measured on every pull request and never used as a gate.
| Crate | What it is | Where Ferralk differs |
|---|---|---|
ignore |
ripgrep's parallel walker with Git-ignore support | Its overrides decide what is yielded, not what is opened, so pruning a subtree needs a hand-written filter_entry. Ferralk prunes from the include pattern itself. |
globset |
Regex-backed matcher in which * crosses / by default |
Ferralk's * stays inside one path component, as in a shell, and no pattern is translated to a regex. WildcardMode::SeparatorCrossing switches a walk to the globset reading when porting patterns. |
walkdir |
Serial traversal with no selection of its own | Ferralk walks in parallel and selects while it walks. |
zlob |
Zig library behind a C ABI | Ferralk is an independent safe-Rust port of its matcher and walker semantics: no C ABI and no source compatibility. See the migration guide. |
Ferralk is stable. From 1.0.0 on, the
1.x stability contract is in force: it states which public
API, corpus semantics, entry-point rules, platform tier, and MSRV policy are
covered by semantic versioning, and which implementation details and feature
flags are not. The checked-in cargo public-api listings are the
API it covers, and a consumer-visible change to any of it is a major release.
1.0.0 settles the findings of the adversarial round run against
1.0.0-rc.1; the compatibility guide
lists every behavior change since 0.9.0, including the ones 1.0.0 makes. Both
crates are published on crates.io; use the current
1.1.0 release line for applications.
[dependencies]
ferralk = "1.1.0" # x-release-please-version
ferralk-glob = "1.1.0" # x-release-please-versionDepend on ferralk-glob alone when you only match strings or paths you already
hold. For an unreleased Git dependency, pin a revision for repeatable builds.
Compile a pattern once and match it many times. Syntax that changes meaning is
always opt-in: PatternOptions::default() reads **, braces, and extglobs as
ordinary text, and PatternOptions::walker() enables all three, exactly as the
walker does:
use ferralk_glob::{Pattern, PatternOptions};
let source_file = Pattern::compile("src/**/*.{rs,toml}", PatternOptions::walker())?;
assert!(source_file.is_match_glob_path("src/lib.rs"));
assert!(!source_file.is_match_glob_path("src/generated/lib.rs.bak"));
# Ok::<(), ferralk_glob::PatternError>(())Walk a tree with the same component-aware pattern language:
use std::error::Error;
use ferralk::{ErrorPolicy, WalkOptions, Walker};
let result = Walker::new(".")
.include("src/**/*.rs")?
.exclude("**/generated/**")?
.respect_git_ignore(true)
.threads(4)
.error_policy(ErrorPolicy::Collect)
.options(WalkOptions::default().files_only(true).sort(true))
.collect()?;
// `collect()?` succeeded, but that does not mean the walk did: under the
// default `ErrorPolicy::Collect` even a root that does not exist is reported
// in `errors()` below rather than as `Err`.
for entry in result.entries() {
println!("{}", entry.path().display());
}
for error in result.errors() {
// `Display` names the operation and path; `source()` says why it failed.
match error.source() {
Some(cause) => eprintln!("{error}: {cause}"),
None => eprintln!("{error}"),
}
}
# Ok::<(), Box<dyn std::error::Error>>(())collect() sizes its workers to the host by default: available parallelism,
held to one performance cluster on Apple Silicon macOS. Use threads(n) to
choose a budget, or threads(1) for a single-threaded collection. Worker budgets are clamped to 1..=256 to bound
per-walk queues and operating-system threads. stream() is intentionally
single-threaded and unsorted so it can yield entries incrementally;
stream_parallel() yields them incrementally from the same workers collect()
uses, in no particular order.
For a runnable, Git-ignore-aware mini-find using both crates, try:
cargo run --example find -- 'src/**/*.rs'The source is in crates/ferralk/examples/find.rs.
The other examples each run one recipe from the
crate documentation against a root you name, for instance
cargo run -p ferralk --example gitignore_walk -- . '**/*.rs':
gitignore_walk, include_exclude (a fast-glob style list with !
excludes), early_stop, and keep_going (errors that do not end the walk).
- Ordinary wildcards stay inside one path component.
*.tsselectsmain.tsin the walk root and notsrc/main.ts; write**/*.tsfor descendants. The walker enables**, braces, and extglobs for you, whileferralk-globleaves each one opt-in throughPatternOptions;PatternOptions::walker()selects the walker's dialect in one call. - Include wildcards do not cover a leading period.
**/*.tsskips.cache/routes.tsand the whole.cachesubtree. Opt in withWalker::match_hidden(true), or drop hidden entries before any pattern is consulted withWalkOptions::skip_hidden(true). Excludes cover hidden names either way, as.gitignorelines do:**/node_modules/**also removes.cache/node_modules. - Patterns use
/on every platform, and\escapes. Keep the root inWalker::new(root)and write the pattern relative to it. Joining aPathBufinto a pattern on Windows produces\*, which asks for a literal star. - Nothing runs until you ask.
.gitignoreis not read, symlinks below the root are not followed, metadata is not fetched, and entries are not sorted by default. Recoverable errors are collected next to the entries under the defaultErrorPolicy::Collect, so a walk never silently returns less than it found.
The usage guide starts with a table of every default and the switch that changes it.
This snapshot was refreshed on 2026-09-04 on a MacBook Pro with an Apple M1 Pro, 10 cores, 32 GB RAM, macOS 26.6.2, Rust 1.97.1, and Node.js 26.8.1. Each lane was run on its own on an idle machine rather than chained behind the others, which matters more than it sounds: an earlier chained refresh on this same host and commit read roughly 60% slower across every arm. It is a local comparison, not a portable promise. Lower is better.
Reading it in one line: Ferralk walks the unscoped query in 20.83 ms against
23.15 ms for the next-fastest Rust arm, and the scoped query in 4.32 ms
against 7.61 ms for ignore with hand-written pruning — the second gap is the
structural one, because it comes from never opening node_modules rather than
from opening it faster.
The two zlob versions in this README are deliberate: the semantic oracle stays frozen at 1.6.3, the source commit recorded in NOTICE and ADR-0007, so the corpus never moves under an upstream release, while the rows below measure the latest release, 1.6.5, because a speed comparison is only useful against what a consumer would install today.
Matcher values use the common src/**/*.rs syntax. The Rust and zlob rows are
Criterion point estimates; the Node.js rows are medians of fifteen
order-rotated samples, each containing 100,000 matches.
| Matcher | Match | Non-match |
|---|---|---|
| Ferralk (compiled) | 11 ns | 3 ns |
| zlob 1.6.5 (compiled) | 35 ns | 2 ns |
globset (compiled) |
38 ns | 37 ns |
fast-glob (interpreted) |
99 ns | 109 ns |
wax (compiled) |
32 ns | 31 ns |
picomatch 4.0.7 (compiled, Node.js) |
75.0 ns | 85.4 ns |
micromatch 4.0.8 (compiled, Node.js) |
71.7 ns | 84.5 ns |
minimatch 10.2.6 (compiled, Node.js) |
207.0 ns | 191.9 ns |
The walker fixture contains 53,601 files. An unscoped query such as
**/*.{ts,tsx} can match anywhere, so every walker must inspect the complete
tree. A scoped query such as {src,packages}/**/*.{ts,tsx} names the only
roots that can match. Ferralk can therefore skip the rest of the tree,
especially node_modules, directly from the pattern.
| Walker | Unscoped, 7,400 matches | Scoped, 2,600 matches |
|---|---|---|
| Ferralk portable, 4 threads | 20.83 ms | 4.32 ms |
| zlob 1.6.5, same portable run | 30.09 ms | 22.08 ms |
| Ferralk macOS-native, 4 threads | 20.83 ms | 3.68 ms |
| zlob 1.6.5, same native run | 21.45 ms | 22.32 ms |
Node.js node:fs sync |
201.53 ms | 26.42 ms |
glob 13.0.6 async |
35.12 ms | 6.34 ms |
fast-glob 3.3.3 async |
41.14 ms | 6.62 ms |
tinyglobby 0.2.17 async |
36.14 ms | 7.14 ms |
fdir 6.5.0 + picomatch async |
35.12 ms | 31.39 ms |
The Rust walker rows are Criterion point estimates; the Node.js rows are
medians of ten order-rotated samples. The APIs and estimators are not identical,
so these are same-host context rather than a universal ranking. zlob appears
twice to keep each Rust comparison inside one invocation; against the native
backend the two engines are level on the unscoped query, and the wider portable
margin is what reading directories through std::fs costs. See
benchmark evidence for the complete tables,
exact commands, sampling details, semantic differences, and limitations.
For the same comparison on real repositories instead of fixtures — including the
ignore-with-hand-pruning arm the RFC's question really turns on — see
Palamedes adoption.
Using Ferralk:
- Usage guide — every default and switch, matching and walking semantics, and operational guidance.
- 1.x stability contract — the semver-covered surface, MSRV policy, and explicit exclusions.
- Consumer pattern contract — for programs that
pass their users' globs on: what 1.x covers, validating and escaping a
pattern, and porting from
globset. - Benchmark evidence — what is measured, how to reproduce it, and what it does not establish.
- Verification depth — the checked-in evidence counted next to comparable crates, and where counting misleads.
- Palamedes adoption — a consumer integration measured over four releases on two real repositories, and what each round changed here.
Using Ferralk from a coding agent:
llms.txt— one screen with the defaults, the three matcher entry points, the traps that compile but return wrong results, and links to the tested recipes on docs.rs.- Agent skill — the same guidance as a loadable
skill; copy
skills/ferralkinto your agent's skills directory, for example.claude/skills/.
Coming from another library:
- Migration guide — mapping from zlob 1.6.3,
a migration table
for
globset,glob, fast-glob,ignore, andwalkdir, and the deliberate differences. - Compatibility matrix — feature-level status against zlob.
Contributing:
- Contributing — the preflight, commit conventions, and the 1.0 release checklist.
- Corpus format — the JSONL behavioral test corpus that is the source of truth for matcher and walker semantics.
- Architecture RFC and ADRs — design rationale and non-goals.
- Deferred follow-up — open work tracked in GitHub after the initial release.
The documentation index lists everything, including the historical audit records.
- Keep filesystem paths as
Path/PathBuf; never turn filenames into lossy UTF-8 strings. - Keep ordinary glob wildcards inside one path component. Use explicit
**for recursive traversal. - Make policy choices explicit: hidden files, symlinks, Git ignores, metadata, sorting, error handling, and cancellation all have documented controls.
- Keep matching byte-first and portable. On Windows, the public API preserves native paths while normalizing only the matcher input representation.
The default portable backend is the stable target. The native-linux and
native-macos features are experimental and may be renamed or removed in a
1.x minor release; everything behind them is outside the 1.x compatibility
contract.
Run the single canonical pull-request preflight in CONTRIBUTING. It includes the separate fuzz workspace and does not require Zig; CI installs Zig for its additional coverage lane. That guide also records repository policy, including commit signing and how performance claims are evidenced. See the contributor-oriented guide for the corpus, fuzzing, and benchmark commands.
Ferralk is dual-licensed under the MIT license or the Apache License, Version 2.0, at your option. It is an independent project inspired by zlob 1.6.3; provenance and attribution are recorded in NOTICE and the frozen reference. Report vulnerabilities through the private process in SECURITY.md.
Ferramenta — A family of Rust tools.
The content pipeline
| Tool | Job |
|---|---|
| ferroni | Oniguruma-compatible regex engine |
| ferriki | Shiki-compatible syntax highlighting |
| ferromark | Markdown to HTML with a secure default and every GFM extension included. |
The language workshop
| Tool | Job |
|---|---|
| ferrolex | Spell checking for text and code |
| ferrocat | Translation catalog engine |
| palamedes | Internationalization for TypeScript applications |
On the workbench
| Tool | Job |
|---|---|
| ferrovia | SVGO-compatible SVG optimizer |
| ferrugo | PDF previews for untrusted files |
TypeScript, React & Rust consulting
Experts in Agentic Software Development
Work with us · More open source
Copyright © 2026 Sebastian Software GmbH