Shell and Editor Configuration

Updated

September 12, 2026

Shell and Editor Configuration

Copying .zshrc snippets between laptops is how aliases drift. The boring default is: typed programs.* modules for the shell, prompt, git, and editor, with plugins fetched by Nix instead of a plugin manager that writes into $HOME.

Mental model

Home Manager modules know the file format. programs.git writes ~/.config/git/config. programs.starship writes ~/.config/starship.toml. You set Nix attributes; you do not maintain a parallel dotfile unless the module is missing.

Module Replaces
programs.bash / zsh / fish Hand-written rc files for aliases, history, completions
programs.starship Prompt theme copied from a gist
programs.git git config --global
programs.neovim / programs.vim Plugin managers (vim-plug curling GitHub on first launch)

Enable the shell you actually log into. Enabling programs.zsh does not change /etc/passwd. The login shell is a NixOS users.users.deskadmin.shell = pkgs.zsh; (and programs.zsh.enable = true; on NixOS for /etc/zshrc). On a foreign distro, chsh once after Home Manager installed zsh in the profile.

programs.neovim.plugins takes nixpkgs vimPlugins, not Plug 'foo' that curls GitHub on first launch. A init.lua via xdg.configFile."nvim/init.lua" is fine when the module’s extraLuaConfig is too small — still no packer sync at runtime.

Worked examples

Case 1: Git as a module, not a ritual

Save as shell_editor.nix:

# shell_editor.nix
{ pkgs, ... }:

{
  programs.git = {
    enable = true;
    userName = "Desk Engineer";
    userEmail = "desk@corp.internal";
    extraConfig = {
      init.defaultBranch = "main";
      pull.rebase = true;
      rerere.enabled = true;
    };
    aliases = {
      st = "status";
      co = "checkout";
    };
    ignores = [ "result" ".direnv/" ];
  };
}
home-manager switch
git config --global --list | head

Output:

user.name=Desk Engineer
user.email=desk@corp.internal
init.defaultbranch=main
alias.st=status

git config --global user.email in a terminal will write a different file or fight the symlink. Stop doing that.

Case 2: Starship prompt

# starship.nix
{
  programs.starship = {
    enable = true;
    enableBashIntegration = true;
    settings = {
      add_newline = false;
      format = "$directory$git_branch$character";
      character.success_symbol = "[>](bold green)";
    };
  };

  programs.bash.enable = true;
}

Starship’s native format is TOML. Home Manager accepts a Nix attrset and renders TOML. You do not keep a second starship.toml in git unless you source it wholesale with .source.

Case 3: Bash aliases and history

# bash.nix
{
  programs.bash = {
    enable = true;
    historyControl = [ "ignoredups" "erasedups" ];
    historySize = 10000;
    shellAliases = {
      ll = "ls -l";
      gs = "git status";
      ".." = "cd ..";
    };
    bashrcExtra = ''
      # rare exceptions that have no module option
      export DESK_WINDOW=A
    '';
  };
}

shellAliases is the boring place for aliases. bashrcExtra is an escape hatch; if it grows past twenty lines, look for a module.

Case 4: Neovim with plugins from nixpkgs

# nvim.nix
{ pkgs, ... }:

{
  programs.neovim = {
    enable = true;
    viAlias = true;
    vimAlias = true;
    extraConfig = ''
      set number
      set relativenumber
    '';
    plugins = with pkgs.vimPlugins; [
      vim-nix
      vim-commentary
    ];
  };
}

Plugins are store paths. There is no ~/.local/share/nvim/site/pack filled by a job on first open. CI can nvim --headless +q without a network.

Case 5: Login shell versus module

On NixOS:

# in configuration.nix
users.users.deskadmin.shell = pkgs.bashInteractive;

Home Manager’s programs.bash.enable = true writes ~/.bashrc. It does not change the passwd shell. If you enable programs.zsh but leave shell = pkgs.bashInteractive, you will wonder why zsh settings never run.

The trap

The trap is Oh My Zsh / a plugin manager plus Home Manager. OMZ clones repos into $HOME on the next login and overwrites what Home Manager just linked. Pick one. The boring pick is Home Manager + nixpkgs plugins.

The other trap is putting secrets in extraConfig (smtp.password, GitHub tokens). Those land in the world-readable store. Use sops-nix / agenix (secrets part) and point the module at the decrypted path.

The boring rule

  • If a programs.<name> module exists, use it.
  • Git identity and aliases live in programs.git, not in a one-off git config.
  • Plugins come from pkgs.vimPlugins / pkgs.zshPlugins, not from a curl-on-login manager.
  • Set the login shell in NixOS (or chsh once on a foreign distro).
  • No secrets in module extraConfig.

Try this

  1. Add programs.git.aliases.lg = "log --oneline --graph -20";, switch, run git lg.
  2. readlink -f ~/.config/git/config and confirm a store path.
  3. Enable starship, open a new bash, and confirm echo $STARSHIP_SESSION_KEY is set (or that the prompt changed). Disable and switch to compare.
  4. List pkgs.vimPlugins in a nix repl (:l <nixpkgs> then pkgs.vimPlugins.vim-nix) to see that the plugin is an ordinary derivation.