Skip to content

ffloyd/nix

Repository files navigation

NixOS + macOS Configuration

Personal dotfiles and system configurations managed with Nix flakes.

Overview

This repository contains my personal configurations for NixOS and macOS machines. It uses Nix flakes to provide reproducible system configurations for:

  • Framework laptop running NixOS with Hyprland
  • Desktop machine running NixOS
  • Work MacBook with nix-darwin

The repository includes an extensive Neovim configuration (~30% of all config lines) with AI integrations.

Architecture

Standard NixOS modules are used for NixOS configuration. home-manager is used for user-level configurations and for sharing modules between NixOS and macOS. nix-darwin is used for macOS system-level configurations.

Home Manager's mkOutOfStoreSymlink is used to directly link dotfiles from the repository to the home directory bypassing the Nix store. This allows editing dotfiles in place without creating a new generation for each change.

Sensitive data is stored in private.nix and hardware-configuration.nix files, which are encrypted using git-crypt.

As an output, the flake provides configurations for several machines. Each machine has a different set of modules enabled.

NixOS hosts use merged configuration that includes both NixOS and Home Manager modules at the same time. MacOS hosts use separate configurations for nix-darwin and Home Manager: I change nix-darwin configuration much less frequently, so there is no point to wait for additional 10+ seconds when updating Home Manager part.

AI Tooling

This repository uses a three-tier system for AI coding assistant configuration (Claude Code and OpenCode):

  1. Shared "as is" (dotfiles/ai-shared/): Content identical for both tools (e.g., coding rules). These files are symlinked directly and are editable without rebuild.

  2. Shared snippets (hm/development-environment/ai-tooling/): Content that needs tool-specific frontmatter (e.g., commit/review instructions). These files are used to generate tool-specific commands/agents and require rebuild to see changes.

  3. Experimental (tool-specific directories like dotfiles/claude/commands/, dotfiles/opencode/agent/): Single-tool experimental files. These are symlinked directly and editable, but require rebuild to discover new files.

Mixed Directories

Home directories like ~/.claude/commands/ and ~/.config/opencode/agent/ contain both generated files (immutable, from Nix store) and experimental files (editable, from dotfiles). This allows stable shared commands to coexist with experimental work-in-progress commands.

Workflow

  1. Experiment: Create file in tool-specific directory (e.g., dotfiles/claude/commands/experimental.md), rebuild once to create symlink, then edit freely.
  2. Share: When stable and needed for both tools:
    • If content is identical → move to dotfiles/ai-shared/
    • If needs tool-specific adjustments → extract to hm/development-environment/ai-tooling/, add generation in ai-tooling.nix

Rebuild Requirements

  • No rebuild needed: Editing existing files in dotfiles/ai-shared/ or tool-specific experimental files
  • Rebuild required: New experimental files, changes to shared snippets in hm/development-environment/ai-tooling/

Repository structure

