← Back to DevBytes

Emacs Project Management: Complete Guide

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:

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

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:

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:

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

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

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:

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles