Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@
*.un~
config/fish/config.local.fish
docs/superpowers/
Brewfile.lock.json
71 changes: 0 additions & 71 deletions Brewfile

This file was deleted.

43 changes: 27 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,47 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## What This Is

A macOS dotfiles repo. Config files live in `config/`, rc files in `rc/`. The `install.rb` script symlinks everything into place.
A macOS dotfiles repo. Config files live in `config/`, rc files in `rc/`. Machine setup is declarative via `mise bootstrap`, driven entirely by `config/mise/config.toml`.

## Install Script
## Bootstrap

Run from the cloned repo (relative dotfile sources resolve against the config file's real location, so it can be cloned anywhere):

```bash
ruby install.rb # symlinks + brew bundle + macOS defaults
ruby install.rb -n # dry run — preview what would happen
ruby install.rb --no-brew --no-macos # symlinks only
export MISE_GLOBAL_CONFIG_FILE="$PWD/config/mise/config.toml"
mise bootstrap -n # preview — changes nothing
mise bootstrap # apply
mise bootstrap status # show drift from the declared state
```

The script does six things:
1. `config/*` → `~/.config/*` (preserving directory structure)
2. `rc/*` → `~/.<name>` (adds dot prefix, e.g. `rc/vimrc` → `~/.vimrc`)
3. `brew bundle` — installs Homebrew packages (auto-detected when `Brewfile` exists, skip with `--no-brew`)
4. `mise install` — installs tools pinned in mise config, e.g. `hk` for git hooks (auto-detected when `mise` present, skip with `--no-mise`)
5. Claude plugins + MCP servers — installs the marketplaces/plugins declared in `claude/settings.json` and registers the rubocop MCP server at user scope (auto-detected when `claude` present, skip with `--no-plugins`). Idempotent: each item is skipped if already present
6. `macos.sh` — applies macOS defaults (auto-detected on macOS, skip with `--no-macos`)
`config/mise/config.toml` declares, in pipeline order:
1. `[bootstrap.hooks.pre-packages]` — installs Homebrew if missing
2. `[bootstrap.packages]` — Homebrew formulae (`brew:`) and casks/fonts (`brew-cask:`)
3. `[dotfiles]` — `symlink-each` for `config/` → `~/.config/`, `claude/` → `~/.claude/`, `bin/` → `~/.local/bin/`, plus per-file `rc/*` → `~/.<name>`. `symlink-each` links each file individually, so machine-local siblings (`config.local`, `config.local.fish`) inside managed dirs are left untouched.
4. `[bootstrap.user].login_shell` — sets fish
5. `[bootstrap.macos.defaults]` — the declarative `defaults write` set; the non-declarative tail (currentHost scope, chflags, PlistBuddy, killall, `$HOME`-expanded screenshot dir) is in `[bootstrap.hooks.post-defaults]`
6. `[tools]` — `mise install`
7. `[tasks.bootstrap]` — depends on `setup-identity`, `setup-vim-dirs`, `setup-claude`
8. `[bootstrap.hooks.final]` — the read-only commit-signing diagnostic

Everything is idempotent — anything already in its desired state is skipped, so re-running is safe.

Files listed in `SKIP` (currently `config.local.example`) are excluded. Existing non-symlink files are backed up with `.backup` suffix.
### Imperative setup: `bin/dotfiles-setup`

At the end of a run it also does a read-only **commit-signing check** (`check_git_signing`): warns, with the exact fix, if `config.local` is missing, the signing key is absent, or that key isn't trusted in `allowed_signers` / not registered on GitHub. It changes nothing — a new machine just gets told what to wire up.
The parts mise can't express declaratively live in `bin/dotfiles-setup` (a Ruby script). The dotfiles phase deploys it to `~/.local/bin` before the `bootstrap` task runs, and it locates the repo via its own realpath — no hardcoded path. Subcommands, each a mise task and idempotent:
- `identity` (`mise run setup-identity`) — interactive git identity → `~/.config/git/config.local`, ssh config, fish local config
- `vim-dirs` (`mise run setup-vim-dirs`) — vim swap/backup/undo dirs
- `claude` (`mise run setup-claude`) — installs the marketplaces/plugins declared in `claude/settings.json` and registers the rubocop MCP server at user scope
- `signing` (`mise run check-signing`) — read-only diagnostic: warns, with the exact fix, if `config.local` is missing, the signing key is absent, or that key isn't in `allowed_signers` / not registered on GitHub

## Structure

- **`config/fish/config.fish`** — Fish shell config: PATH, Homebrew, aliases (eza, bat, zoxide, fzf, rg), git/ruby abbreviations, Starship prompt
- **`config/git/`** — Git config with SSH signing, histogram diff, zdiff3 merge conflicts, rerere, auto-rebase on pull. Per-machine identity goes in `~/.config/git/config.local` (see `config.local.example`)
- **`config/vim/`** — Modular vim config auto-sourced via glob in `rc/vimrc`. Files prefixed with `_` (e.g. `_plug.vim`, `_nvim-defaults.vim`, `_macvim.vim`) are sourced explicitly and excluded from the glob
- **`config/starship.toml`** — Single-line Starship prompt with Nerd Font symbols
- **`config/mise/config.toml`** — Global mise settings (legacy version files, experimental features)
- **`claude/`** — Claude Code config symlinked into `~/.claude`: `settings.json` (permissions, status line, `SessionStart` hook, plus the enabled plugins and known marketplaces that drive the install-script plugin step), `statusline.sh` (hostname-led status line), `hooks/machine-context.sh` (a `SessionStart` hook that injects the hostname into the model's context so it knows which machine it's on), and `bin/rubocop-mcp` (a per-repo-adaptive rubocop MCP launcher, registered user-scope by the install script). Note: `settings.json` is a tracked file but Claude also writes to `~/.claude/settings.json` at runtime (theme, plugin state), so machine-local tooling that rewrites it — e.g. peon-ping — is intentionally **not** tracked here; its hooks land in `~/.claude/settings.json.backup` after an install run
- **`config/mise/config.toml`** — mise settings and pinned `[tools]`, plus the whole `mise bootstrap` config (packages, dotfiles, login shell, macOS defaults, setup tasks)
- **`claude/`** — Claude Code config symlinked into `~/.claude`: `settings.json` (permissions, status line, `SessionStart` hook, plus the enabled plugins and known marketplaces that drive the `setup-claude` task), `statusline.sh` (hostname-led status line), `hooks/machine-context.sh` (a `SessionStart` hook that injects the hostname into the model's context so it knows which machine it's on), and `bin/rubocop-mcp` (a per-repo-adaptive rubocop MCP launcher, registered user-scope by the `setup-claude` task). Note: `settings.json` is a tracked file but Claude also writes to `~/.claude/settings.json` at runtime (theme, plugin state), so machine-local tooling that rewrites it — e.g. peon-ping — is intentionally **not** tracked here; its hooks land in `~/.claude/settings.json.backup` after a bootstrap run
## Conventions

- Commit messages: **short, imperative, title case** (e.g. `Add Pagination to Query Endpoint`). No bullet-point bodies unless a single sentence of context is genuinely needed.
Expand Down
85 changes: 46 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,55 +1,59 @@
# Dotfiles

Config files for macOS. Managed with symlinks via `install.rb`.
Config files for macOS. Provisioned declaratively with [`mise bootstrap`](https://mise.jdx.dev/bootstrap.html) — one config (`config/mise/config.toml`) describes the packages, dotfile symlinks, login shell, macOS defaults, and tools for the whole machine.

## New Laptop Setup

### 1. Install Homebrew
### 1. Install mise

```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
curl https://mise.run | sh # or: brew install mise
```

Everything else — including Homebrew itself — is installed by the bootstrap.

### 2. Clone This Repo

Clone it anywhere — `install.rb` resolves paths relative to itself:
Clone it anywhere; nothing hardcodes a path (dotfile sources resolve relative to the config file):

```bash
git clone <repo-url>
git clone <repo-url> dotfiles
cd dotfiles
```

### 3. Install Everything
### 3. Bootstrap

Run bootstrap **from the repo** so the relative dotfile sources resolve correctly:

```bash
ruby install.rb -n # preview what will happen
ruby install.rb # do it
export MISE_GLOBAL_CONFIG_FILE="$PWD/config/mise/config.toml"
mise trust "$MISE_GLOBAL_CONFIG_FILE"
mise bootstrap -n # preview — changes nothing
mise bootstrap # apply
```

This does three things in order:
1. **Symlinks** config files into place (`config/*` → `~/.config/*`, `rc/*` → `~/.<name>`, `claude/*` → `~/.claude/*`). Existing files are backed up with a `.backup` suffix.
2. **`brew bundle`** — installs packages from the `Brewfile` (auto-detected when the file exists).
3. **`macos.sh`** — applies macOS defaults (auto-detected on macOS).
In order, this:

Each step can be controlled with flags:
1. Installs Homebrew if missing, then the **`[bootstrap.packages]`** (formulae + casks/fonts).
2. Symlinks **dotfiles** (`config/*` → `~/.config/*`, `rc/*` → `~/.<name>`, `claude/*` → `~/.claude/*`, `bin/*` → `~/.local/bin/*`). `symlink-each` links each file individually, so machine-local files like `~/.config/git/config.local` are left untouched.
3. Sets **fish** as the login shell.
4. Writes **macOS defaults**.
5. Installs the pinned **tools** (`ruby`, `node`, …).
6. Runs the **`bootstrap` task** — per-machine setup that isn't declarative (git identity, vim dirs, Claude plugins), then a read-only commit-signing diagnostic.

```bash
ruby install.rb --no-brew # skip brew bundle
ruby install.rb --no-macos # skip macOS defaults
```
Check for drift any time with `mise bootstrap status`. Re-running is safe: anything already in its desired state is skipped.

### 5. Set Up Git Identity
> Run bootstrap from the cloned repo, not against the deployed `~/.config/mise` symlink — relative sources resolve against the config file's real location.

Create `~/.config/git/config.local` with your per-machine identity (see `config/git/config.local.example`):
### 4. Git Identity

```ini
[user]
email = you@example.com
name = Your Name
signingKey = ~/.ssh/id_ed25519.pub
The bootstrap task seeds `~/.config/git/config.local` interactively. If it couldn't prompt (no TTY during bootstrap), run it directly:

```bash
mise run setup-identity # or: dotfiles-setup identity
```

### 6. Set Up an SSH Key for GitHub (Auth + Signing)
### 5. Set Up an SSH Key for GitHub (Auth + Signing)

If you ran `gh auth login` and let it generate an SSH key, the key already exists at `~/.ssh/id_ed25519` and is registered with GitHub as an **authentication** key. Otherwise generate one manually:

Expand All @@ -70,20 +74,13 @@ Then add the public key to `config/git/allowed_signers` so local verification wo
echo "$(git config user.email) $(cat ~/.ssh/id_ed25519.pub)" >> config/git/allowed_signers
```

Confirm everything is wired up with an empty signed commit:
`mise run check-signing` re-runs the diagnostic and prints the exact fix for anything still missing. Confirm everything is wired up with an empty signed commit:

```bash
git commit --allow-empty -m "test signing" && git log --show-signature -1
```

### 7. Set Fish as Default Shell

```bash
echo /opt/homebrew/bin/fish | sudo tee -a /etc/shells
chsh -s /opt/homebrew/bin/fish
```

### 8. Install Vim Plugins
### 6. Install Vim Plugins

Open vim and run:

Expand All @@ -98,14 +95,24 @@ config/
fish/config.fish — shell: PATH, aliases, abbreviations
git/ — git config, global ignore, diff attributes, allowed signers
ghostty/config — terminal appearance and keybinds
mise/config.toml — runtime version manager settings
mise/config.toml — tools + the mise bootstrap config (packages, dotfiles, macOS defaults)
starship.toml — prompt theme (single-line, Nerd Font icons)
vim/ — modular vim config (auto-sourced via glob)
rc/
vimrc — vim entry point (sources files from config/vim/)
bin/
dotfiles-setup — per-machine setup driven by mise tasks (identity, vim dirs, Claude, signing)
claude/
commands/ — custom Claude Code slash commands
Brewfile — Homebrew packages and casks
install.rb — installer: symlinks, brew bundle, macOS defaults
macos.sh — macOS system preferences (run via install.rb or standalone)
settings.json — enabled plugins + marketplaces, hooks, permissions
```

## Managing Dotfiles

Bootstrap is declarative and idempotent, so day-to-day changes are just edits to `config/mise/config.toml` and the files under `config/`, `rc/`, etc. Useful commands:

```bash
mise bootstrap status # what's drifted from the declared state
mise dotfiles apply -n # preview dotfile symlink changes
mise run setup-identity # (re)seed git identity / ssh / fish-local
mise run check-signing # audit the commit-signing chain
```
Loading