Just Another Geek

I am blogging about Information Security since 2003

16 Dec 2022

HOWTO build a robust CLI application using Cookiecutter, click and Nix

I don’t know about you but usually, when I develop a quick&dirty tool, I usually create one from scratch in in my ~/projects folder and then I create a symbolic link within one of my $PATH entries.

Eventually, it will stop working because of broken dependencies, repository renamed or worst case: Migrating to a new workstation without bulk importing my $HOME folder.

Today, I needed to create a new tool and resisted copy/pasting and joggling around as usual. Instead, I listened to James Clear wisdom:

If you’re having trouble changing your habits, the problem isn’t you. The problem is your system. Bad habits repeat themselves not because you don’t want to change, but because you have the wrong system for change.

And the reality is that I had never invested having “something that works”.

To make it worked, I did three things:

  • I finally took the time to learn the minimum vital about setuptools and deploying console applications.

  • I created a cookiecutter template that includes most of the usual suspects (and the new setuptool’s skill I learned today).
    From now on, when I want to create a new Python CLI tool, it boils down to:

    $ cd ~/projects/
    $ cookiecutter https://github.com/nbareil/cookiecutter-py-cli
    project_name [Dummy project name]: test tote
    project_slug [test_tote]:
    $ ls test_tote/
    default.nix  pyproject.toml  test_tote.py
  • I added the new tool into my Nix home-manager setup:

    { config, lib, pkgs, ... }:
      til-manager = pkgs.python3.pkgs.buildPythonPackage rec {
      pname = "til-manager";
      version = "0.0.1";
      format = "pyproject";
      src = pkgs.fetchgit {
          url = "https://github.com/nbareil/til-manager.git";
          rev = "03f0fba023e931dc31b5a793231ab2ea4679e464";
          sha256 = "1g565rkfik16xzk5cf0jwjk97nbj9fpxsg6igaza9ygnx5c05k26";
      buildInputs = [
      propagatedBuildInputs = [
      doCheck = false;
    in {
      home.packages = [

And voila! I now have a functional and sustainable system: I am guaranteed that everything will work on the long term: Dependencies will never be broken, the CLI tool will always be within my $PATH folders as any other application.