Dotfiles for my macOS work host plus an isolated Ubuntu LTS dev VM.
The host is intentionally minimal: Homebrew, chezmoi, the ,* helper
scripts, a curated set of GUI casks, JFrog credential plumbing, and
nb for notes. All development — editor, language runtimes, LSPs,
git tooling — lives inside the VM.
bash <(curl -fsSL https://raw.githubusercontent.com/saimon-moore/home-sweet-home/main/bootstrap/host/install.sh)Idempotent — safe to re-run.
Before running the installer:
- macOS Sonoma or newer on an Apple Silicon Mac.
install.shrefuses to run elsewhere. - Xcode Command Line Tools — the Homebrew install in
install.shwill trigger the CLT installer if missing. Click through it when prompted and re-run if needed. - Backed-up SSH key reachable from the new machine (iCloud,
external drive, password manager, etc.). After the installer
finishes you'll copy this into
~/.ssh/:id_ed25519(+.pub) — Onlyfy/XING identity and signing key. Used directly by the chezmoi-managed~/.ssh/configfor bothgithub.comandsource.xing.com.
- Work IT will push the MDM-managed apps separately via
Company Portal / Self Service — Cortex XDR, FortiClient, Iru
Self Service, uniFLOW, authigo2, BeeCore, Phrase desktop. See
ADAPTING.md→ Manual installs. - A few chezmoi prompt answers ready:
- Git author name + email
- GitHub username (
saimon-moore) - Whether this machine will be used for
develop(nofor the host,yesinside the VM later) - Whether you want
opencodeinstalled
- Installs Homebrew (non-interactive) if missing.
- Installs
chezmoivia Homebrew. chezmoi init --apply saimon-moore/home-sweet-homewhich:- Renders every templated dotfile into
$HOME. - Fires
run_once_after_host-brew-bundle.sh.tmpl→ runsbrew bundleagainstbootstrap/host/Brewfile(installs the curated brew formulae and 35 GUI casks). - Fires
run_once_after_nb-notebooks-bootstrap.sh.tmpl→ clones thexingnb notebook fromgit@github.com:saimon-moore/nbinto~/.nb/xing. - Drops
~/Desktop/home-sweet-home.md(a daily-use quick reference).
- Renders every templated dotfile into
- Runs
,verifyso you see a colour-coded pass/fail summary before the next-steps instructions print.
The Homebrew bundle step can take a while on first run and will occasionally pause for macOS to ask permission for a cask install (accessibility, input monitoring, etc.). Approve and let it resume.
- Open a new terminal. Shell PATH changes and integrations load on shell start, not mid-session.
- Drop your SSH keys into
~/.ssh/:The nb bootstrap hook clones overchmod 700 "$HOME/.ssh" cp <backup>/id_ed25519 "$HOME/.ssh/" cp <backup>/id_ed25519.pub "$HOME/.ssh/" chmod 600 "$HOME/.ssh"/id_ed25519 chmod 644 "$HOME/.ssh"/id_ed25519.pub
git@github.com:...and the chezmoi-managed~/.ssh/configroutes github.com through this key, so the file must be in place beforechezmoi apply(or re-apply afterwards with,chezmoi-init). Confirm with,verify. - Create the dev VM:
,create-vm. This provisions lima with the Ubuntu LTS template defined inlima/dev-ubuntu.yaml. - Open the VM:
,dev. - Bootstrap
devinside the VM (one time):Answermkdir -p "$HOME/.ssh" chmod 700 "$HOME/.ssh" ssh-keygen -q -t ed25519 -N '' -C "dev@dev" -f "$HOME/.ssh/id_ed25519" chezmoi init --apply saimon-moore/home-sweet-home mise install chezmoi apply
develop=yesand use the same identity as on the host. - Sync JFrog credentials into the VM when you need private artifact access — see the JFrog section below.
- MDM / manual installs from
ADAPTING.md→ Manual installs. Most of these are delivered by IT; Paseo is the only one you install by hand.
,verify on the host checks:
- Homebrew, chezmoi, git installed
- Spot-check of Brewfile CLIs (
eza,fzf,rg(from ripgrep),fd,lazygit,lima,gh,jq,bat,nb) chezmoi status— no pending changes- Git aliases loaded (
git pamet al.) +commit.gpgsign = true ~/.ssh/configreferencesid_ed25519and the key file is present~/.nb/xingis a real git repo- lima dev VM exists
~/Desktop/home-sweet-home.mdis in place
Exits non-zero on any hard failure so you can wire it into scripts or CI.
See ~/Desktop/home-sweet-home.md — that's where the real cheatsheet
lives now. Short version:
,deventers the dev VM shell (a zellij session).,cheatsheetprints the full terminal-tool keybinding reference.,chezmoi-updatepulls this repo and applies.,verifyre-runs the health check.
Daily commands, VM networking, the terminal IDE stack, nb basics,
and the JFrog sync flow are all covered on the Desktop README.
Use ,vm-cp for one-off copies and ,vm-rsync for larger or
repeated syncs.
vm: prefixes a path inside the Lima guest. Paths without that
prefix are on the macOS host.
Quick copy with ,vm-cp:
,vm-cp ./notes.md vm:~/notes.md # host -> VM
,vm-cp vm:~/build.log ./ # VM -> host
,vm-cp -r ./dist vm:~/deploy # recursive push
,vm-cp -r vm:~/code/myproj ./myproj # recursive pullThis wraps limactl copy, so it's the easiest choice for a single
file or a small directory.
Incremental sync with ,vm-rsync:
,vm-rsync -avz --progress ./src/ vm:~/dest/
,vm-rsync -avz --delete ./src/ vm:~/dest/
,vm-rsync --dry-run -avz ./src/ vm:~/dest/
,vm-rsync -avz vm:~/code/myproj/ ./myproj/Use ,vm-rsync when you want rsync features like --dry-run,
--delete, --exclude, resumable transfers, or fast repeated syncs
of a large tree.
The ,zagent zellij layout opens a ,agent pane, which routes to
whichever AI coding harness is currently selected. Switch at any time
with ,agent-select.
- opencode — installed unconditionally, both on the host via the
Brewfile (
brew "opencode") and inside the dev VM via mise (opencode = "latest"). - codex — installed on the host via
cask "codex"and in the VM via mise ("npm:@openai/codex" = "latest").
Both are always available. The shim just decides which one launches
when you invoke ,agent.
,agent-select # show the current selection + availability of each
,agent-select codex # persist a selection (sticks across shells)
,agent-select --clear # drop the persistent selection; ,agent falls back to opencodeOne-shot override without persisting:
AGENT_HARNESS=codex ,agent,zagent always honours whatever ,agent-select currently says.
- Install its CLI. Add a line to
bootstrap/host/Brewfile(macOS host) and/orchezmoi/dot_config/mise/config.toml.tmpl(dev VM) — e.g."npm:@some-org/foo-agent" = "latest". - Register the binary name. Append it to
KNOWN_HARNESSESnear the top ofchezmoi/dot_local/bin/executable_,agent-select. chezmoi apply.
Agent skills live centrally in ~/.agent/skills/ and are managed by
openskills. For Codex
compatibility, chezmoi also keeps ~/.agents/skills symlinked to the
same directory. This repo commits:
chezmoi/dot_agent/openskills-manifest.txt— the list of skill names this machine should have.chezmoi/dot_agent/skills/<name>/.openskills.json— the origin metadata openskills needs to fetch each skill.
After every chezmoi apply, the
run_onchange_after_openskills-bootstrap.sh.tmpl hook reconciles disk state
against the manifest:
- reads the unique
sourcevalues from the committed.openskills.jsonfiles, - runs
npx openskills install <source> --universalfor each, - prunes any skill dir on disk whose name is not in the manifest,
- regenerates
~/.agent/AGENTS.mdvianpx openskills sync, - refreshes
~/.agents/skillsto point at~/.agent/skills.
Run it manually with chezmoi apply or just `npx openskills install
JFrog credentials stay sourced from 1Password on the host and are
copied explicitly into the VM when needed. The host shell provides
,jfrog_oidc_env, which exports JFROG_OIDC_USER and
JFROG_OIDC_TOKEN.
,jfrog_oidc_env
,sync-jfrog-to-vm --host your.jfrog.example.comIf Ruby gems use a different host than the primary JFrog host, pass
--ruby-host too.
To also wire npm up to a JFrog npm registry, pass --npm-registry:
,sync-jfrog-to-vm \
--host your.jfrog.example.com \
--npm-registry https://your.jfrog.example.com/artifactory/api/npm/npm-virtual/For a scoped registry (recommended when JFrog only hosts your own
packages and public packages still come from npmjs), add
--npm-scope company. The sync then writes
@company:registry=... instead of a global registry=... line.
The sync writes two files inside the VM:
~/.config/home-sweet-home/jfrog-oidc.env— sourced automatically by the VM shell. ExposesJFROG_OIDC_USER,JFROG_OIDC_TOKEN,JFROG_HOST,JFROG_REALM, and a BundlerBUNDLE_<host>variable.~/.npmrc— only touched when--npm-registryis passed. Auth lines go between# BEGIN home-sweet-home jfrog npm auth/# END home-sweet-home jfrog npm authsentinels, so re-running the sync replaces the block idempotently and leaves the rest of the file alone..npmrcis managed ascreate_in chezmoi (created once withignore-scripts=true, never overwritten), so the auth lines persist acrosschezmoi apply.
Earlier versions of this repo's SSH config defined a github-onlyfy
alias that routed git@github-onlyfy:... URLs through the Onlyfy
key. The current setup drops the alias and instead points
Host github.com directly at id_ed25519, since this machine
only ever needs that one key for github.com.
If you restore work repos from a backup that was originally cloned
under the old setup, their remotes will still look like
git@github-onlyfy:owner/repo.git and git fetch/git push will
fail once the alias is gone. Use:
,migrate-github-onlyfy-remotes ~/code # dry-run, prints proposed rewrites
,migrate-github-onlyfy-remotes --apply ~/code # actually rewrites the URLsThe script walks the given directory recursively, finds every git
repo, and for each remote URL of the form git@github-onlyfy:...
runs git remote set-url to replace the host with github.com.
Both fetch and push URLs are updated. Safe to re-run; only matching
URLs are touched.
# Install Homebrew (https://brew.sh) yourself, then:
brew install chezmoi
chezmoi init --apply saimon-moore/home-sweet-homechezmoi can read this repo directly from GitHub because the repo
root has .chezmoiroot pointing at chezmoi/.
Every OAuth-based harness (codex, opencode, claude-code) spins up a
local HTTP listener on 127.0.0.1:<port> and redirects the browser
to it after login. When the harness runs in the VM, the host browser
can't reach that listener, so the redirect lands on a connection
error. No port forwarding needed — the callback URL's query
string carries the token, so hitting the URL from inside the VM
completes the flow.
Recipe:
-
Start login in the VM. Depending on harness:
- codex:
codex login(or just runcodex/,agentand follow the prompt on first use). - opencode:
/connectinside an opencode session. - claude-code:
/logininside claude-code (or first run).
The harness prints an auth URL.
- codex:
-
Open the URL on the host Mac and complete sign-in.
-
The browser redirects to
http://localhost:<port>/callback?...and shows a connection error. Copy the full URL from the address bar. -
In any VM shell (a new zellij pane,
,devin another tab, etc.):curl '<paste-the-full-localhost-url-here>'
The harness's listener inside the VM receives the callback and finishes auth.
,verifyreports failures → it names what's missing. Usually the fix ischezmoi applyorbrew bundle --file ~/.local/share/chezmoi/bootstrap/host/Brewfilein a fresh shell.chezmoi statusnon-empty →chezmoi diffto inspect, thenchezmoi apply.- VM won't start →
limactl stop dev; limactl delete dev; ,create-vm. VMs from before the vzNAT change need this recreate. git pull/git pushhangs inside the VM → on some host/VPN/Wi-Fi paths, PMTU discovery breaks for Lima'slima0interface and SSH stalls atexpecting SSH2_MSG_KEX_ECDH_REPLY. Fresh VMs now install a boot-time fix that setslima0to MTU 1280. Existing VMs can apply the same workaround immediately withsudo ip link set dev lima0 mtu 1280, then either recreate the VM with,create-vmor install the same change permanently.- nb notebook clone failed → the SSH key wasn't in place when
the hook ran. Fix the key file, then either re-run
,chezmoi-initor touch the script to re-hash it andchezmoi apply. - Cask install hung on permission prompt → re-run
brew bundleafter approving; casks are idempotent. chezmoi initwith username-only shorthand resolves elsewhere → always usesaimon-moore/home-sweet-homeexplicitly.
ADAPTING.md— customization guide: every prompt, every gate, manual-install catalog, opinionated choices at a glance.bootstrap/host/Brewfile— the full host manifest.bootstrap/host/install.sh— the one-line installer (you can read it end-to-end in <100 lines).