Skip to content
wcyganPublic

About

Configuration for Development Environments

Resources

Stars

194 stars

Watchers

8 watching

Forks

Repository files navigation

Dotfiles

Modern developer configuration with safe installation and Nix package management.

Installation

git clone https://github.com/wcygan/dotfiles.git
cd dotfiles
./bootstrap.sh

bootstrap.sh is the supported entry point. With Nix available, it enters the repository development shell and runs the locked Python CLI:

nix develop --no-write-lock-file .#default --command uv run --locked python -m dotfiles_setup install

The default install upgrades or adds this repository's active Nix profile, links the managed configuration, prepares Rust tooling, and verifies the result. Remove an inactive profile element for this checkout before you rerun the install. It does not edit Bash or zsh startup files. Opt in to that behavior only when wanted:

./bootstrap.sh install --shell-handoff

On macOS, if Nix is missing, ./bootstrap.sh opens the Determinate macOS installer package and exits; finish that installer, start a fresh shell, then rerun the command. On Linux or WSL, explicitly authorize the Determinate Nix install instead:

./bootstrap.sh --install-nix --yes

Full documentation available at: https://wcygan.github.io/dotfiles/

Install Flow

./bootstrap.sh install runs as journaled, reversible steps under one per-user lock. An interrupted step leaves a recovery manifest; recover finishes or reverses it.

flowchart TD
    A["./bootstrap.sh install"] --> B["Nix dev shell → locked Python CLI"]
    B --> C["Per-user mutation lock<br/>(locking.py)"]
    C --> D{"Interrupted<br/>operation pending?"}
    D -- "yes" --> E["Refuse: run ./bootstrap.sh recover"]
    D -- "no" --> F["Nix profile<br/>upgrade/add active element"]
    F --> G["Link managed config<br/>(links.py)"]
    G --> H["Plan → stage → journal →<br/>atomic no-replace apply<br/>(mutations.py)"]
    H -- "any failure" --> I["Guarded rollback →<br/>recovery-needed manifest"]
    H -- "ok" --> J["Rust tooling pin<br/>(rustup.py)"]
    J --> K["Strict verify<br/>(verify.py)"]
    I -.-> R["recover: journal replay<br/>or reverse rollback"]
    E -.-> R
    K --> L["PASS: install complete"]
Loading

What You Get

  • 🚀 Modern CLI tools: ripgrep, fd, bat, eza, fzf, delta, and more
  • 🐟 Fish shell: Full configuration with functions and abbreviations
  • ⭐ Starship prompt: Beautiful, fast, and informative
  • 📦 Nix packages: Reproducible across macOS, Ubuntu, and Fedora
  • 🔗 Safe symlinks: Automatic backups before any changes

Project Structure

dotfiles/
├── .agents/             # Project-local operating skills
├── config/             # Configuration files
│   ├── agents/          # Authoritative global agent instructions
│   ├── fish/           # Fish shell config
│   ├── codex/          # Codex config template and compatibility link
│   ├── zed/            # Zed config
│   ├── ghostty/        # Ghostty config
│   ├── starship.toml   # Starship prompt
│   └── shell-nix.sh    # Bash/zsh compatibility
├── src/dotfiles_setup/  # Locked Python setup CLI
├── tests/               # Python and platform test suites
├── flake.nix           # Nix package definitions
├── bootstrap.sh         # Nix bootstrap and Python CLI bridge
├── pyproject.toml       # Python project metadata and tool configuration
├── uv.lock              # Locked Python dependencies
└── docs/               # Documentation site

Post-Install Extras

Peon Ping (Claude Code sound notifications)

Peon Ping plays Warcraft-style sound cues when Claude Code needs attention. Not managed by Nix — install separately via Homebrew:

brew install PeonPing/tap/peon-ping && peon-ping-setup

This adds hooks to your local ~/.claude/ config. It is not managed by this repository.

Agent Skills

Dotfiles-specific operating skills live in .agents/skills/; reusable skills are authored and published from the dedicated agent-skills repository, which is authoritative. This repository pins that external collection by commit and installs it as a Codex user-scope catalog without vendoring its source:

