A complete macOS dev environment managed by Nix. Everything is declarative — one darwin-rebuild switch rebuilds your entire setup from scratch. This guide walks you through creating every file from zero.
┌─────────────────────────────────────────────────────────────┐
│ Ghostty terminal │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ tmux ││
│ │ ┌──────────────────────┬──────────────────────────────┐││
│ │ │ nvim │ opencode (prefix+o) │││
│ │ │ opencode.nvim │ oh-my-opencode agents │││
│ │ │ supermaven │ notifier → tmux status bar │││
│ │ │ Ctrl+. toggle │ /lin → Linear context │││
│ │ ├──────────────────────┤ │││
│ │ │ lazygit (prefix+l) │ │││
│ │ │ Ctrl+a = AI commit │ │││
│ │ └──────────────────────┴──────────────────────────────┘││
│ │ status: λ session │ 1 nvim │ path │ main +2 -1 ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
Worktree workflow:
main session ──wt feature──→ main/feature session (new tmux session + git worktree)
──wt fix────→ main/fix session
──wtl ENG-1─→ main/fix session (from Linear issue, marks In Progress)
- What you're building
- How the workflow works
- Keybinding quick reference
- Prerequisites
- Install Nix
- Create your config repo
- Infrastructure files
- System modules
- Shell & CLI modules
- Dev tool modules
- Editor module (Neovim)
- Terminal module (Ghostty)
- Custom overlay packages (tmux scripts)
- First build!
- Set up API keys
- Adding new apps
- Day-to-day commands
After following this guide, you'll have:
- Fish shell with git aliases (
g,ga,gc,gp), PR workflow (ghpc,ghpm), and project tools (ds,pj,envsource) - tmux with Ctrl+Space prefix, git status bar, notification system, and one-key panes for OpenCode and Lazygit
- OpenCode AI coding assistant with oh-my-opencode agents, tmux notifications, and Linear issue integration
- Git worktrees (
wt) that create isolated tmux sessions per branch — switch contexts instantly - Linear integration (
wtl) that creates worktrees from issues, marking them In Progress - Neovim with LSP for 16 languages, Telescope, opencode.nvim, Supermaven AI autocomplete
- Lazygit with Ctrl+a to generate commits via AI, and PR shortcuts
- Ghostty terminal that auto-launches tmux with Flexoki Light theme
- Declarative Homebrew — GUI apps managed via Nix (anything not declared gets removed on rebuild)
- Touch ID for sudo (even inside tmux)
Everything is managed by Nix. Change a .nix file, run one command, done.
- macOS on Apple Silicon (M1/M2/M3/M4)
- An Anthropic API key (for OpenCode)
- A Supermaven account (free tier, for AI autocomplete in nvim — optional)
- A 1Password account (for SSH key signing — or you can remove the signing config from git.nix)
# Install Nix (the package manager)
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
# Install Rosetta (needed for some Homebrew x86 packages)
softwareupdate --install-rosettaClose and reopen your terminal after installing Nix.
Run these commands to set up the directory structure. Every file you'll create is listed here.
mkdir -p ~/nix-config && cd ~/nix-config
git init
# Infrastructure
mkdir -p lib
mkdir -p overlay/pkgs/{flexoki-tmux,tmux-extras,tmux-nerd-font-window-name}
# Modules
mkdir -p modules/{hosts,users}
mkdir -p modules/system
mkdir -p modules/darwin
mkdir -p modules/fonts
mkdir -p modules/cli
mkdir -p modules/dev
mkdir -p modules/editor
mkdir -p modules/terminal
mkdir -p modules/browserNow follow each section below and create every file. The comment at the top of each code block shows the file path.
This declares all your dependencies (nixpkgs, home-manager, nix-darwin, Homebrew) and wires them together.
# flake.nix
{
description = "My nix-darwin configuration";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
utils.url = "github:numtide/flake-utils";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
darwin = {
url = "github:lnl7/nix-darwin/master";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-homebrew = {
url = "github:zhaofengli/nix-homebrew";
inputs.nixpkgs.follows = "nixpkgs";
};
# Homebrew taps (pinned via flake, not mutable)
homebrew-core = { url = "github:homebrew/homebrew-core"; flake = false; };
homebrew-cask = { url = "github:homebrew/homebrew-cask"; flake = false; };
homebrew-bundle = { url = "github:homebrew/homebrew-bundle"; flake = false; };
};
outputs = { nixpkgs, utils, ... }@inputs:
let
overlay = import ./overlay/overlay.nix;
overlays = [ overlay ];
mkDarwinHost = import ./lib/mkDarwinHost.nix { inherit inputs overlays; };
in
{
inherit overlay overlays;
darwinConfigurations = {
# CHANGE THIS: replace "mymac" with your hostname
# Run: scutil --get LocalHostName
mymac = mkDarwinHost ./modules/hosts/mymac.nix;
};
}
// utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs { inherit system overlays; };
in {
packages = pkgs;
devShell = pkgs.mkShell {
buildInputs = with pkgs; [ nil statix nixfmt-rfc-style shellcheck nodePackages.prettier pre-commit ];
shellHook = ''pre-commit install -f'';
};
}
);
}# lib/mkDarwinHost.nix
{ inputs, overlays }:
let
system = "aarch64-darwin";
inherit (inputs) nixpkgs home-manager darwin nix-homebrew;
in
host:
darwin.lib.darwinSystem {
inherit system;
specialArgs = { inherit inputs system; };
modules = [
home-manager.darwinModules.home-manager
nix-homebrew.darwinModules.nix-homebrew
host
{
nixpkgs = { inherit overlays; config.allowUnfree = true; };
home-manager = { useGlobalPkgs = true; };
}
];
}# lib/mkUserImports.nix
{ lib, inputs, ... }@args:
let inherit (inputs) home-manager;
in
username: imports:
lib.forEach imports (imp:
import imp (args // { inherit (home-manager) lib; inherit username; })
)# overlay/overlay.nix
self: super: { } // import ./pkgs/pkgs.nix self# overlay/pkgs/pkgs.nix
pkgs: {
flexoki-tmux = pkgs.callPackage ./flexoki-tmux/flexoki-tmux.nix { };
tmux-extras = pkgs.callPackage ./tmux-extras/tmux-extras.nix { };
tmux-nerd-font-window-name =
pkgs.callPackage ./tmux-nerd-font-window-name/tmux-nerd-font-window-name.nix { };
}The overlay packages (flexoki-tmux, tmux-extras, tmux-nerd-font-window-name) are in section 11 below.
# modules/hosts/mymac.nix
# CHANGE THIS: rename the file to match your hostname
_: {
networking.hostName = "mymac"; # CHANGE THIS: run scutil --get LocalHostName
imports = [
../users/mymac-yourname.nix # CHANGE THIS: point to your user file
../darwin/darwin-default.nix
../darwin/sudo-touchid.nix
../darwin/watcher.nix
../fonts/caskaydia-cove.nix
../system/nix.nix
../system/pkg-config.nix
../cli/wget.nix
../cli/unrar.nix
];
}# modules/users/mymac-yourname.nix
# CHANGE THIS: rename the file
{ pkgs, ... }@args:
let
mkUserImports = import ../../lib/mkUserImports.nix args;
username = "yourname"; # CHANGE THIS: run whoami
in
{
imports = mkUserImports username [
# System
../system/home.nix
../darwin/primary-user.nix
../darwin/homebrew.nix
# Shell & CLI
../cli/fish.nix
../cli/starship.nix
../cli/direnv.nix
../cli/tmux.nix
../cli/zoxide.nix
../cli/bat.nix
../cli/eza.nix
../cli/fzf.nix
../cli/ripgrep.nix
# Dev tools
../dev/git.nix
../dev/lazygit.nix
../dev/opencode.nix
../dev/claude-code.nix
# Editor
../editor/nvim.nix
# Browser
../browser/firefox.nix
# Terminal
../terminal/ghostty.nix
# ── Add your own below ──
# Create a file like ../chat/slack.nix with: _: { homebrew.casks = [ "slack" ]; }
# Then add the import here and rebuild.
];
}# modules/system/nix.nix
{ pkgs, ... }:
{
nix = {
package = pkgs.nixVersions.latest;
extraOptions = ''
experimental-features = nix-command flakes
'';
};
}# modules/system/home.nix
{ username, ... }:
let homeDirectory = "/Users/${username}";
in
{
users.users.${username}.home = homeDirectory;
home-manager.users.${username} = {
home = { inherit homeDirectory username; stateVersion = "25.05"; };
};
}# modules/system/pkg-config.nix
{ pkgs, ... }: { environment.systemPackages = with pkgs; [ pkg-config ]; }# modules/darwin/primary-user.nix
{ username, ... }: { system.primaryUser = username; }# modules/darwin/darwin-default.nix — macOS trackpad, dock, finder settings
_: {
system.stateVersion = 5;
system.defaults = {
trackpad = { Clicking = true; TrackpadThreeFingerDrag = true; };
dock = { wvous-br-corner = 1; mru-spaces = false; autohide = true; show-recents = false; };
finder = { AppleShowAllExtensions = true; AppleShowAllFiles = true; };
menuExtraClock = { ShowAMPM = true; };
};
}# modules/darwin/sudo-touchid.nix — Touch ID for sudo, even inside tmux
{ pkgs, ... }:
{
environment.systemPackages = with pkgs; [ pam-reattach ];
environment.etc = {
"pam.d/sudo_local" = {
text = ''
auth optional ${pkgs.pam-reattach}/lib/pam/pam_reattach.so
auth sufficient pam_tid.so
'';
};
};
}# modules/darwin/watcher.nix — increase file watcher limits
{ pkgs, ... }:
{
environment.etc = {
"sysctl.conf" = {
enable = true;
text = ''
kern.maxfiles=131072
kern.maxfilesperproc=65536
'';
};
};
}# modules/darwin/homebrew.nix — declarative Homebrew
{ username, inputs, config, ... }:
let inherit (inputs) homebrew-core homebrew-cask homebrew-bundle;
in
{
nix-homebrew = {
enable = true;
enableRosetta = true;
user = username;
taps = {
"homebrew/homebrew-core" = homebrew-core;
"homebrew/homebrew-cask" = homebrew-cask;
"homebrew/homebrew-bundle" = homebrew-bundle;
};
mutableTaps = false;
};
homebrew = {
enable = true;
# WARNING: any cask NOT declared in your modules will be REMOVED on rebuild
onActivation.cleanup = "zap";
taps = builtins.attrNames config.nix-homebrew.taps;
};
}# modules/fonts/caskaydia-cove.nix
{ pkgs, ... }: { fonts.packages = with pkgs; [ nerd-fonts.caskaydia-cove ]; }# modules/browser/firefox.nix
_: { homebrew.casks = [ "firefox" ]; }# modules/cli/wget.nix
{ pkgs, ... }: { environment.systemPackages = with pkgs; [ wget ]; }# modules/cli/unrar.nix
{ pkgs, ... }: { environment.systemPackages = with pkgs; [ unrar ]; }This is the big one. It sets Fish as your default shell and defines all the git workflow functions.
# modules/cli/fish.nix
{ username, pkgs, lib, ... }:
{
environment = {
systemPackages = [ pkgs.fish ];
shells = [ pkgs.fish ];
};
users.users.${username} = { shell = pkgs.fish; };
programs.fish.enable = true;
home-manager.users.${username} = {
programs.fish = {
enable = true;
interactiveShellInit = ''
ssh-add --apple-load-keychain 2> /dev/null
set fish_cursor_default block
set fish_cursor_insert line
set -U fish_greeting
fish_add_path -amP /usr/bin
fish_add_path -amP /opt/homebrew/bin
fish_add_path -amP /opt/local/bin
fish_add_path -m /run/current-system/sw/bin
fish_add_path -m /Users/${username}/.nix-profile/bin
'';
shellAliases = {
g = "git";
ga = "git add";
gaa = "git add .";
gb = "git branch";
gc = "git commit";
gp = "git push";
ds = "nix develop . --command $SHELL";
ls = "eza -lag";
cat = "bat";
};
functions = {
# Push + create PR + open in browser
ghpc = "git push && gh pr create --fill $argv && gh pr view --web";
# Merge PR (squash) + pull main + cleanup worktree
ghpm = ''
gh pr merge -s --admin $argv
or return 1
set main_root (git worktree list --porcelain | head -1 | string replace "worktree " "")
set current_root (git rev-parse --show-toplevel)
git -C "$main_root" pull
if test "$main_root" != "$current_root"; and set -q TMUX
set wt_name (basename $current_root)
wtrm --force $wt_name
end
'';
# Push + create + merge in one command
ghpcm = "ghpc $argv && ghpm";
# Jump to project and enter dev shell
pj = "cd $argv; ds";
fish_command_not_found = "__fish_default_command_not_found_handler $argv";
# Checkout branch with auto-pull
gco = ''
set current_branch (git rev-parse --abbrev-ref HEAD)
git checkout $argv; and if not string match -q -- '-*' $argv && test "$current_branch" != "$argv"
git pull
end
'';
# Source a .env file into current shell
envsource = ''
for line in (cat $argv | grep -v '^#' | grep -v '^\s*$')
set item (string trim $line | string replace -r '\s*=\s*' '=' | string split -m 1 '=')
set -gx $item[1] $item[2]
echo \"Exported key $item[1]\"
end
'';
_git_clean_stale_lock = ''
set -l git_dir (git rev-parse --git-dir 2>/dev/null)
or return 0
set -l lock "$git_dir/index.lock"
if test -f "$lock"
if not lsof "$lock" >/dev/null 2>&1
rm -f "$lock"
echo "Removed stale index.lock"
end
end
'';
# ── Git worktree management (see "How the workflow works" section) ──
wt = ''
set git_root (git rev-parse --show-toplevel 2>/dev/null)
or begin; echo "wt: not a git repo"; return 1; end
_git_clean_stale_lock
set name $argv[1]
set branch $argv[2]
set main_root (git worktree list --porcelain | head -1 | string replace "worktree " "")
set repo_name (basename $main_root)
set wt_base (dirname $main_root)
if test -z "$name"
if not set -q TMUX; echo "wt: not in tmux"; return 1; end
set wt_dir "$wt_base/$repo_name.worktrees"
if not test -d "$wt_dir"; or test (count (command ls "$wt_dir" 2>/dev/null)) -eq 0
echo "wt: no worktrees found"; return 1
end
set name (command ls "$wt_dir" | fzf --prompt="worktree> " --height=40%)
or return 1
end
set wt_path "$wt_base/$repo_name.worktrees/$name"
if set -q TMUX
set parent_session (command tmux display-message -p '#{session_name}' | string split -m 1 '/')[1]
set session_name "$parent_session/$name"
if command tmux has-session -t "=$session_name" 2>/dev/null
command tmux switch-client -t "=$session_name"; return 0
end
end
set -l is_new 0
if not test -d "$wt_path"
git fetch origin 2>/dev/null
set base_branch (git rev-parse --abbrev-ref HEAD 2>/dev/null)
if test -z "$base_branch" -o "$base_branch" = "HEAD"
echo "wt: detached HEAD — checkout a branch first"; return 1
end
if test -n "$branch"
if git show-ref --verify --quiet "refs/heads/$branch"
git worktree add "$wt_path" "$branch"
else if git show-ref --verify --quiet "refs/remotes/origin/$branch"
git worktree add --track -b "$branch" "$wt_path" "origin/$branch"
else
git worktree add -b "$branch" "$wt_path" "$base_branch"
end
else
git worktree add -b "$name" "$wt_path" "$base_branch"
end
or begin; echo "wt: failed to create worktree"; return 1; end
echo "Created worktree at $wt_path (from $base_branch)"
direnv allow "$wt_path" 2>/dev/null
set is_new 1
end
if set -q TMUX
command tmux new-session -d -s "$session_name" -c "$wt_path"
if test $is_new -eq 1
set -l setup_file "$wt_base/$repo_name.worktrees/.setup"
if test -f "$setup_file"
command tmux send-keys -t "=$session_name" "sh '$setup_file'" Enter
end
end
command tmux switch-client -t "=$session_name"
else
echo "Not in tmux — run: cd $wt_path && opencode"
end
'';
lin = ''
if test (count $argv) -eq 0
linear issue view
else
linear issue $argv
end
'';
wtl = ''
_git_clean_stale_lock
set issue_id $argv[1]
if test -z "$issue_id"
set -l selection (linear issue list --no-pager 2>/dev/null | fzf --ansi --prompt="issue> " --height=40%)
or return 1
set issue_id (string match -r '[A-Z]+-\d+' -- $selection)
end
set name $argv[2]
if test -z "$name"
read -P "worktree name> " name
or return 1
if test -z "$name"; echo "wtl: name required"; return 1; end
end
if not set -q TMUX; echo "wtl: requires tmux"; return 1; end
set parent_session (command tmux display-message -p '#{session_name}' | string split -m 1 '/')[1]
set session_name "$parent_session/$name"
if command tmux has-session -t "=$session_name" 2>/dev/null
command tmux switch-client -t "=$session_name"; return 0
end
set original_branch (git rev-parse --abbrev-ref HEAD)
linear issue start $issue_id
or return 1
set branch_name (git rev-parse --abbrev-ref HEAD)
git checkout $original_branch 2>/dev/null
wt $name $branch_name
'';
wtls = "git worktree list $argv";
wtrm = ''
set git_root (git rev-parse --show-toplevel 2>/dev/null)
or begin; echo "wtrm: not a git repo"; return 1; end
set -l force 0
set -l args
for arg in $argv
if test "$arg" = "--force" -o "$arg" = "-f"; set force 1
else; set -a args $arg; end
end
set name $args[1]
set main_root (git worktree list --porcelain | head -1 | string replace "worktree " "")
set repo_name (basename $main_root)
set wt_base (dirname $main_root)
set -l auto_name 0
if test -z "$name"
if set -q TMUX
set -l current (command tmux display-message -p '#{session_name}')
if string match -q '*/*' -- "$current"
set name (string split -m 1 '/' -- "$current")[2]
set auto_name 1
end
end
end
if test -z "$name"
set wt_dir "$wt_base/$repo_name.worktrees"
if not test -d "$wt_dir"; or test (count (command ls "$wt_dir" 2>/dev/null)) -eq 0
echo "wtrm: no worktrees found"; return 1
end
set name (command ls "$wt_dir" | fzf --prompt="remove> " --height=40%)
or return 1
end
set wt_path "$wt_base/$repo_name.worktrees/$name"
if not test -d "$wt_path"; echo "wtrm: no worktree found for '$name'"; return 1; end
if test $auto_name -eq 1
read -P "wtrm: remove worktree '$name'? [y/N] " confirm
if not string match -qi 'y' -- "$confirm"; return 0; end
end
set -l self_rm 0; set -l current_session ""; set -l parent_session ""
if set -q TMUX
set current_session (command tmux display-message -p '#{session_name}')
set -l target_session (command tmux list-sessions -F '#{session_name}' | grep "/$name\$" | head -1)
if test -n "$target_session" -a "$current_session" = "$target_session"
set self_rm 1
set parent_session (string split -m 1 '/' -- "$current_session")[1]
end
end
if test $self_rm -eq 1
if not command tmux has-session -t "=$parent_session" 2>/dev/null
set parent_session (command tmux list-sessions -F '#{session_name}' | grep -v "^$current_session\$" | head -1)
if test -z "$parent_session"; echo "wtrm: no other session to switch to"; return 1; end
end
set -l branch (git -C "$wt_path" rev-parse --abbrev-ref HEAD 2>/dev/null)
if test $force -eq 0
if test -n "$(git -C "$wt_path" status --porcelain 2>/dev/null)"
echo "wtrm: worktree has uncommitted changes — use 'wtrm --force' to force"; return 1
end
end
set -l cleanup "tmux kill-session -t '=$current_session'"
if test $force -eq 1
set cleanup "$cleanup; git -C '$main_root' worktree remove --force '$wt_path'"
else
set cleanup "$cleanup; git -C '$main_root' worktree remove '$wt_path'"
end
if test -n "$branch" -a "$branch" != "HEAD"
if test $force -eq 1; set cleanup "$cleanup; git -C '$main_root' branch -D '$branch' 2>/dev/null"
else; set cleanup "$cleanup; git -C '$main_root' branch -d '$branch' 2>/dev/null"; end
end
command tmux switch-client -t "=$parent_session"
command tmux run-shell -b "$cleanup"
else
if set -q TMUX
set -l target_session (command tmux list-sessions -F '#{session_name}' | grep "/$name\$" | head -1)
if test -n "$target_session"; command tmux kill-session -t "=$target_session"; end
end
set -l branch (git -C "$wt_path" rev-parse --abbrev-ref HEAD 2>/dev/null)
if test $force -eq 1; git worktree remove --force "$wt_path"
else; git worktree remove "$wt_path"; end
or begin; echo "wtrm: worktree has changes — use 'wtrm --force $name' to force"; return 1; end
if test -n "$branch" -a "$branch" != "HEAD"
if test $force -eq 1; git branch -D "$branch" 2>/dev/null
else; git branch -d "$branch" 2>/dev/null; end
and echo "Removed worktree '$name' and branch '$branch'"
or echo "Removed worktree '$name' (branch '$branch' not fully merged)"
else; echo "Removed worktree '$name'"; end
end
'';
};
shellInit = ''
complete -f -c gco -a '(git branch --all | string replace -r "^[\*\s]+" "" | string replace -r "^remotes/" "")'
complete -f -c wt -n "test (count (commandline -opc)) -eq 1" -a '(
set -l mr (git worktree list --porcelain 2>/dev/null | head -1 | string replace "worktree " "")
set -l rn (basename $mr 2>/dev/null)
set -l wd (dirname $mr 2>/dev/null)/$rn.worktrees
test -d $wd 2>/dev/null; and command ls $wd 2>/dev/null
)'
complete -f -c wt -n "test (count (commandline -opc)) -eq 2" -a '(git branch -a --format="%(refname:short)" 2>/dev/null | string replace -r "^origin/" "" | sort -u | grep -v "^HEAD")'
complete -f -c lin -n "test (count (commandline -opc)) -eq 1" -a "view list start create pr"
complete -f -c wtrm -l force -s f -d "Force remove even with uncommitted changes"
complete -f -c wtrm -a '(
set -l mr (git worktree list --porcelain 2>/dev/null | head -1 | string replace "worktree " "")
set -l rn (basename $mr 2>/dev/null)
set -l wd (dirname $mr 2>/dev/null)/$rn.worktrees
test -d $wd 2>/dev/null; and command ls $wd 2>/dev/null
)'
'';
};
};
}# modules/cli/starship.nix — prompt with worktree awareness
{ username, ... }:
{
home-manager.users.${username} = {
programs.starship = {
enable = true;
settings = {
format = "\${custom.worktree}$all";
character = {
success_symbol = "[λ](bold green)";
error_symbol = "[λ](bold red)";
vicmd_symbol = "[λ](bold blue)";
};
nix_shell = { format = "via [$symbol]($style) "; symbol = " "; };
custom.worktree = {
command = "basename (dirname (git rev-parse --show-toplevel)) | string replace '.worktrees' ''";
when = "string match -q '*.worktrees*' (git rev-parse --show-toplevel 2>/dev/null)";
format = "[$output/]()";
shell = [ "fish" ];
};
aws.disabled = true;
gcloud.disabled = true;
};
};
};
}# modules/cli/direnv.nix
{ username, ... }:
{ home-manager.users.${username} = { programs.direnv = { enable = true; nix-direnv.enable = true; }; }; }# modules/cli/bat.nix
{ username, ... }:
{ home-manager.users.${username} = { programs.bat = { enable = true; }; }; }# modules/cli/eza.nix
{ username, ... }:
{ home-manager.users.${username} = { programs.eza = { enable = true; }; }; }# modules/cli/fzf.nix
{ username, pkgs, ... }:
{ home-manager.users.${username} = { home.packages = with pkgs; [ fzf ]; }; }# modules/cli/ripgrep.nix
{ username, pkgs, ... }:
{ home-manager.users.${username} = { home.packages = with pkgs; [ ripgrep ]; }; }# modules/cli/zoxide.nix — smart cd that learns your directories
{ username, pkgs, ... }:
{
home-manager.users.${username} = {
programs.zoxide = {
enable = true;
enableFishIntegration = true;
options = [ "--cmd=cd" ]; # replaces cd with zoxide
};
};
}# modules/cli/tmux.nix
{ username, pkgs, ... }:
let inherit (pkgs) tmux-extras;
in
{
home-manager.users.${username} = {
xdg.configFile."tmux/tmux-nerd-font-window-name.yml".text = ''
config:
fallback-icon: "?"
show-name: false
always-show-fallback-name: true
icons:
.opencode-wrapp: ""
task: ""
'';
programs.tmux = {
enable = true;
shell = "${pkgs.fish}/bin/fish";
prefix = "C-space";
terminal = "screen-256color";
keyMode = "vi";
mouse = true;
baseIndex = 1;
historyLimit = 50000;
sensibleOnTop = false;
extraConfig = ''
set -g renumber-windows on
set -g escape-time 1
set -g display-time 4000
set -g status-interval 5
set -g focus-events on
setw -g aggressive-resize on
set -ga terminal-overrides ",*-256color*:Tc"
set -g extended-keys on
set -g allow-passthrough on
bind-key R source-file ~/.config/tmux/tmux.conf \; display-message "Config reloaded"
set -g status-right "#(${tmux-extras}/bin/tmux-status-right #{pane_current_path})"
set -g status-right-length 200
bind-key '?' display-popup -w 64 -h 80% -E "${tmux-extras}/bin/tmux-cheatsheet"
bind-key 'n' display-popup -w 60 -h 20 -E "${tmux-extras}/bin/tmux-notify-panel"
bind-key 'N' run-shell "${tmux-extras}/bin/tmux-notify goto"
# Open opencode / lazygit in a pane at nearest git root
bind-key o run-shell 'tmux split-window -h -c "$(${tmux-extras}/bin/tmux-git-root-path "#{pane_current_path}")" opencode'
bind-key l run-shell 'tmux split-window -h -c "$(${tmux-extras}/bin/tmux-git-root-path "#{pane_current_path}")" lazygit'
bind-key c run-shell 'tmux new-window -c "$(${tmux-extras}/bin/tmux-git-root-path "#{pane_current_path}")"'
bind-key '"' split-window -c "#{pane_current_path}"
bind-key % split-window -h -c "#{pane_current_path}"
bind-key s choose-tree -sZO name
bind-key 'g' run-shell "${tmux-extras}/bin/tmux-group"
bind-key 'G' run-shell "${tmux-extras}/bin/tmux-ungroup"
bind-key 'C-R' run-shell "${tmux-extras}/bin/tmux-remote toggle"
'';
plugins = with pkgs; [
{ plugin = flexoki-tmux; }
tmuxPlugins.better-mouse-mode
{ plugin = tmux-nerd-font-window-name; }
{
plugin = tmuxPlugins.resurrect;
extraConfig = ''
set -g @resurrect-capture-pane-contents 'on'
set -g @resurrect-strategy-nvim 'session'
set -g @resurrect-restore-cwd 'on'
'';
}
{
plugin = tmuxPlugins.continuum;
extraConfig = ''
set -g @continuum-restore 'on'
set -g @continuum-save-interval '10'
'';
}
];
};
};
}# modules/dev/git.nix
{ username, pkgs, ... }:
{
home-manager.users.${username} = {
programs.git = {
enable = true;
lfs.enable = true;
package = pkgs.git;
settings = {
user = {
email = "you@example.com"; # CHANGE THIS
name = "Your Name"; # CHANGE THIS
signingkey = "ssh-ed25519 AAAA..."; # CHANGE THIS: your SSH public key
};
gpg.format = "ssh";
"gpg \"ssh\"" = {
program = "/Applications/1Password.app/Contents/MacOS/op-ssh-sign";
};
commit.gpgsign = true;
init.defaultBranch = "main";
pull.rebase = false;
push.autoSetupRemote = true;
core.ignorecase = false;
};
};
# If you don't use 1Password for signing, remove the gpg, "gpg \"ssh\"", and commit.gpgsign lines.
home.packages = with pkgs; [ gh ];
programs.gh = { enable = true; settings.git_protocol = "ssh"; };
};
}# modules/dev/opencode.nix
{ username, pkgs, ... }:
let
jsonFormat = pkgs.formats.json { };
inherit (pkgs) tmux-extras;
in
{
home-manager.users.${username} = {
programs.opencode = {
enable = true;
settings = {
theme = "flexoki";
plugin = [
"@simonwjackson/opencode-direnv"
"@mohak34/opencode-notifier@latest"
"@kdcokenny/opencode-worktree@latest"
"oh-my-opencode@latest"
];
command = {
lin = {
template = "Here is the Linear issue for this branch:\n\n!`linear issue view $ARGUMENTS`\n\nSummarize the issue and ask what I want to work on.";
description = "Load Linear issue context";
};
};
permission = { external_directory = "allow"; };
};
};
xdg.configFile."opencode/oh-my-opencode.json".source = jsonFormat.generate "oh-my-opencode.json" {
"$schema" = "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json";
agents = {
build = { model = "anthropic/claude-opus-4-6"; };
sisyphus = { model = "anthropic/claude-opus-4-6"; variant = "max"; };
oracle = { model = "anthropic/claude-opus-4-6"; variant = "max"; };
explore = { model = "anthropic/claude-haiku-4-5"; };
multimodal-looker = { model = "opencode/glm-4.7-free"; };
prometheus = { model = "anthropic/claude-opus-4-6"; variant = "max"; };
metis = { model = "anthropic/claude-opus-4-6"; variant = "max"; };
momus = { model = "anthropic/claude-opus-4-6"; variant = "max"; };
atlas = { model = "anthropic/claude-sonnet-4-5"; };
sisyphus-junior = { model = "anthropic/claude-sonnet-4-6"; };
};
categories = {
visual-engineering = { model = "anthropic/claude-opus-4-6"; variant = "max"; };
ultrabrain = { model = "anthropic/claude-opus-4-6"; variant = "max"; };
quick = { model = "anthropic/claude-haiku-4-5"; };
unspecified-low = { model = "anthropic/claude-sonnet-4-5"; };
unspecified-high = { model = "anthropic/claude-opus-4-6"; variant = "max"; };
writing = { model = "anthropic/claude-sonnet-4-5"; };
};
};
xdg.configFile."opencode/opencode-notifier.json".source =
jsonFormat.generate "opencode-notifier.json" {
sound = true;
notification = false;
suppressWhenFocused = false;
command = {
enabled = true;
path = "${tmux-extras}/bin/tmux-notify";
args = [ "add" "--event" "{event}" "{message}" ];
minDuration = 0;
};
};
};
}# modules/dev/lazygit.nix
{ username, pkgs, ... }:
{
home-manager.users.${username} = {
programs.lazygit = {
enable = true;
settings = {
customCommands = [
{ key = "<c-a>"; context = "files";
command = ''opencode run "Look at the staged changes and create a commit following conventional commit conventions. Just commit directly."'';
output = "terminal"; description = "Generate commit with OpenCode"; }
{ key = "O"; context = "localBranches";
command = "git push && gh pr create --web";
description = "Create PR (push + open)"; output = "log"; loadingText = "Creating PR..."; }
{ key = "<c-o>"; context = "localBranches";
command = ''fish -c "ghpc"''; description = "Create PR (push + fill + open)"; output = "log"; }
{ key = "<c-x>"; context = "localBranches";
command = ''fish -c "ghpm"''; description = "Merge PR (squash)"; output = "log"; }
{ key = "<c-p>"; context = "localBranches";
command = ''fish -c "ghpcm"''; description = "Create + Merge PR"; output = "terminal"; }
];
gui = {
nerdFontsVersion = "3";
theme = {
activeBorderColor = [ "#205EA6" "bold" ]; inactiveBorderColor = [ "#CECDC3" ];
optionsTextColor = [ "#205EA6" ]; selectedLineBgColor = [ "#E6E4D9" ];
selectedRangeBgColor = [ "#DAD8CE" ]; cherryPickedCommitBgColor = [ "#24837B" ];
cherryPickedCommitFgColor = [ "#FFFCF0" ]; unstagedChangesColor = [ "#AF3029" ];
defaultFgColor = [ "#100F0F" ];
};
};
};
};
};
}# modules/dev/claude-code.nix
{ username, pkgs, ... }:
{ home-manager.users.${username} = { home.packages = with pkgs; [ claude-code ]; }; }The Neovim config is 1000+ lines — too large to inline here. See the nvim.nix file in this gist. Copy it to modules/editor/nvim.nix.
What it includes:
- LSP for 16 languages (Lua, Nix, TypeScript, Python, Go, Rust, Dart, Elixir, Tailwind, HTML, CSS, JSON, Bash, YAML, TOML, Markdown) — all installed via Nix, no Mason needed
- Formatters (stylua, nixfmt, prettierd, black, gofumpt, rustfmt)
- Plugins: Telescope, which-key, Treesitter, nvim-cmp, gitsigns, neogit, diffview, oil, supermaven, opencode.nvim
- Flexoki Light theme
- VS Code-like keybindings (Cmd+P, Cmd+Shift+F, Alt+j/k)
- Built-in cheatsheet popup (Space+?)
OpenCode keybindings in Neovim:
| Key | Action |
|---|---|
Ctrl+. |
Toggle OpenCode TUI |
Ctrl+a |
Ask OpenCode about selection/cursor |
Ctrl+x |
OpenCode actions menu |
go / goo |
Send range/line to OpenCode |
Space oa |
Toggle OpenCode |
# modules/terminal/ghostty.nix
{ username, pkgs, ... }:
let inherit (pkgs) tmux-extras;
in
{
home-manager.users.${username} = {
programs.ghostty = {
enable = true;
enableFishIntegration = true;
package = pkgs.ghostty-bin;
settings = {
command = "${tmux-extras}/bin/tmux-attach"; # Auto-launch tmux on open
theme = "Flexoki Light";
font-family = "CaskaydiaCove Nerd Font";
font-family-bold = "CaskaydiaCove Nerd Font";
font-family-italic = "auto";
font-family-bold-italic = "auto";
font-size = 12;
cursor-style = "bar";
cursor-style-blink = true;
adjust-cursor-thickness = 2;
macos-option-as-alt = true;
macos-titlebar-style = "transparent";
window-theme = "auto";
gtk-titlebar = false;
window-padding-x = 8;
window-padding-y = 8;
keybind = [
"super+p=text:\\x1b[80;6u" # Cmd+P → Telescope find_files
"super+shift+f=text:\\x1b[70;6u" # Cmd+Shift+F → Telescope live_grep
];
};
};
};
}The overlay has three custom packages. The shell scripts and Nix derivations for all three are included as separate files in this gist. Copy them to the paths shown below.
Flexoki tmux theme — overlay/pkgs/flexoki-tmux/:
flexoki-tmux.nix(from this gist)flexoki-bar.conf(from this gist)flexoki.tmux(from this gist)
tmux-extras — overlay/pkgs/tmux-extras/:
tmux-extras.nix(from this gist)- All
.shfiles from this gist (tmux-attach.sh, git-status.sh, cheatsheet.sh, etc.)
tmux-nerd-font-window-name — overlay/pkgs/tmux-nerd-font-window-name/:
tmux-nerd-font-window-name.nix(from this gist)
| Script | Purpose |
|---|---|
tmux-attach |
Attach to most recent session or create main |
tmux-git-status |
Git branch + changes/insertions/deletions in status bar |
tmux-git-root-path |
Find nearest git root for window/pane commands |
tmux-path-widget |
Current path display in status bar |
tmux-status-right |
Compose all widgets into the right side of the status bar |
tmux-cheatsheet |
Scrollable keybinding reference popup |
tmux-notify |
Notification state manager (add/dismiss/list/goto) |
tmux-notify-panel |
Interactive floating notification panel |
tmux-notify-widget |
Notification count badge (bell icon) in status bar |
tmux-group / tmux-ungroup |
Multi-monitor grouped sessions |
tmux-remote / tmux-remote-widget |
Remote access mode (SSH + battery saving) |
tmux-battery-widget |
Battery level in status bar (remote mode) |
OpenCode finishes a task
→ opencode-notifier plugin fires
→ tmux-notify add --event complete "Task done: ..."
→ stored in /tmp/tmux-notifications.json
→ tmux status bar shows 1
→ prefix+n opens panel → Enter jumps to the source window
Make sure you've created every file above, then:
cd ~/nix-config
# Commit everything (nix flakes require a git repo)
git add -A
git commit -m "initial nix-darwin config"
# First build — this takes a while (downloads everything)
sudo nix run nix-darwin \
--extra-experimental-features nix-command \
--extra-experimental-features flakes \
-- switch --flake .#mymac
# All subsequent rebuilds are just:
sudo darwin-rebuild switch --flake .#mymacOpen Ghostty. It auto-launches tmux. You're in.
# OpenCode needs an Anthropic API key
export ANTHROPIC_API_KEY="sk-ant-..."
# Add to your shell config permanently:
# In fish: set -Ux ANTHROPIC_API_KEY "sk-ant-..."
# First run of OpenCode (inside tmux):
opencode
# It will prompt for the API key if not set
# GitHub CLI login (for ghpc, ghpm, PR commands):
gh auth loginGit worktrees let you have multiple branches checked out in separate directories. Combined with tmux, each worktree gets its own session.
~/projects/
my-app/ # Main checkout (main branch)
my-app.worktrees/
feature-auth/ # Worktree: feature-auth branch
fix-bug/ # Worktree: fix-bug branch
.setup # Optional: runs on new worktree creation
| Command | What it does |
|---|---|
wt |
FZF picker to switch to an existing worktree session |
wt feature |
Create worktree + tmux session, or switch if exists |
wt fix existing-branch |
Create worktree from existing branch |
wtls |
List all worktrees |
wtrm |
Remove worktree + session + branch (FZF or auto-detect) |
wtrm --force name |
Force remove with uncommitted changes |
Typical flow:
wt feature-auth→ creates worktree + switches to tmux sessionmy-app/feature-auth- Code, commit,
ghpc→ pushes + creates PR + opens browser ghpm→ merges PR + pulls main + cleans up worktree
Install the Linear CLI (brew tap schpet/tap && brew install linear), then:
| Command | What it does |
|---|---|
lin |
View current branch's Linear issue |
wtl |
FZF pick issue → create worktree (marks In Progress) |
wtl ENG-123 my-feat |
Create worktree from specific issue |
/lin in OpenCode |
Load Linear issue context into AI conversation |
Create my-app.worktrees/.setup to auto-run on new worktrees:
#!/bin/bash
npm install
cp .env.example .env| Key | Action |
|---|---|
prefix + o |
Open OpenCode pane (at git root) |
prefix + l |
Open Lazygit pane (at git root) |
prefix + ? |
Cheatsheet popup |
prefix + n |
Notification panel |
prefix + N |
Jump to last notification |
prefix + s |
Session picker |
prefix + c |
New window (at git root) |
prefix + " |
Split horizontal |
prefix + % |
Split vertical |
prefix + g |
Grouped session (multi-monitor) |
prefix + G |
Leave grouped session |
| Key | Action |
|---|---|
Ctrl+. |
Toggle OpenCode TUI |
Ctrl+a |
Ask OpenCode about selection |
Ctrl+x |
OpenCode actions menu |
go / goo |
Send range/line to OpenCode |
Cmd+P |
Find file (Telescope) |
Cmd+Shift+F |
Search in project |
Space ? |
Cheatsheet |
Space ff |
Find file |
Space g |
Live grep |
Space Gg |
Open Neogit (source control) |
Space Gd |
Diff view |
| Key | Action |
|---|---|
Ctrl+a |
Generate commit with OpenCode |
O |
Push + create PR in browser |
Ctrl+o |
Push + fill PR + open |
Ctrl+x |
Merge PR (squash) |
Ctrl+p |
Create + merge PR |
| Command | Action |
|---|---|
g ga gaa gb gc gp |
Git shortcuts |
gco branch |
Checkout + auto-pull |
ghpc |
Push + create PR + open browser |
ghpm |
Merge PR + pull main + cleanup |
ghpcm |
Push + create + merge |
wt / wt name |
Switch/create worktree |
wtrm |
Remove worktree + session + branch |
wtl |
Create worktree from Linear issue |
lin |
View Linear issue |
ds |
Enter nix dev shell |
pj path |
cd + enter dev shell |
envsource .env |
Load .env into shell |
GUI app (Homebrew cask):
# modules/chat/slack.nix
_: { homebrew.casks = [ "slack" ]; }CLI tool (Nix package):
# modules/dev/jq.nix
{ username, pkgs, ... }:
{ home-manager.users.${username} = { home.packages = with pkgs; [ jq ]; }; }Then add the import to your user file and rebuild.
# Rebuild after changing any .nix file
sudo darwin-rebuild switch --flake .#mymac
# Update all inputs (nixpkgs, home-manager, etc.)
nix flake update
sudo darwin-rebuild switch --flake .#mymac
# Enter a project's dev shell
cd ~/projects/my-app
ds
# Format / lint nix files
nixfmt modules/cli/fish.nix
statix check .