Files Purpose
flake.nix The main entry point, defines configuration for all hosts.
globals.nix An attrs set with some global values. Propogated to all modules.
private.nix An attrs set with some global values that are personal data. Encrypted. Propogated to all modules.
hosts/* Each host has a folder with machine-specific configuration. hardware-configuration.nix is a hardware configuration and also encrypted.
hm/*.nix Configuration modules that shared between NixOS and MacOS.
hm/development-environment/ai-tooling/ AI shared snippets for generation (immutable, rebuild required for changes).
nixos/*.nix NixOS configuration modules. Can include home-manager configurations that are NixOS-specific.
darwin/*.nix nix-darwin configuration modules.
dotfiles/ai-shared/ AI content shared identically between tools (editable, no rebuild needed).
dotfiles/claude/, dotfiles/opencode/ Tool configuration and experimental AI files (editable, rebuild to discover new files).
dotfiles/* (other) These files are linked directly, not through Nix store. It allows to edit them in place without creating a new generation for each change.

Setup on NixOS

The reposiory should be cloned into nix folder in the user's home folder.

Installation command (in the root folder of the repository):

sudo nixos-rebuild switch --flake .

Setup on MacOS

The reposiory should be cloned into nix folder in the user's home folder.

Install Nix and homebrew. Homebrew will be used only for installing casks. Then run the following command in the root folder of the repository:

nix --extra-experimental-features nix-command --extra-experimental-features flakes run nix-darwin -- switch --flake .
nix --extra-experimental-features nix-command --extra-experimental-features flakes run home-manager/master -- switch --flake .

Configuration Philosophy

This configuration uses an objective-based organization approach instead of more traditional category-based organization. Rather than grouping configurations by technical category (LSP, UI, Git tools), modules are organized around specific objectives or goals.

Each module focuses on what you want to achieve rather than what technology it uses. This approach is inspired by OKR (Objectives and Key Results) methodology, adapted for configuration management. The same philosophy drives both the Neovim configuration and the Nix module structure.

On practice, nix module files are named after the objective/goal they designed to achieve and start with a comment explaining the objective. Each nix module may contain many sub-modules that implement technical features needed to achieve the objective. So, it's ok to comment a sub-module with "setup hyprland", but the file with it should be about "desktop environment setup"/desktop.nix (which explains reason we need hyprland) and not "hyprland configuration"/hyprland.nix.

It helps me to stay focused on the end goals and avoid concentrating too much on technical details.

Not an every module here strictly follows this philosophy and it's by design. If you did OKR, you would understand that defining good objectives is hard and often takes a lot of time and practice. While it forces to ask yourself "why" more often, sometimes it's just easier to put something in a separate file without overthinking.

Common Nix Patterns

Define module as a set of small sub-modules

Following defined configuration philosophy, I want each module to be a set of small sub-modules, each responsible for a specific feature. While the top-level module in a file is responsible for a specific objective, it may include multiple sub-modules that implement the technical details needed to achieve that objective. The benefit of this approach is that for every option, I can easily say which technical feature it belongs to and which higher-level objective it helps to achieve.

Every Nix module (NixOS, home-manager, nix-darwin) has imports attribute that can be used to include other modules. But also it can consume "inline" sub-modules:

{...}:
{
  imports = [
    # include other file
    ./submodule1.nix

    # inline sub-module
    {
      options.bla-bla.enable = true;
    }
  };
}

I cannot just split configuration using comments because Nix by default forbids duplicate keys in an attrs set:

{
  #
  # Feature A
  #
  # utils for feature A
  home.pkgs = with pkgs; [ pkgA pkgB ];

  # ...

  #
  # Feature B
  #
  # utils for feature B
  home.pkgs = with pkgs; [ pkgC pkgD ]; # <-- error: duplicate key
}

Such behavior is inconvenient when you want to split a module into smaller isolated parts each responsible for a specific feature. mkMerge function from nixpkgs.lib can also be used to merge multiple attrs sets into one, but it has some critical limitations (cannot merge modules that define options or imports).

Common tasks

Validation & Testing (NixOS)

  • Check flake validity: nix flake check
  • Test if configuration builds: nixos-rebuild dry-build --flake .
  • Preview activation changes: nixos-rebuild dry-activate --flake .
  • Format code: nix fmt .
  • Lint: statix check

Applying Changes (NixOS)

  • Apply configuration: nixos apply (or use alias os-rebuild)
  • Apply to boot only: nixos apply --no-activate --install-bootloader (or alias os-rebuild-boot)

Maintenance

  • Upgrade dependencies: nix flake update
  • Upgrade specific input: nix flake update nixpkgs-aot
  • Clean old generations: nixos generation delete --min 5 --all (or alias os-gc)

Using pkgs-aot (Ahead-of-Time Packages)

When nixpkgs-unstable has build failures blocking full system upgrade, use pkgs-aot to access newer package versions selectively.

{ pkgs, pkgs-aot, ... }:
{
  environment.systemPackages = [
    pkgs.firefox         # from main nixpkgs
    pkgs-aot.neovim      # newer version with isolated dependencies (from nixpkgs-aot)
  ];
}

Note: pkgs-aot.neovim uses its complete dependency tree from nixpkgs-aot (separate from pkgs). Use for self-contained applications, be careful with libraries and global services.

About

Personal NixOS/MacOS declarative config

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •