Setting up commitlint globally using nix and home-manager

Why enforce commit message conventions?

When your Git commit messages follow a clear and consistent convention, your project’s history becomes much more valuable. Instead of cryptic or vague messages, you get a chronological story that explains not just what changed, but why. This makes it far easier to understand the evolution of your codebase, whether you’re debugging an issue, reviewing old decisions, or onboarding new contributors.

For example, it’s common to see messages like “fix issue with login” or “set correct timeout,” which don’t explain what the actual problem was or why the previous value was incorrect. Compare that to a message like:

fix(auth): prevent login failure when username contains special characters

Previously, users with special characters in their usernames were unable to log in due to improper input sanitization. This commit updates the authentication logic to correctly handle special characters, ensuring all valid usernames are accepted. This fixes issue #42 and improves the login experience for affected users.

This kind of commit message not only states what was changed, but also explains the context and reasoning behind the change. It makes it much easier to understand the history of your project and the motivations for each decision. This level of detail turns your Git log into a valuable resource, rather than a collection of mysterious notes.

What is commitlint(-rs)?

commitlint is a popular tool that checks your Git commit messages against a set of rules—most commonly, the Conventional Commits specification. By enforcing these rules, commitlint helps teams write clear, meaningful commit messages that improve project history, automate changelogs, and streamline collaboration. It can be integrated into your workflow to automatically reject commits that don’t meet your standards, making it easier to maintain high-quality commit messages across your entire codebase.

While the original commitlint is written in JavaScript, there’s also commitlint-rs, a Rust-based alternative. commitlint-rs offers similar functionality but is easier to install and manage with Nix and home-manager, and is therefore the tool we’ll use in this post.

Note: This setup enables commit message linting for your own environment. It won’t enforce commit conventions for collaborators or anyone else working on the same repositories as you, unless they explicitly set up something similar for themselves. If you wish to enforce this for your team, then consider checking commit messages in your CI pipelines (which I encourage doing).

Why use Nix and home-manager?

One of the biggest advantages of using Nix and home-manager is the ability to define your entire development environment in a single, declarative configuration file. This means you can specify all your tools, settings, and customizations in one place, making your setup reproducible and easy to share or restore. Whether you’re setting up a new machine or keeping your environment consistent across multiple devices, Nix and home-manager ensure that everything is just the way you want it—with minimal manual intervention.

Prerequisites

Before you get started, make sure you have both Nix and home-manager installed and set up on your system. It also helps to have a basic understanding of how to edit your home-manager configuration files.

Installing commitlint with Nix and home-manager

There are three steps to setup this successfully in your environment, to have commitlint-rs lint all your commit messages before your commit goes through.

  1. Install the package in your home environment

Add pkgs.commitlint-rs to home.packages:

  home.packages = [
    pkgs.commitlint-rs
  ]
  1. Setup the commitlint configuration file

There are two ways to add your .commitlintrc.yaml file with home-manager, depending on your preference:

Add the .commitlintrc.yaml file to your home directory using home.file:

home.file = [
    ".commitlintrc.yaml" = {
      source = ./.commitlintrc.yaml;
    };
]

By doing it this way, you can include the local file .commitlintrc.yaml file that you have locally next to your home.nix file.

Or, you can include the contents of the file directly, like this:

home.file = [
    ".commitlintrc.yaml" = {
      text = ''
        # See https://keisukeyamashita.github.io/commitlint-rs/config/default/ for rules
        rules:
          type-empty:
            level: error
          description-empty:
            level: error
          subject-empty:
            level: error
          body-empty:
            level: error
      '';
    };
]
  1. Setup the git hook file

To ensure commitlint runs automatically on every commit, you need to add a commit-msg hook to your global Git hooks directory. Again we’ll do this declaratively by using home.file:

    ".config/git/hooks/commit-msg" = {
      text = ''
        #!/bin/sh
        exec ${pkgs.commitlint-rs}/bin/commitlint --edit "$1"
      '';
      executable = true;
    };
  1. Register the git hook with git

The final piece of the puzzle is to register our new hook with git:

programs = {
  git = {
    hooks = {
      commit-msg = "${config.home.homeDirectory}/.config/git/hooks/commit-msg";
    };
  };
};

Testing your setup

After running home-manager switch (or nixos-rebuild switch, or nix build .. - depending on how you use nix and home-manager), you should now have commitlint available in your environment. You can test it like this:

$ echo "testing" | commitlint
type is empty

$ echo $?
1

If you move into a git repository, and try to commit something with a non-useful message, it can look like this:

$ git commit -m "progress"
type is empty

If you however provide a commit message following your rules, then it’ll work:

$ git commit -m "feat: Always accept 42 as a valid answer"
[main 8380724] feat: Always accept 42 as a valid answer
 1 file changed, 0 insertions(+), 0 deletions(-)

Conclusion

With commitlint-rs managed by Nix and home-manager, you can ensure consistent, high-quality commit messages across all your projects—automatically and effortlessly. This setup is easy to maintain and adapts seamlessly as your workflow evolves.