workspace¶
Where is the project root? Nearly every CLI tool needs to answer that before it does
anything else. workspace answers it the way git and go do — walk up from a starting
directory until a marker file appears.
ws, err := workspace.DetectFromCWD(afero.NewOsFs(), workspace.DefaultMarkers)
if err != nil {
return errors.Wrap(err, "not inside a project")
}
fmt.Println("Project root:", ws.Root) // e.g. /home/user/project
fmt.Println("Detected via:", ws.Marker) // e.g. go.mod
Why¶
- The filesystem is injected.
DetectandDetectFromCWDboth take anafero.Fs, so the walk runs entirely in memory in tests — no temp directories, no touching the real disk. Passafero.NewOsFs()in production,afero.NewMemMapFs()in tests. See test filesystem walks in memory. - Markers are ordered; first match wins.
DefaultMarkersis{".gtb/manifest.yaml", "go.mod", ".git"}— a generated-project manifest outranks the Go module root, which outranks the git root. Bring your own set for any ecosystem — see detect other project types. - The walk is bounded. It climbs at most
DefaultMaxDepth(100) parents before giving up withErrNotFound, so a stray path can never scan unboundedly. Override withWithMaxDepth. The full mechanism is in the marker walk. - No framework weight. The entire dependency graph is afero,
cockroachdb/errors, and the standard library — enforced by adepfootprinttest, and the reason this is its own module.
What it will not do¶
Before you build on it: the walk returns the nearest enclosing marker and cannot be
asked for the outermost one, it does not resolve symlinks, it never opens the marker file,
and every failed search returns the same ErrNotFound whatever caused it. The full list is
in what workspace does not do.
Start here¶
- Getting started — detect a project root in a few lines, and handle the failure case.
- Detect other project types — Node, Rust, Python.
- Find the repository root, not the nearest module — the monorepo case.
- Test filesystem walks in memory — the afero seam.
- The marker walk — precedence, depth bound, and root stop.
- Reference — every exported symbol, every default, and what happens when an argument is wrong.
Godoc signatures live on pkg.go.dev.
Further reading¶
The blog carries a curated route through this subject: Building a command-line tool in Go collects everything written about it, ordered so you can start at the beginning rather than newest-first.
Ask phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.