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.
Create username/dotfiles on GitHub with three simple files:
brew "tmux"
brew "neovim"
cask "ghostty"
cask "zed@preview"
cask "orbstack"Homebrew integration.
[tools]
node = "24"
python = "3.14"Lock your tool versions. Never drift.
dotfiles/.zshrc → ~/.zshrc
dotfiles/.gitconfig → ~/.gitconfig
Symlink mapping. Dead simple.
No custom syntax. No learning required.
curl -fsSL https://hola.ac/install | bashThis downloads the binary for your architecture (arm64/x86_64) and installs it to the current directory.
brew install ratazzi/hola/holaDownload 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/- 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
# One command to set up everything
hola apply
# Packages, tools, dotfiles, and system settings - all done90% 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") }
endConfigure 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
endFeatures:
- ✅ Type-safe: Boolean, Integer, Float, String - automatically handled
- ✅ Idempotent: Only updates when values differ
- ✅ Per-host domain:
current_host truetargetsdefaults -currentHost - ✅ Auto-restart: Automatically restarts Finder/Dock/SystemUIServer when needed
- ✅ No manual
defaultscommands: 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.
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 => :buildhola 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 scriptsNormal 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
endThe 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.
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.rbA 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 => :releasehola 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.
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
endThe five resources cover the reusable release boundary:
xcode_buildconstructsxcodebuildinvocations without shell quoting and usescreatesas its convergence marker.macos_signing_certificatecreates or unlocks a CI keychain, imports a P12 only when the identity is absent, and configures key access forcodesign.macos_codesignsigns 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 oncodesign --deep.macos_dmgstages an app withditto, 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_notarizesupports 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.
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 deployHola 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- ~6 MB - Single static binary with embedded Ruby interpreter
- 8ms - Cold start time
- Zero dependencies - No runtime required
- Native code - Compiled for your architecture
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 taskMIT
Stop learning tools. Start coding.