Adopting Emacs' Built-in project.el for Project Navigation
Emacs ships with project.el, a leaner alternative to Projectile. Learn how to configure custom project roots, verify detection, and weigh the trade-offs.
18 Mar 2026, 11:28 UTC

The root problem
Every Emacs workflow eventually hits a wall: you want to search across “the project,” but Emacs needs to know where the project ends and the rest of the filesystem begins. If you use a third-party package like Projectile, you already have an answer. Emacs 27 and later, however, include a built-in library, project.el, that can handle the same job with fewer dependencies—if you configure it for your directory layout.
The decision to adopt project.el is not about whether it works, but whether its leaner model fits your repositories and whether you are willing to teach it about non-standard roots.
Default behavior and core commands
By default, project.el walks upward from the current buffer until it finds a version-control directory such as .git or .hg. That directory becomes the project root. From there, a small set of commands covers the most common navigation tasks:
| Command | Default binding | Purpose |
|---|---|---|
project-find-file | C-x p f | Open a file from the project root |
project-find-regexp | C-x p g | Search across project files |
project-shell-command | C-x p ! | Run a shell command in project root |
project-switch-project | C-x p p | Switch to another known project |
These commands rely on external tools such as find or ripgrep to enumerate files and search content. If those tools are on your PATH, the commands work out of the box in any Git repository.
Teaching project.el about monorepos and custom roots
The default VCS-based heuristic breaks down in monorepos. If a single Git repository contains multiple independently deployed services, the repository root is often too broad: searching for a symbol returns matches from unrelated directories.
There are two ways to narrow the scope. First, depending on your Emacs version, you can extend project-vc-extra-root-markers with filenames that should also mark a root. Second, you can add a custom function to project-find-functions that returns a directory name or nil.
The following example treats any directory containing a .project.el file as a project root, even when it sits inside a larger Git repository. Add this to your init file and evaluate it, or run M-x eval-buffer in the buffer:
(defun project-try-marker (dir)
"Find project root via .project.el marker file."
(let ((root (locate-dominating-file dir ".project.el")))
(when root
(expand-file-name root))))
(add-to-list 'project-find-functions #'project-try-marker)
Where to run this: in your Emacs init file (~/.emacs.d/init.el or ~/.emacs), or interactively via M-x eval-expression. No special permissions are required beyond read access to the directories involved.
Expected check: after evaluating, call M-x project-find-file from a buffer inside a directory that contains .project.el. The file list should be scoped to that sub-directory, not the repository root. To confirm the function is active, evaluate:
(member #'project-try-marker project-find-functions)
It should return non-nil.
Trade-offs, limitations, and performance
Adopting project.el means trading ecosystem depth for a smaller dependency footprint. Projectile offers a more extensive command map, more third-party integrations, and a longer history of community documentation. project.el is leaner: fewer conveniences out of the box, but nothing extra to install or maintain.
Performance on very large repositories can be a concern because project-find-file and project-find-regexp shell out to external tools. If file listing is slow, tune project-vc-ignores to exclude build artifacts, dependency directories, or large data folders. Configuring a faster grep backend—where your Emacs version supports it—can also reduce search latency.
Version variance is a real limitation. Commands and variables introduced in Emacs 28 or 29 may not exist in older installations. Before relying on a specific variable such as project-vc-extra-root-markers, check it with M-x describe-variable. Finally, the default prefix C-x p may collide with user bindings or window-manager shortcuts on some systems; if it does, rebind the commands to a different prefix in your init file.
Verifying the migration
A practical way to check the result is to work in a repository you know well. Open a file deep in the tree, run M-x project-find-file, and confirm the candidate list starts at the expected root. Run M-x project-find-regexp with a term you know appears in only one sub-package, and verify the scope. If you added custom markers, use M-x describe-variable RET project-vc-extra-root-markers to confirm they are recognized.
If you later decide project.el is too lean, removing your custom function is straightforward: delete or comment out the add-to-list line and restart Emacs or re-evaluate your init file. No state outside your configuration is changed by the commands themselves.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.