./bootstrap.sh agent-skills
./bootstrap.sh agent-skills --check

agent-skills.lock.toml records the repository, full commit SHA, agent, and shared relative directory. Installation uses gh skill install --all --pin <commit> --force --dir "$HOME/.agents/skills". It then uses gh skill list --dir "$HOME/.agents/skills" to verify source, version, pin, scope, and path metadata. Existing same-name skills from another source or location stop the operation. The explicit directory keeps the destination independent of GitHub CLI host defaults. Author changes in the agent-skills repository, advance the tracked pin intentionally, and rerun the command. GitHub CLI does not prune files or skill directories removed upstream, so inspect retired skills when the pin advances.

Other Codex state is managed narrowly: config/codex/config.toml is a portable template copied to ${CODEX_HOME:-~/.codex}/config.toml only when missing, and config/agents/AGENTS.md is the authoritative global instruction file linked to both ~/.agents/AGENTS.md and ${CODEX_HOME:-~/.codex}/AGENTS.md. config/codex/AGENTS.md remains a repository compatibility link to that source. Skills, the GitHub CLI skill lock, custom agents, and the rest of CODEX_HOME remain machine-local outputs rather than repository content. Codex may write machine-specific [projects] trust entries into the local config; keep those out of the tracked template.

Pi credentials, sessions, models, and global catalogs remain machine-local state.

npm Global Tools

The installer configures npm's user prefix and a one-day dependency release cooldown in ~/.npmrc:

prefix=${HOME}/.local
min-release-age=1

That keeps npm install -g writable when npm comes from Nix, lets package releases settle for 24 hours before install, and exposes global binaries through ~/.local/bin, which the shell config already adds to PATH behind Nix-managed tools.

Example:

npm install -g @playwright/cli@latest
playwright-cli --help

Zed

Nix provides zed and zeditor. On macOS, launch the managed app with zed or ~/.nix-profile/Applications/Zed.app. An existing Homebrew Zed installation is retained; command -v zed shows which CLI takes precedence.

Nightly Nix updates

Enable a local 03:00 schedule after installing the profile:

make nightly-enable
make nightly-status
make nightly-disable

After that first opt-in, normal ./bootstrap.sh install remembers the choice and repairs the scheduler on subsequent installs. Enabling or repairing it runs a harmless job through launchd/systemd to prove that the scheduler can execute this checkout and write an acknowledgement. Strict make verify checks the generated configuration, registration, and installation probe without starting jobs or modifying state. A broken timer fails verification rather than silently appearing installed.

macOS uses a user LaunchAgent; Linux uses a systemd user timer. The user service manager must be running. WSL needs systemd enabled. The job does not wake the computer or run at login. A late wake-up outside 03:00–04:00 local time is skipped by the updater itself. No commits, pushes, Git pulls, or skill catalog installations run automatically.

Each GitHub input revision is quarantined for seven complete days after this machine first observes it. The first week collects candidates without installing them. This delays newly observed Nix snapshots; it is not a guarantee about every upstream package's release date and is not protection against zero-days. It also delays security fixes. Manual make update bypasses quarantine when you explicitly need an urgent update.

Candidates withdrawn from the current branch history are rejected. Eligible candidates are tested in a temporary checkout before the real profile and lockfile change. The job uses the setup mutation lock and journal, stops launching work before the window closes, and reserves time for rollback. A failed installation restores the previous profile and guarded lockfile; an interruption leaves recovery state and prevents another update until reviewed.

Logs and observation metadata live under ${XDG_STATE_HOME:-~/.local/state}/dotfiles/nightly. make nightly-status shows the latest recorded result and reason. For a dirty-checkout skip, it also records Git's short status, including filenames but not file contents; this is a snapshot taken after the guard detects changes. last-run.log contains build and test diagnostics. The scheduler captures output separately (on Linux, use journalctl --user -u dotfiles-nightly.service). These files are local, not a history or remote alerting system; an interrupted process may leave an older result.

The updated flake.lock remains local for review, with numbered backups for guarded recovery. Unrelated checkout changes cause a skip. Re-enabling explicitly accepts the current checkout as the baseline; committing a clean checkout also lets it proceed. Disable the schedule before moving or removing the checkout. Normal setup commands continue preserving committed lockfiles.

CLI migration checks

Run the installed-profile checks before removing an alternate installation:

python3 scripts/check-cli-migration.py --group first
python3 scripts/check-cli-migration.py --group next

The first group exercises encryption, Kubernetes configuration, container image creation, package scripts, version control, and a temporary Temporal server. It requires a running Docker engine to execute a local act workflow with a pinned container image. The next group exercises document rendering, OCR, metadata, video encoding, benchmarking, and a localhost yt-dlp download.

Both checks exclude Homebrew from PATH and use disposable files, repositories, keys, and service state. They do not access your Kubernetes cluster or Temporal server. The yt-dlp package includes its own Deno runtime.

Remove Homebrew copies only after their group passes, then rerun the checks. Retain formulae required by other Homebrew applications; for example, FreeRDP and unpaper still depend on Homebrew FFmpeg. No automatic dependency cleanup is part of this migration.

Dependency Install Cooldowns

The repo's docs site uses npm (docs/package-lock.json). The installer manages dependency release-age settings for npm and Bun:

  • Bun: links config/bunfig.toml to global bunfig locations with minimumReleaseAge = 259200.
  • npm: manages min-release-age=1 in ~/.npmrc. npm does not currently expose a documented package or scope exemption key for this setting. Bun exempts the @wcygan/* workspace scope from the cooldown.

Per-Project Dev Environments (nix-direnv)

For reproducible per-project tool versions, use nix-direnv.

One-time machine setup

  1. Ensure direnv is in flake.nix packages
  2. Fish hook is preconfigured at config/fish/conf.d/20-direnv.fish
  3. Global direnv config allows nix-direnv

Per-repository setup

  1. Create .envrc:

    use flake
  2. Create flake.nix with a dev shell:

    {
      inputs.nixpkgs.url = "github:NixOS/nixpkgs";
      outputs = { self, nixpkgs }: {
        devShells.x86_64-darwin.default = nixpkgs.legacyPackages.x86_64-darwin.mkShell {
          packages = [ /* project-specific tools */ ];
        };
      };
    }
  3. direnv allow to trust the environment.

Commit .envrc and flake.nix; gitignore .direnv/. Use cases: language toolchains (Node 20 vs 18), pinned database clients, exact build tool versions, locked cloud CLIs.

For troubleshooting concurrent direnv rebuilds and Nix daemon issues, the agent has a dedicated nix-direnv-perf skill.

Nix Installation (macOS)

Use the Determinate Systems macOS Installer. When Nix is absent, ./bootstrap.sh opens its package URL and tells you to rerun after the package installation completes. The package handles macOS integration, automatic updates, and Apple Silicon optimization.

For Linux and WSL, the bootstrapper only invokes the official Determinate installer after the explicit --install-nix --yes confirmation shown above.

Setup and Verification

Use the Make targets for the common workflow, or pass the matching command to ./bootstrap.sh directly:

make install                 # ./bootstrap.sh
make link                    # ./bootstrap.sh link
make link-dry                # ./bootstrap.sh link --dry-run
make git-user                # ./bootstrap.sh git-user
make setup-shell-handoff     # ./bootstrap.sh shell-handoff
make setup-rustup-components # ./bootstrap.sh rustup
make verify                  # ./bootstrap.sh verify
make doctor                  # ./bootstrap.sh doctor
make uninstall               # ./bootstrap.sh uninstall
make uninstall-dry           # ./bootstrap.sh uninstall --dry-run
./bootstrap.sh recover       # inspect interrupted-operation recovery

