nixsh

Every shell on every machine, declared.

One declaration renders into fish, bash and zsh — each in its own syntax. What genuinely differs between shells stays per-shell, unabstracted. And the shell binary comes from your system, not from a second copy on PATH.

one declaration, three shells
$ grep EDITOR ~/.config/fish/conf.d/00-nixsh.fish ~/.bashrc
fish:  set -gx EDITOR "hx"
bash:  export EDITOR="hx"

$ command -v fish
/usr/bin/fish        # the system's, not a second copy

How it works

1

Add the input

No nixpkgs of its own — nixsh takes pkgs from your evaluation, so it never adds a second nixpkgs to your closure.

2

Import a backend

homeModules.nixsh writes config. nixosModules.nixsh also installs the shells, because on NixOS the system is nix. systemManagerModules.nixsh publishes package names for a foreign distro's own reconciler.

3

Declare once

Enable the shells you want, set the shared environment, and add per-shell config where the shells genuinely differ.

Features

One environment, three syntaxes

Environment variables and PATH are declared once. fish gets set -gx and fish_add_path; bash and zsh get export. It is the only layer that genuinely translates, so it is the only layer that is shared.

Aliases stay per-shell, on purpose

fish is not POSIX. A generic alias DSL rendering to three syntaxes would render to three syntaxes badly, so aliases, functions and completions are declared per shell and never translated. The restraint is the design.

The binary stays the system's

home-manager's programs.fish.enable installs its own fish, which on a foreign distro lands ahead of /usr/bin/fish — so your login shell and your interactive shell become different builds. nixsh writes config only and never enables it.

Architecture

  nixsh.environment.variables.EDITOR = "hx"
                    |
        +-----------+-----------+
        |           |           |
      fish        bash         zsh
        |           |           |
   set -gx      export      export
        |           |           |
  conf.d/     initExtra   initExtra
  00-nixsh.fish

  binary:  /usr/bin/fish   <- the system's
           (never a second copy on PATH)

Shared where it translates

The environment layer is the only thing abstracted, because the only difference is the export keyword. Everything else is passed through verbatim in the shell's own language.

Config in nix, binaries in the system

On NixOS the system is nix, so the shells are installed. Anywhere else they come from the distro and nixsh writes config only — one shell per machine, not two.

Install

Download binary

Grab the latest release for your platform:

Download from GitHub

Foreign distro (Arch, CachyOS)

nixarch.packages.pacman = config.nixsh.archPackages;

Build from source

git clone https://github.com/julian-corbet/nixsh
cd nixsh
make install

Nix with flakes enabled. No other inputs — nixsh pulls no nixpkgs of its own.

Commands

nixsh.fish.enable = true;Config for fish, written as a conf.d drop-in so it composes with whatever already owns config.fish.
nixsh.environment.variables.EDITOR = "hx";Lands in every enabled shell at once, each in its own syntax.
nixsh.fish.universalVariables.fish_greeting = "";fish owns fish_variables itself, so universal variables are applied idempotently at startup — the one piece of fish state a config file cannot declare.