Modular, idempotent dev environment for disposable exe.dev Ubuntu VMs (works on plain Ubuntu 24.04 too).
Clone once, run ./install, re-run anytime. Add or remove tools by editing modules — no need to rebuild a machine image.
Repo: github.com/robertguss/exe-bootstrap (public — clone over HTTPS, no SSH key required)
# 1. SSH into the VM, then clone (HTTPS — no GitHub SSH key needed)
git clone https://github.com/robertguss/exe-bootstrap.git ~/exe-bootstrap
cd ~/exe-bootstrap
./install
exec zsh
./doctorThen complete After install: required & optional steps below. The installer configures tools; it does not complete interactive logins or secret backups for you.
| Goal | How |
|---|---|
| Disposable VMs | One clone + ./install reproduces the environment |
| Easy add/remove | One module file + line in config/modules.enabled |
| Safe re-runs | Modules skip work that is already done |
| No secrets in git | fnox + age; keys stay on disk / offline backup |
| exe.dev friendly | Thin optional setup-exe.dev.sh; primary path is manual clone |
| Area | Tools |
|---|---|
| Shell | zsh as default (login shell + $SHELL + interactive bash auto-switch), Oh My Zsh, plugins (git, zsh-autosuggestions, zsh-syntax-highlighting), starship, atuin, zoxide |
| Secrets | fnox + age (generates a fresh key at ~/.config/fnox/age.txt if missing) |
| Toolchain | mise managing Node (latest LTS), Python, Go, Rust, Ruby, Erlang, Elixir |
| Node | aube package manager (works with existing lockfiles; no global pnpm/bun) |
| Tasks | just (j / jl aliases) |
| Python / Astral | uv, ruff, ty, pipx, cookiecutter (npp alias) |
| CLI | ripgrep, fd, fzf, bat, eza, jq, yq, htop, tree, tmux, trash-cli |
| Ops | lazydocker, bottom (btm), btop, dust, dua |
| Agent DX | ast-grep (sg), gitleaks, git-absorb, difftastic (difft), AGENTS.md templates |
| Git | identity in home/.gitconfig, gh, lazygit, git-delta, hk (hooks binary; per-repo enable) |
| Health | ./doctor — post-install / anytime VM health check |
| Editor | Neovim + stock LazyVim starter (~/.config/nvim) |
| Multiplexer | herdr (hr alias) |
| Agents | Grok Build (x.ai/build), Devin CLI, Pi/Shelley if already on the image |
Dotfiles are symlinked from home/ into $HOME. Machine-specific overrides go in ~/.zshrc.local (created empty; never overwritten by install after first create).
./install # full install (idempotent)
./install --list # modules on/off
./install --only 60-fnox # single module (prefix or full id)
./install --dry-run # show what would run
./install --help
./doctor # health check (pass / warn / fail)Work through this on every new VM. The install script only prints a short reminder; this section is the source of truth.
| Priority | What | Why |
|---|---|---|
| P0 — do immediately | New shell, back up age key | Shell env + secrets unusable without these |
| P1 — before real work | GitHub auth, Grok Build, Devin | Clone private repos / use agents |
| P2 — recommended | LazyVim first launch, atuin, fnox secrets | Editor plugins + history + API keys |
| P3 — optional | herdr habits, starship tweaks, exe.dev sharing | Quality of life |
exec zsh
# or disconnect and SSH back inInstall sets the login shell to zsh, exports SHELL during install, and adds a ~/.bashrc bridge so interactive bash (including tools that spawn $SHELL=/bin/bash after chsh, e.g. herdr panes) auto-execs zsh. New SSH logins get zsh from passwd.
Confirm:
echo $SHELL # should end with /zsh
echo $0 # should be -zsh or zsh
mise --version
node -v # LTS via mise
which grok nvim herdr fnox hk
./doctor # full health check (from the bootstrap repo)Install generates (if missing):
~/.config/fnox/age.txt
This file decrypts secrets. Copy it offline (password manager, encrypted USB, 1Password secure note, etc.). Do not commit it.
# Show public key only (safe to store in fnox recipients)
grep "public key:" ~/.config/fnox/age.txt
# Confirm permissions
ls -la ~/.config/fnox/age.txt # expect -rw------- (600)If you destroy the VM without a backup, encrypted secrets are gone for that key.
These tools are installed but need you to log in. None of this is automated (by design).
Needed for private repos, PRs, gh browse, etc.
gh auth status # see if already logged in
gh auth login # if not — HTTPS or SSH; use your robertguss account
gh auth setup-git # optional: use gh as git credential helperSSH note: first push/clone via git@github.com may need:
ssh-keyscan -t ed25519,rsa github.com >> ~/.ssh/known_hosts
# Ensure this VM has an SSH key loaded or use exe.dev GitHub integrationexe.dev: you can also attach the GitHub integration so private clones work without storing a PAT on the VM.
Install: official script in module 70-agents (x.ai/build).
grok --version
grok # first run: complete browser / API auth as prompted
# or set API key if you use key-based auth — prefer fnox, not plain .zshrcDocs: https://docs.x.ai/build/overview
Update later: grok update
Binary is installed; login is interactive and was skipped in non-interactive installs.
devin --version
devin # complete Cognition / Devin account loginDocs: https://docs.devin.ai/cli
Often preinstalled on exeuntu. Bootstrap only checks presence; it does not reconfigure auth.
command -v pi
# Follow current exe.dev / Shelley docs if login is required on your imageStock LazyVim starter is cloned to ~/.config/nvim. Plugin install happens on first open:
nvim
# Wait for lazy.nvim to fetch plugins; quit when finished (:q)Fonts: icons need a Nerd Font in your local terminal (or VS Code remote terminal font), not on the VM.
Installed non-interactively (no setup questions during ./install):
- Binary via mise (fallback: official setup
--non-interactive) - Local history:
atuin import autoonce per machine (marker under~/.local/state/exe-bootstrap/) - Shell hook:
eval "$(atuin init zsh)"in managed~/.zshrc - Cloud sync is not configured during install
Optional later:
atuin register # new Atuin Cloud account
atuin login # existing account
atuin syncDo not put secrets in .zshrc or this git repo.
Typical first-time flow (after age key exists):
# In a project or in ~/.config/fnox/
fnox init
fnox provider add age --id main --recipients "$(grep 'public key:' ~/.config/fnox/age.txt | awk '{print $NF}')"
fnox secret set SOME_API_KEY --provider main # prompts for value
fnox exec -- some-command # run with secrets
# Shell integration is already in .zshrc via: eval "$(fnox activate zsh)"See config/fnox.toml.example and https://fnox.jdx.dev/.
Migrate keys that used to live in your Mac .zshrc (Gemini, Linear, Hetzner, etc.) into fnox, and rotate any key that was ever committed or pasted into chat.
Already set via linked home/.gitconfig:
| Field | Value |
|---|---|
user.name |
Robert Guss |
user.email |
robertguss@gmail.com |
core.editor |
nvim |
| pager | delta |
Override per-repo with git config user.email ... if needed. Signing keys (GPG/SSH commit signing) are not configured — add manually if you use them.
npp # cookiecutter https://github.com/robertguss/cookiecutter-modern-python-projectsNeeds network; first run may prompt for template variables.
hk is on PATH after install. It does not enable hooks in every repo.
hk --version
cd your-project
hk install --mise # attach hooks for this repo (needs project hk config)See docs/hk.md and https://hk.jdx.dev/.
cd ~/exe-bootstrap # or wherever you cloned
./doctor # exit 1 if required checks failUse after every fresh install and when something “feels wrong.”
| Item | Action |
|---|---|
| herdr | Run herdr or hr to start agent-aware sessions; herdr update to upgrade |
| lazydocker | lazydocker or alias lzd — Docker TUI (daemon must be running) |
| bottom / btop | btm (alias bottom) or btop — live process/CPU monitors |
| dust / dua | dust (alias duh) or dua i — disk usage; helpful on size-limited VMs |
| ast-grep | sg — structural search; see docs/agent-dx.md |
| gitleaks | gitleaks protect --staged before risky commits; optional hk hook |
| git-absorb | Fold fixups into right commits on feature branches |
| difftastic | difft — structural diffs (opt-in; delta remains default pager) |
| AGENTS.md | cp templates/AGENTS.md ./AGENTS.md in each project |
| uv / ruff / ty | Astral stack — see docs/astral.md; aliases rc / rcf / rf for ruff |
| just | Command runner — just / j / jl (just --list); see docs/just.md |
| Starship | Config is linked from home/starship.toml → ~/.config/starship.toml; edit in repo or replace link |
| Personal aliases | Edit home/aliases.zsh in the repo, or use ~/.zshrc.local for this VM only |
| Default shell failed? | sudo chsh -s "$(command -v zsh)" "$USER" then export SHELL=$(command -v zsh) && exec zsh (re-login or herdr server stop if panes still bash) |
| herdr panes are bash? | Stale $SHELL in the herdr server. Fix: export SHELL=$(command -v zsh), herdr server stop, re-run herdr. Re-run ./install --only 10-zsh-omz for the bash→zsh bridge. |
| Docker | Usually preinstalled on exe.dev; user often already in docker group |
| exe.dev HTTP proxy | Dev servers: see Next.js/Vite notes; share port with ssh exe.dev share port <vm> <port> |
| Auto setup-script | Not enabled by default; see Optional first-boot script |
[ ] exec zsh (or new SSH session)
[ ] ./doctor (fix fails, fix until clean enough to work)
[ ] mise / node / nvim / grok / hk respond on PATH
[ ] Backed up ~/.config/fnox/age.txt offline
[ ] gh auth login (if needed)
[ ] grok — completed first-run auth
[ ] devin — completed first-run auth
[ ] nvim — first launch finished plugin install
[ ] atuin import / login (optional)
[ ] Moved API keys into fnox (optional but recommended)
[ ] Nerd Font set on local terminal client
[ ] (per project) hk install --mise when you want hooks
Enabled list: config/modules.enabled (comment out a line to skip).
| Module | Responsibility |
|---|---|
00-base |
apt essentials, trash-cli, dirs (~/bin, ~/Projects, …) |
10-zsh-omz |
zsh, default shell, Oh My Zsh, autosuggestions + syntax-highlighting |
15-starship-atuin |
starship + atuin installers |
20-mise |
mise binary + link config/mise.toml + mise install |
21-languages |
language runtimes, uv, pipx, cookiecutter; erlang before elixir |
22-aube |
aube via mise |
23-astral |
uv, ruff, ty (Astral Python DX) |
25-hk |
hk git hook manager (binary only; per-repo hk install --mise) |
30-cli-tools |
CLI from config/packages.cli (+ apt fallbacks) |
31-git-tools |
gh, lazygit, delta |
32-ops-tools |
lazydocker, bottom, btop, dust, dua |
33-agent-dx |
ast-grep, gitleaks, git-absorb, difftastic (+ templates/) |
40-neovim-lazyvim |
neovim via mise (≥0.11.2; not apt) + ~/.local/bin link + LazyVim starter (won’t clobber existing LazyVim) |
50-herdr |
herdr install/update |
60-fnox |
fnox + age key generation |
70-agents |
Grok Build, Devin CLI; detect Pi/Shelley |
90-dotfiles |
symlink .zshrc, aliases, .gitconfig, starship.toml |
Modules are sourced in filename order. Each defines run().
- Copy an existing file under
modules/→modules/XX-mytool.sh. - Implement
run()using helpers fromlib/common.sh(apt_install,have,log,ok,skip,link_file, …). - Add
XX-mytooltoconfig/modules.enabled. - For mise-managed CLIs, add a line to
config/packages.cliand/orconfig/mise.toml. - Commit, push, run
./installor./install --only XX-mytool.
- Comment out or delete the id in
config/modules.enabled. - Optionally delete the module file and drop tools from
config/mise.toml/packages.cli. - Re-run install (does not always uninstall packages already on disk — remove manually if you need disk space).
| Kind | Where |
|---|---|
| Shared aliases, git identity, starship | home/ in this repo |
| This-VM-only exports, experiments | ~/.zshrc.local |
| Secrets | fnox + age, never git |
exe-bootstrap/
├── install # entrypoint
├── doctor # health check (pass/warn/fail)
├── setup-exe.dev.sh # optional thin first-boot (clone + install)
├── lib/common.sh # logging, apt, symlink helpers
├── modules/ # numbered install steps
├── docs/ # POST_INSTALL.md, hk.md, …
├── config/
│ ├── modules.enabled # feature flags
│ ├── mise.toml # languages + many CLIs
│ ├── packages.cli # CLI list for 30-cli-tools
│ ├── omz-plugins.txt # documentation of OMZ plugins
│ └── fnox.toml.example
├── home/
│ ├── .zshrc # → ~/.zshrc
│ ├── aliases.zsh # → ~/.aliases.zsh
│ ├── .gitconfig # → ~/.gitconfig
│ └── starship.toml # → ~/.config/starship.toml
└── README.md # this file
Primary workflow is still SSH → clone → ./install.
If you later want auto-bootstrap on ssh exe.dev new:
# One-off for a new VM (script stays small; clones this public repo over HTTPS)
export EXE_BOOTSTRAP_REPO=https://github.com/robertguss/exe-bootstrap.git
cat setup-exe.dev.sh | ssh exe.dev new --setup-script /dev/stdinDefault setup-exe.dev.sh already uses the HTTPS URL.
Do not put secrets in the setup script. After boot, still do the P0/P1 steps (age backup, agent logins).
- This repo is public — never commit secrets, even “temporarily.”
- Never commit
age.txt,.env, or API keys. .gitignoreblocks common secret paths; double-check beforegit add -f.- Rotate any credential that lived in plain shell config or chat logs.
- Prefer fnox (or exe.dev integrations) over
export KEY=...in dotfiles. - Git identity in
home/.gitconfigis personal (name/email); forks should edit it.
| Symptom | Fix |
|---|---|
| Commands missing after install | exec zsh or new SSH session; ensure ~/.local/bin on PATH |
mise tools not found |
eval "$(mise activate zsh)" then mise reshim |
Elixir / erl: not found |
./install --only 21-languages (erlang must install first) |
chsh failed or install hung on “setting default shell” |
Fixed: install never prompts. Ctrl+C if stuck on old clone, git pull, re-run. Manually: sudo chsh -s "$(command -v zsh)" "$USER" then exec zsh |
| LazyVim / nvim errors | First launch needs network; try nvim again; backup and re-run ./install --only 40-neovim-lazyvim only if config isn’t customized |
nvim is system 0.9.x / LazyVim says install nvim |
Ubuntu apt is too old; bootstrap installs via mise + ~/.local/bin/nvim. Re-run ./install --only 40-neovim-lazyvim, then hash -r / new shell. Confirm: which nvim → ~/.local/bin/nvim, nvim --version ≥ 0.11.2 |
gh / git SSH host key errors |
ssh-keyscan -t ed25519,rsa github.com >> ~/.ssh/known_hosts |
| Devin “login canceled” during install | Normal in non-interactive runs; run devin yourself |
| Dotfile conflict | Install backs up to *.bak.<timestamp> then symlinks managed files |
| Want stock OMZ zshrc back | Remove symlink and restore a .bak.* file |
Re-run everything safely:
cd ~/exe-bootstrap && git pull && ./install- exe.dev docs
- exe.dev customization / setup scripts
- mise · fnox · aube
- Grok Build · Devin CLI
- LazyVim · herdr · starship · atuin
Personal bootstrap, public for easy cloning. Use/adapt as you like; no warranty.