doctor is advisory: it reports the host and development-shell environment as PASS or WARN without treating not-yet-installed state as an acceptance failure. verify is strict and read-only. It requires an active Nix profile element sourced from this exact checkout, required binaries in that element's store output, Python 3.13, every managed link at its platform-specific destination, and a regular machine-local Codex config.toml. A full install always ends with strict verification and fails when a required operation was failed or skipped.

Mutating commands share a fail-fast per-user lock under the XDG cache home, so two setup processes cannot race. Link setup preflights its complete inventory, records each destination's prior identity, rechecks it before replacement, and stops when it detects a change. It uses atomic replacements, restores physical destinations if replacement fails, and journals recovery metadata under ${XDG_STATE_HOME:-$HOME/.local/state}/dotfiles/. doctor, verify, dry runs, and the default recover inspection remain read-only and do not take the mutation lock. If an interrupted operation is recorded, inspect it first:

./bootstrap.sh recover
./bootstrap.sh recover --apply --yes  # explicit, guarded mutation

Recovery restores only recorded backups or links whose current state still matches the interrupted operation. It refuses to overwrite later user changes and leaves the recovery manifest in place when manual intervention is needed. The manifest records paths, link targets, statuses, timestamps, and one-way content hashes only; it never records Git identity values, Codex trust contents, or file contents. Shell startup-file and .npmrc symlink referents must remain inside HOME; setup refuses external referents with manual guidance instead of modifying an unexpected file.

Rust is pinned in rust-toolchain.toml to the exact 1.97.1 corrective release, including rust-analyzer. ./bootstrap.sh rustup installs and resolves that toolchain explicitly and never changes the global rustup default. Update the pin intentionally after reviewing an official Rust release, then rerun the Rust and strict-verification tests.

Committed CI and local Docker acceptance use full GitHub Action SHAs and multi-platform image manifest digests, so pull requests test exactly the reviewed inputs. A separate scheduled/manual upstream-freshness workflow strips the committed image digests in a temporary runner file and builds the current Ubuntu 24.04, Fedora 43, and Nix 2.30.3 tags. It reports compatibility drift but never edits the repository. Dependabot proposes weekly Action and Docker digest updates for human review; no automatic merge is configured. Platform refreshes are evaluated against the official Ubuntu release lifecycle and Fedora release lifecycle.

Pull-request CI calls the same repository targets used locally: locked quality and syntax checks, four-system flake evaluation, and the canonical Ubuntu/Fedora Docker driver. Docker builds use separate GitHub Actions cache scopes, load the resulting image, and run strict installation plus Fish/direnv runtime smoke. Pytest JUnit reports are retained on both success and failure. Documentation changes build on pull requests, but Pages deployment is restricted to a push to main. Native macOS 15 arm64 acceptance runs weekly or manually in an isolated temporary home; Intel macOS 15 acceptance is manual so routine pull requests remain Linux-only and economical.

The Python package lives in src/dotfiles_setup/: cli.py owns command parsing, while focused modules own profile management, linking and cleanup, advisory diagnostics, strict verification, Rust setup, shell handoff, and Git identity. Run a command inside the Nix development environment with uv run --locked python -m dotfiles_setup <command>; normally, prefer bootstrap.sh so the Nix boundary is applied consistently.

To roll back a package change, use nix profile rollback. Configuration links are safe to rerun and pre-existing files are kept as timestamped backups. The annotated pre-transactional-setup-recovery tag is the rollback point before the transactional setup boundary. Keep a known-good Git tag before any later configuration migration so the repository state itself is also easy to restore.

Quick Reference

# Update the flake and managed profile
make update

# Add new packages
# Edit flake.nix, then update the managed profile:
make profile

# Run the locked Python checks
make test-pre      # Ruff plus the complete pytest suite
make test-local    # ephemeral-HOME-focused pytest suite
make test-shell    # shell-handoff pytest suite
make test-syntax   # Bash and Fish parsing
make test-eval     # all four flake systems, evaluation only
make test-docker   # Python Ubuntu/Fedora driver
make test           # every non-Docker test

# Documentation
make docs-build    # clean install and production build
make docs

About

Configuration for Development Environments

Resources

Stars

194 stars

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages