What is Project Management in Emacs
Project management in Emacs refers to the set of tools and workflows that allow you to group related files, buffers, and configurations under a unified context—a "project." Instead of navigating a sprawling directory tree manually, you operate within a defined scope that knows where your source files live, which version control system you use, and what custom settings apply.
Emacs offers two primary project management systems:
- project.el – Built-in since Emacs 25.1, lightweight, and tightly integrated with core Emacs
- Projectile – A third-party package with advanced features, extensive caching, and a rich command set
Both systems identify projects by detecting special marker files. The most common markers include .git directories, Makefile, pom.xml, package.json, Cargo.toml, and many others. Once a project root is recognized, Emacs can switch contexts instantly—changing your entire working state to match the project you select.
Core Concepts
- Project Root – The top-level directory containing the marker file
- Project Buffer List – All buffers associated with the current project
- Project Switching – Commands to jump between projects seamlessly
- Project-aware Commands – Operations like find-file, grep, and shell that scope themselves to the project
Why Project Management Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Without project management, developers frequently fall into several productivity traps:
- Context switching pain – Manually killing buffers, changing directories, and re-launching terminals every time you move between codebases
- Scattered buffer lists – Hundreds of open buffers from unrelated projects, making
switch-to-buffernearly unusable - Accidental edits – Editing files from the wrong project because paths look similar
- Repeated setup – Setting compile commands, environment variables, and linter configurations over and over for the same project
Project management solves these by giving you a single point of entry for everything related to a codebase. You think in terms of "I am working on Project X" rather than "I am somewhere inside /home/dev/work/project-x/src/components/..."
Key benefits include:
- Instant project switching – One command changes your entire context
- Scoped search and navigation – Find files, grep, and browse only within the project
- Per-project configuration – Automatically apply the right settings for each codebase
- Session persistence – Restore exactly where you left off when reopening a project
- Integration with external tools – Compile, test, and run scripts from the project root automatically
Using Built-in project.el
Since Emacs 25.1, project.el ships as part of Emacs. It provides a minimal but powerful project interface that works out of the box with no extra configuration. In Emacs 28+, it gained even more features including project-finding and switching commands.
Finding and Switching Projects
The central command is project-switch-project, bound to C-x p p by default. When invoked, it presents a list of known projects and, after selection, opens a menu of actions:
;; Default key binding
C-x p p → project-switch-project
;; After selecting a project, you see a prompt like:
;; Switch to project: /home/user/projects/myapp/
;; [f] Find file [g] Find regexp [d] Dired
;; [b] Switch buffer [e] Eshell [s] Shell
;; [c] Compile [r] Run command
You can customize this menu with project-switch-commands:
(setq project-switch-commands
'((project-find-file "Find file")
(project-find-regexp "Find regexp")
(project-dired "Dired")
(project-eshell "Eshell")
(project-compile "Compile")
(project-switch-to-buffer "Switch buffer")
(magit-status "Magit")))
Discovering Project Roots
Emacs detects project roots by scanning ancestor directories for known markers. You can customize the detection list:
(setq project-vc-extra-root-markers
'(".project.el" ;; Custom Emacs project file
"CMakeLists.txt"
"meson.build"
"gradle.build"
"docker-compose.yml"))
For non-VC projects (directories without version control), Emacs falls back to looking for these markers in project-vc-extra-root-markers. You can also explicitly register projects:
;; Manually add a project directory
(project-remember-project
(project-current t "/home/user/docs/"))
;; List all remembered projects
(project-remembered-projects)
Key Built-in Commands
C-x p f–project-find-file: Fuzzy-find a file in the current projectC-x p g–project-find-regexp: Grep across all project files (requiresripgreporgrep)C-x p d–project-dired: Open Dired at the project rootC-x p b–project-switch-to-buffer: Switch to a buffer belonging to the projectC-x p e–project-eshell: Launch Eshell at the project rootC-x p s–project-shell: Launch a terminal at the project rootC-x p c–project-compile: Runcompilefrom the project rootC-x p k–project-kill-buffers: Kill all buffers belonging to the project
Per-Project Configuration with .dir-locals.el
For project-specific settings that Emacs should apply automatically, create a .dir-locals.el file at the project root. This works with both built-in project.el and Projectile:
;; .dir-locals.el at project root
((nil . ((compile-command . "make test")
(tab-width . 4)
(indent-tabs-mode . nil)))
(python-mode . ((python-interpreter . "python3")
(flycheck-python-pylint-executable . "pylint")))
(web-mode . ((web-mode-code-indent-offset . 2))))
The nil alist applies to all modes, while mode-specific alists (like python-mode) only apply to buffers in that mode. Emacs asks once whether to apply these variables permanently for safety.
Custom Project Functions
You can build custom commands that operate in project context using project-current:
(defun my/project-run-tests ()
"Run the project's test suite."
(interactive)
(let* ((proj (project-current))
(root (project-root proj)))
(compile (format "cd %s && npm test" root))))
(defun my/project-open-todo ()
"Open the project's TODO file."
(interactive)
(let* ((proj (project-current))
(root (project-root proj)))
(find-file (expand-file-name "TODO.md" root))))
Using Projectile (Advanced)
Projectile is the most popular third-party project management package for Emacs. It predates project.el and offers a richer feature set including caching, indexing, and extensive integration with other packages.
Installation
Install via MELPA using use-package:
(use-package projectile
:ensure t
:config
(projectile-mode +1)
;; Recommended: use faster indexing
(setq projectile-indexing-method 'alien) ;; uses external tools
(setq projectile-enable-caching t)
;; Key bindings
(define-key projectile-mode-map (kbd "C-c p") 'projectile-command-map))
The recommended prefix is C-c p, which mirrors C-x p for built-in project commands. All Projectile commands live under projectile-command-map.
Project Detection
Projectile recognizes projects by scanning for a wider range of markers than project.el. The default list includes:
;; Default project markers in projectile-project-root-files
'(".git" ;; Git repository
"rebar.config" ;; Erlang
"project.clj" ;; Clojure Leiningen
"pom.xml" ;; Maven
"build.sbt" ;; Scala SBT
"gradlew" ;; Gradle wrapper
"setup.py" ;; Python
"Cargo.toml" ;; Rust
"go.mod" ;; Go modules
"package.json" ;; Node.js
"CMakeLists.txt" ;; CMake
"Makefile" ;; Make
...and many more)
You can add custom markers:
(setq projectile-project-root-files
(append projectile-project-root-files
'(".projectile" ;; Projectile-specific marker
"docker-compose.yml"
"ansible.cfg"
"terraform.tf")))
;; For projects outside version control
(setq projectile-project-root-files-bottom-up
'(".projectile")) ;; Scan upward for these
Core Projectile Commands
C-c p f–projectile-find-file: Fuzzy-find file in project (usesprojectile-completion-system)C-c p F–projectile-find-file-dwim: Find file at point or under cursorC-c p s g–projectile-grep: Search across project filesC-c p p–projectile-switch-project: Switch to another projectC-c p a–projectile-find-other-file: Switch between source/header/testC-c p b–projectile-switch-to-buffer: Switch buffer within projectC-c p d–projectile-find-dir: Find directory in projectC-c p k–projectile-kill-buffers: Kill all project buffersC-c p t–projectile-test-project: Run project testsC-c p c–projectile-compile-project: Compile the projectC-c p r–projectile-run-project: Run the projectC-c p e–projectile-edit-dir-locals: Edit.dir-locals.el
Indexing and Caching
Projectile maintains an index (cache) of project files for fast completion. The indexing method determines speed and accuracy:
;; Available indexing methods
;; 'alien - Uses external tools: find, fd, or ripgrep (fastest)
;; 'native - Pure Emacs Lisp (portable, slower)
;; 'hybrid - Mix of both
(setq projectile-indexing-method 'alien)
;; Enable persistent caching across Emacs sessions
(setq projectile-enable-caching t)
;; Cache file location
;; Default: ~/.emacs.d/projectile.cache
(setq projectile-cache-file (expand-file-name "projectile.cache" user-emacs-directory))
;; Invalidate cache manually
;; M-x projectile-invalidate-cache or C-c p i
;; Clear all caches:
;; M-x projectile-reset-cache
When using 'alien indexing, Projectile prefers fd if installed, falling back to find. You can explicitly set the command:
(setq projectile-generic-command "fd . --color=never --type f -0"
projectile-git-command "git ls-files -zco --exclude-standard")
Per-Project Configuration with .projectile
In addition to .dir-locals.el, Projectile supports its own project configuration file:
;; .projectile at project root (Elisp format)
((compile-command . "make && make test")
(test-command . "make test")
(run-command . "make run")
(src-dir . "src/")
(test-dir . "tests/")
(related-files-fn . my-custom-related-files-function))
This file can also be a plain list of files to ignore (like .gitignore):
;; .projectile as ignore file
*.elc
*.pyc
__pycache__/
node_modules/
dist/
build/
Integration with Other Tools
Projectile integrates seamlessly with several popular Emacs packages:
;; Counsel/ivy integration
(use-package counsel-projectile
:ensure t
:config
(counsel-projectile-mode +1)
;; Then use: C-c p f for ivy-powered file finding
)
;; Helm integration
(use-package helm-projectile
:ensure t
:config
(helm-projectile-on)
;; Then use: C-c p f for helm-powered file finding
)
;; Magit integration - use projectile context in magit
(setq projectile-switch-project-action 'projectile-magit)
;; Now C-c p p opens magit-status instead of a file prompt
Customizing the Switch Action
When you switch projects with C-c p p, Projectile invokes projectile-switch-project-action. Customize it to suit your workflow:
;; Open the project dashboard (dired) on switch
(setq projectile-switch-project-action 'projectile-dired)
;; Open a specific file
(setq projectile-switch-project-action
(lambda ()
(projectile-find-file "README.md")))
;; Do nothing, just change the default directory
(setq projectile-switch-project-action 'projectile-ignore)
Finding Related Files
One of Projectile's best features is switching between related files—source, header, test, and fixture files. The command projectile-find-other-file (C-c p a) cycles through alternatives:
;; Customize related file patterns
(setq projectile-other-file-alist
'(("src/\(.*\)\.cpp" . "include/\1.hpp") ;; C++ source ↔ header
("src/\(.*\)\.c" . "include/\1.h") ;; C source ↔ header
("src/\(.*\)\.js" . "tests/\1.test.js") ;; JS source ↔ test
("src/\(.*\)\.py" . "tests/test_\1.py") ;; Python source ↔ test
("lib/\(.*\)\.ex" . "test/\1_test.exs") ;; Elixir source ↔ test
))
Regenerating Tags
Projectile can automatically regenerate TAGS files for code navigation:
(setq projectile-tags-backend 'gtags) ;; Use GNU Global
;; Other options: 'etags, 'ctags, 'auto
;; Auto-regenerate tags on project switch
(setq projectile-regenerate-tags t)
;; Manual regeneration
;; M-x projectile-regenerate-tags
Project Management with Bookmarks and Desktop
Beyond project.el and Projectile, Emacs offers built-in session management that complements project workflows:
;; Save and restore your entire Emacs session
;; Including buffers, windows, and points within files
(desktop-save-mode 1)
;; Save desktop automatically
(setq desktop-auto-save t)
(setq desktop-save-interval 300) ;; every 5 minutes
;; Bookmark projects for quick access
;; M-x bookmark-set → save current location
;; M-x bookmark-jump → jump to saved location
;; Bookmark a project root with context
(defun my/bookmark-project-root ()
"Bookmark the current project root."
(interactive)
(let ((proj (project-current)))
(when proj
(bookmark-set (project-name proj))
(message "Bookmarked project: %s" (project-name proj)))))
Integrating with Tab-Bar and Workspaces
In Emacs 27+, tab-bar-mode allows you to organize projects into separate tabs. Combine this with project management for a workspace-like experience:
;; Enable tab-bar
(tab-bar-mode 1)
;; Create a new tab scoped to a project
(defun my/project-tab ()
"Create a new tab for the current project."
(interactive)
(let ((proj (project-current)))
(when proj
(tab-bar-new-tab)
(tab-bar-rename-tab (project-name proj))
;; Switch to project's main file or dired
(project-dired))))
;; Close all tabs belonging to a project
(defun my/project-close-tabs ()
"Close all tabs for the current project."
(interactive)
(let ((proj-name (project-name (project-current))))
(tab-bar-close-other-tabs)
(message "Closed tabs for %s" proj-name)))
For more advanced workspace management, consider packages like persp-mode or eyebrowse, which integrate well with project.el and Projectile.
Best Practices
1. Choose One System and Stick With It
While project.el and Projectile can coexist, their key bindings overlap (C-x p vs C-c p). Pick one as your primary interface. Many developers start with built-in project.el and only reach for Projectile when they need advanced features like related-file switching or alien indexing.
2. Set Up .dir-locals.el Early
Create a .dir-locals.el file as soon as you start a project. Include at minimum:
- The correct
compile-command - Indentation rules for the project's coding style
- Any linter or formatter executables
Commit this file to version control so the whole team benefits from consistent Emacs configuration.
3. Use Fast External Indexing
Install fd or ripgrep on your system and configure Projectile's alien indexing. The speed difference on large codebases (thousands of files) is dramatic—from seconds to milliseconds for file finding operations.
4. Combine with Version Control Awareness
Pair project management with Magit or VC mode. When you switch projects, your version control context should switch too. Configure projectile-switch-project-action to open Magit status, giving you immediate visibility into your repository state.
5. Leverage Project-Specific Shells
Use project-eshell or project-shell instead of manually changing directories in a terminal. These commands always start at the project root, ensuring your build commands, scripts, and file operations work correctly regardless of which buffer you were in when you launched the shell.
6. Clean Up Buffers Regularly
When you finish working on a project, run project-kill-buffers (C-x p k or C-c p k). This prevents buffer bloat and keeps your Emacs session fast. Combine this with desktop-save-mode so you can safely restart Emacs without losing context.
7. Use Project-Aware Grep Exclusively
Replace generic grep and rgrep with project-find-regexp or projectile-grep. These commands automatically scope searches to the project, excluding build artifacts, node_modules, and other noise defined in your ignore files.
8. Create Custom Project Commands
Every project has unique workflows—running a dev server, deploying to staging, opening project documentation. Wrap these in custom commands that use project-current or projectile-project-root to find the correct context:
(defun my/project-start-dev-server ()
"Start the project's development server."
(interactive)
(let* ((proj (project-current))
(root (project-root proj)))
(compile (format "cd %s && npm run dev" root))
(message "Dev server started for %s" (project-name proj))))
9. Maintain a Clean Project Marker Strategy
Be deliberate about which marker files define your projects. In monorepos with multiple packages, you may want each package to be its own project (using package.json or Cargo.toml as markers). In simpler repos, the .git directory at the root is sufficient. Add .projectile files to explicitly designate boundaries when needed.
10. Integrate with Completion Frameworks
Whether you use Ivy, Helm, Vertico, or the default completion, ensure your project commands leverage that system. Packages like counsel-projectile and helm-projectile dramatically improve the file-finding experience with fuzzy matching, previews, and instant results.
Conclusion
Project management transforms Emacs from a simple text editor into a full development environment where context switching is effortless. Whether you choose the built-in project.el for its simplicity and zero-config approach, or Projectile for its advanced indexing, related-file navigation, and extensive customization, the key is to build habits around project-aware commands. Start by binding C-x p p or C-c p p into your muscle memory, configure .dir-locals.el for each codebase, and let Emacs handle the rest. With these tools, you'll spend less time navigating and more time building—exactly where a developer's focus belongs.