Managing Packages

haven manages Homebrew packages and mise language runtimes alongside your dotfiles, keeping everything in sync in a single repo.

Homebrew

Installing packages

Use haven brew install instead of bare brew install — it runs the install and updates your Brewfile so the package is tracked:

haven brew install ripgrep
haven brew install bat
haven brew install iterm2 --cask          # GUI apps and fonts
haven brew install ripgrep --module shell  # add to a specific module's Brewfile

Uninstalling packages

haven brew uninstall ripgrep
haven brew uninstall iterm2 --cask

Removes the formula from all Brewfiles in the repo, then runs brew uninstall.

Brewfile layout

Path Used when
brew/Brewfile haven brew install with no --module
brew/Brewfile.<name> haven brew install --module <name>

You can also write Brewfiles directly:

# brew/Brewfile.shell
brew "fish"
brew "starship"
brew "ripgrep"
brew "fd"
brew "bat"
cask "iterm2"

Applying Brewfiles

haven apply --brews                        # install packages from all active Brewfiles
haven apply --brews --module shell         # only the shell module's Brewfile

Removing unreferenced packages

haven apply --brews --remove-unreferenced-brews   # auto-remove anything not in a Brewfile
haven apply --brews --interactive                  # prompt before removing each package
haven apply --brews --zap                          # remove + zap cask app data

Checking Homebrew drift

haven status --brews    # which packages are present / missing
haven diff --brews      # detailed diff

mise (language runtimes)

mise manages language runtimes (Node.js, Python, Ruby, Go, etc.). haven integrates with it via module configuration.

Configuring mise in a module

# modules/shell.toml
[mise]
config = "source/mise.toml"    # path relative to repo root

On apply, haven runs mise install using the specified config file. If mise is not installed, the section is skipped with a hint.

Example mise.toml

# source/mise.toml
[tools]
node = "lts"
python = "3.12"
go = "latest"

Track this file with haven:

haven add ~/.mise.toml
# or reference source/mise.toml directly in your module

Modules

Modules group Homebrew packages and mise configs under a name. They control packages only — dotfiles are tracked via magic-name encoding in source/, not modules.

Creating a module

# modules/shell.toml
[homebrew]
brewfile = "brew/Brewfile.shell"

[mise]
config = "source/mise.toml"
# modules/work.toml
[homebrew]
brewfile = "brew/Brewfile.work"

requires_op = true    # skip if 1Password isn't available

1Password guard

Adding requires_op = true to a module causes haven to skip that module's brew and mise steps if the op CLI is not installed or the user is not signed in — instead of failing hard:

# modules/secrets.toml
requires_op = true

[homebrew]
brewfile = "brew/Brewfile.secrets"

Profiles

Profiles control which modules are active on each machine. Declared in haven.toml:

[profile.default]
modules = ["shell", "git", "packages"]

[profile.work]
extends = "default"         # inherits all modules from default
modules = ["work", "secrets"]

[profile.personal]
extends = "default"
modules = ["personal"]

[profile.minimal]
modules = ["shell"]

extends gives you single-level inheritance — the parent's modules are applied first, then the child's are appended (duplicates removed).

Applying a profile

haven apply --profile work
haven apply --profile minimal
haven status --profile work

The last-used profile is saved in ~/.haven/state.json and reused automatically unless overridden.

New machine tip

Commit haven.toml with all your profiles. On a new machine:

haven init gh:yourname/haven --apply --profile work

One command clones the repo and applies the right profile.

Custom template variables

Add machine-specific variables to haven.toml for use in .tmpl files:

[data]
work_email    = "alice@corp.example"
kanata_path   = "/usr/local/bin/kanata"
homebrew_path = "/opt/homebrew"

Access them in any .tmpl file as {{ data.<key> }}. Run haven data to see all variables in scope.