Skip to content

Repository files navigation

Hola

Brewfile + mise.toml + dotfiles = Done

Set up your Mac in minutes. Hola is a single-binary configuration manager written in Zig. It installs Homebrew packages, sets up dotfiles, and configures macOS defaults—all from a single command.

What You Need

Create username/dotfiles on GitHub with three simple files:

1. 🍺 ~/.Brewfile (Homebrew's native format)

brew "tmux"
brew "neovim"
cask "ghostty"
cask "zed@preview"
cask "orbstack"

Homebrew integration.

2. 🛠️ mise.toml (mise's native format)

[tools]
node = "24"
python = "3.14"

Lock your tool versions. Never drift.

3. 📂 ~/.dotfiles/ (your dotfiles)

dotfiles/.zshrc      → ~/.zshrc
dotfiles/.gitconfig  → ~/.gitconfig

Symlink mapping. Dead simple.

No custom syntax. No learning required.


Installation

Quick Install (Recommended)

curl -fsSL https://hola.ac/install | bash

This downloads the binary for your architecture (arm64/x86_64) and installs it to the current directory.

Homebrew

brew install ratazzi/hola/hola

Manual Download

Download the latest release from GitHub Releases:

# macOS (Apple Silicon)
curl -fsSL https://github.com/ratazzi/hola/releases/latest/download/hola-macos-aarch64 -o hola
chmod +x hola
xattr -d com.apple.quarantine hola
sudo mv hola /usr/local/bin/

# Linux (x86_64)
curl -fsSL https://github.com/ratazzi/hola/releases/latest/download/hola-linux-x86_64 -o hola
chmod +x hola
sudo mv hola /usr/local/bin/

Why Hola?

Convention Over Configuration

  • Zero learning curve: Use Brewfile and mise.toml you already know
  • No custom syntax: No templates, no special comments, no magic
  • Native integration: First-class Homebrew and mise support
  • macOS declarative: Configure Dock and system preferences as code
  • Tool version locking: Reproducible environments across machines

It Just Works™

# One command to set up everything
hola apply

# Packages, tools, dotfiles, and system settings - all done

Advanced: ~/.config/hola/provision.rb (Optional)

90% of users only need Brewfile + mise.toml + dotfiles/.

For the other 10% who need complex logic, we provide a beautiful Ruby DSL:

# resources.rb - reads like English, because it's Ruby

file "/etc/hosts" do
  content "127.0.0.1 local.dev"
end

execute "install-oh-my-zsh" do
  command 'sh -c "$(curl -fsSL https://ohmyz.sh/install.sh)"'
  not_if { Dir.exist?("~/.oh-my-zsh") }
end

 macOS Native Integration

Configure macOS settings declaratively with full type safety:

# Configure macOS Dock
macos_dock do
  apps [
    '/Applications/Google Chrome.app/',
    '/Applications/Zed Preview.app/',
    '/Applications/Ghostty.app/',
  ]
  orientation "bottom"
  autohide false
  magnification true
  tilesize 50
  largesize 40
end

# Keyboard repeat rate (lower = faster)
macos_defaults 'keyboard repeat rate' do
  global true
  key 'KeyRepeat'
  value 1
end

macos_defaults 'initial key repeat delay' do
  global true
  key 'InitialKeyRepeat'
  value 15
end

macos_defaults 'show all file extensions' do
  domain 'com.apple.finder'
  key 'AppleShowAllExtensions'
  value true
end

# Per-host keys (`defaults -currentHost write ...`)
macos_defaults 'menu bar item spacing' do
  global true
  current_host true
  key 'NSStatusItemSpacing'
  value 6
end

Features:

  • Type-safe: Boolean, Integer, Float, String - automatically handled
  • Idempotent: Only updates when values differ
  • Per-host domain: current_host true targets defaults -currentHost
  • Auto-restart: Automatically restarts Finder/Dock/SystemUIServer when needed
  • No manual defaults commands: Just declare what you want

No YAML hell. No cryptic property lists. Just readable code.

If you know Ruby, you already know this. If you don't, you can still read it.

Project Tasks with hola run

Put a Holafile in a project to define repeatable, state-aware development tasks. Hola finds it from the current directory or any parent directory, then runs each resource as soon as it is declared. holafile.rb is also canonical; the Rake file names remain as legacy fallbacks and emit a migration warning when discovered implicitly:

directory ".cache"

file ".cache/config" => ".cache" do
  File.open(".cache/config", "wb") { |file| file.write("ready\n") }
end

desc "Build the project"
task :build => ".cache/config" do
  sh "zig build"
end

namespace :db do
  task :migrate, [:environment] do |_task, args|
    args.with_defaults :environment => "development"
    sh "./bin/migrate #{args.environment}"
  end
end

task :default => :build
hola run                         # Run the default task
hola run build                   # Run an explicit task
hola build                       # Shorthand when it does not collide with a built-in
hola run "db:migrate[staging]"   # Pass task arguments
hola run -T                      # List described tasks
hola run -P                      # Show prerequisites
hola run -n build                # Dry run
hola run --trace build           # Trace task invocation
hola run --output compact build  # Animated spinner for established scripts

Normal output is the default, including on a TTY. It is append-only and shows resource actions, nested composite resources, live command streams, change details, and structured failure diagnostics. --output compact enables the animated spinner; the older plain and pretty mode names remain accepted as aliases.

The embedded mruby task runtime is inspired by Rake rather than compatible with it. It covers the common task surface: dependencies, namespaces, arguments, task enhancement and re-enabling, Rake::Task[], file, directory, string suffix rules, Dir.glob, FileList, rake/clean, local require/ require_relative, FileUtils, and streaming sh output. In task mode, file and directory always have their standard Rake meanings; file_task remains as a compatibility alias for file. Configuration resources enter through the explicit resources gateway and converge immediately when declared inside a task:

task :configure do
  resources do
    directory "build"
    file "build/version.txt" do
      content VERSION
    end
  end
end

task :compile do
  resources.execute "compile" do
    command "zig build"
  end
end

The gateway delegates to the complete Hola::Resources.* API, so extensions have one stable namespace without forcing every call site to repeat the long prefix. Provision scripts retain their existing top-level resource DSL and may also use the namespace.

Reusable provisioning phases

A provisioning recipe may split its flat resource stream with phase markers. The marker changes the declaration context for the resources that follow it; it does not add another Ruby block or another output indentation level:

# scripts/deploy.rb
phase :prepare

git "/srv/app/releases/next" do
  repository "https://example.com/app.git"
end

execute "build application"

phase :deploy

link "/srv/app/current" do
  to "/srv/app/releases/next"
end

execute "restart application"

The same recipe supports a complete unattended run or a single operator-selected stage:

hola provision scripts/deploy.rb
hola provision --phase prepare scripts/deploy.rb
hola provision --phase deploy scripts/deploy.rb

A Holafile can import those phases as namespaced tasks. No duplicate mise tasks or wrapper scripts are required:

import_phases "scripts/deploy.rb", :as => :app

task :release => ["app:prepare", "app:deploy"]
task :default => :release

hola run app:prepare executes one phase, while hola run release composes both. Imported phases also appear in hola run -T. Recipes without a phase marker keep the existing flat provisioning behavior and the Chef-compatible top-level resource style. Agent tasks may provide an optional "phase" field; resource callback results include their phase name.

macOS release resources

hola run can replace repeated macOS release shell in CI workflows. Each release step remains independently composable instead of being hidden inside one project-specific command:

APP = "CoulsonApp/build/DerivedData/Build/Products/Release/Coulson.app"
DMG = "build/Coulson.dmg"
KEYCHAIN = File.join(ENV.fetch("RUNNER_TEMP", "/tmp"), "build.keychain")

task :certificate do
  resources.macos_signing_certificate ENV.fetch("DEVELOPER_ID_IDENTITY") do
    certificate_base64 ENV.fetch("DEVELOPER_ID_CERTIFICATE_P12")
    password ENV.fetch("DEVELOPER_ID_CERTIFICATE_PASSWORD")
    keychain KEYCHAIN
  end
end

task :build do
  resources.xcode_build "Coulson" do
    workspace "CoulsonApp/Coulson.xcworkspace"
    scheme "CoulsonApp"
    configuration "Release"
    derived_data_path "CoulsonApp/build/DerivedData"
    settings(
      "MARKETING_VERSION" => ENV.fetch("VERSION"),
      "CURRENT_PROJECT_VERSION" => ENV.fetch("BUILD_NUMBER")
    )
    creates APP
  end
end

task :sign => [:certificate, :build] do
  resources.macos_codesign APP do
    identity ENV.fetch("DEVELOPER_ID_IDENTITY")
    keychain KEYCHAIN
    nested true       # Sign Mach-O files and nested bundles inside-out.
  end
end

task :package => :sign do
  resources.macos_dmg DMG do
    source APP
    volume_name "Coulson"
    window_bounds [120, 120, 780, 540]
    icon_position "Coulson.app", [180, 235]
    icon_position "Applications", [480, 235]
  end
end

task :sign_dmg => :package do
  resources.macos_codesign DMG do
    identity ENV.fetch("DEVELOPER_ID_IDENTITY")
    keychain KEYCHAIN
  end
end

task :release => :sign_dmg do
  resources.macos_notarize DMG do
    keychain_profile ENV.fetch("NOTARY_PROFILE")
  end
end

The five resources cover the reusable release boundary:

  • xcode_build constructs xcodebuild invocations without shell quoting and uses creates as its convergence marker.
  • macos_signing_certificate creates or unlocks a CI keychain, imports a P12 only when the identity is absent, and configures key access for codesign.
  • macos_codesign signs individual binaries or whole bundles. It can discover nested Mach-O files, frameworks, apps, extensions, and XPCs and sign them inside-out. Verification may be deep; signing never relies on codesign --deep.
  • macos_dmg stages an app with ditto, adds the Applications link and optional Finder layout or background, creates the image atomically, and verifies it. A valid image newer than all source files is left untouched.
  • macos_notarize supports a notarytool keychain profile, Apple ID credentials, or an App Store Connect API key. It waits for acceptance, reports rejection logs, staples and validates the ticket, and runs the Gatekeeper assessment.

Apps with differently entitled embedded executables should declare multiple macos_codesign resources: sign each special helper with its own identifier and entitlements first, then sign the outer app with nested false. Apps whose embedded code shares one signing policy can use nested true.

This is an mruby runtime, not system Ruby. Regular expressions, native gems, backticks, exit, and the block form of sh are unavailable. Use raise or abort to stop a task. Delayed notifications are flushed after all requested tasks; subscriptions cannot target resources that are only declared by a later task.


Remote provisioning over SSH

Run a local provision script on a single remote macOS or Linux host:

hola provision provision.rb --host deploy@example.com

# From macOS to Linux: the matching release is downloaded automatically.
hola provision deploy/provision.rb --host deploy@example.com --bundle deploy --sudo

# Or upload a local build instead of the release.
hola provision provision.rb --host deploy@example.com --remote-binary ./hola-linux-x86_64

hola provision provision.rb --host deploy@example.com --port 2222 \
  --identity ~/.ssh/deploy --known-hosts ./known_hosts --phase deploy

Hola connects using embedded libssh2, verifies the server against known_hosts, and authenticates using your SSH agent or the explicit private key. Unknown or changed host keys are rejected. Encrypted private keys should be loaded into your agent first.

--host is looked up in ~/.ssh/config the way OpenSSH does: Host patterns with *, ? and !, Match on host, originalhost, user, localuser and all, Include (relative to ~/.ssh, with globs), and the first obtained value wins. Hola honours HostName, User, Port, IdentityFile (accumulated, none skipped), IdentityAgent (including per-host agent sockets), UserKnownHostsFile and %h/%r/%p/%u/%d/%n tokens. Command-line options override the file. Authentication tries the agent first, then each existing IdentityFile in order; --identity uses that key alone. ProxyJump is refused with a clear error, and ProxyCommand, Match exec, passwords and inventories are not supported.

The same lookup applies to SSH remotes of hola git-clone and the git resource, whose libgit2 transport does not read the file either. An alias such as work:org/repo.git is rewritten with the configured HostName, User and Port before libgit2 sees it (a port turns scp form into ssh://host:port/~/path), while the repository keeps the alias as its remote URL. The agent is tried first, honouring IdentityAgent, then each configured IdentityFile; without any, the default ~/.ssh/id_* keys. The resource's ssh_key property still uses that key alone.

The target needs SSH/SFTP and a POSIX shell with standard Unix utilities; Hola and Ruby do not need to be installed. On a matching OS/architecture, Hola uploads its own executable. Otherwise it downloads the same version for the remote platform from GitHub Releases into the local ~/.cache/hola/remote/ and uploads that; the controller needs internet access, the target does not. A version with no release asset (a development build) falls back to the nightly build, which is revalidated by ETag on every run. Pass --remote-binary to upload a specific local build instead; it must support the same remote protocol. OS and CPU checks do not establish libc or minimum OS compatibility. Binaries are cached by SHA-256 under the remote user's ~/.cache/hola/remote/.

Without --bundle, only the local script is uploaded. With --bundle DIR, the entire directory is uploaded, including hidden files, and the script must be inside it. Symlinks and special files are rejected; regular files retain their executable bit. Use a dedicated deployment directory containing only files you intend to send. The remote working directory is the bundle root (or the temporary script directory for a single script). require_relative resolves against the containing Ruby file in both local and remote provision scripts.

--phase, output mode, data bags and secrets bags apply remotely. Bag URLs are resolved on the controller; the resulting JSON travels over SSH stdin without being added to the remote command line. Remote scripts must currently be local files. --sudo uses sudo -n, so non-interactive permission must already be configured. Output is streamed back and provisioning failures return a nonzero exit status. The worker writes a separate structured completion record, avoiding any need to parse resource output as a protocol.

Temporary workspaces are cleaned after completed runs. If the execution connection is interrupted, Hola reports an unknown outcome, does not retry, and retains the workspace path for inspection because the remote process may still be running.

The isolated SSH integration suite can be run with OpenSSH installed:

bash test/remote_provision.bash /absolute/path/to/hola

Performance

Built with Zig. Stupid Fast.

  • ~6 MB - Single static binary with embedded Ruby interpreter
  • 8ms - Cold start time
  • Zero dependencies - No runtime required
  • Native code - Compiled for your architecture

Commands

hola apply             # Run Brewfile + mise.toml + symlinks
hola provision         # Run provision.rb (advanced)
hola run [task]        # Run Rake-inspired project tasks
hola <task>            # Shorthand for a project task

License

MIT

Stop learning tools. Start coding.

About

Zero-dependency macOS setup tool. Combines Brewfile, mise, and dotfiles management into a single binary.

Topics

Resources

Stars

25 stars

Watchers

2 watching

Forks

Releases

Contributors

Languages