Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

exe-bootstrap

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)


Quick start (fresh VM)

# 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
./doctor

Then complete After install: required & optional steps below. The installer configures tools; it does not complete interactive logins or secret backups for you.


Design goals

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

What gets installed

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).


Commands

./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)

After install: required & optional steps

Work through this on every new VM. The install script only prints a short reminder; this section is the source of truth.

Priority overview

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

P0 — Immediately after ./install

1. Start a new shell

exec zsh
# or disconnect and SSH back in

Install 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)

2. Back up the age private key (required if you use fnox)

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.


P1 — Auth & accounts (interactive)

These tools are installed but need you to log in. None of this is automated (by design).

GitHub CLI (gh)

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 helper

SSH 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 integration

exe.dev: you can also attach the GitHub integration so private clones work without storing a PAT on the VM.

Grok Build (coding agent)

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 .zshrc

Docs: https://docs.x.ai/build/overview
Update later: grok update

Devin CLI

Binary is installed; login is interactive and was skipped in non-interactive installs.

devin --version
devin                   # complete Cognition / Devin account login

Docs: https://docs.devin.ai/cli

Pi / Shelley (exe.dev image agents)

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 image

P2 — Recommended one-time setup

LazyVim / Neovim (first launch)

Stock 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.

Atuin (shell history)

Installed non-interactively (no setup questions during ./install):

  • Binary via mise (fallback: official setup --non-interactive)
  • Local history: atuin import auto once 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 sync

fnox secrets (API keys, tokens)

Do 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.

Git identity

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.

Cookiecutter / npp alias

npp    # cookiecutter https://github.com/robertguss/cookiecutter-modern-python-projects

Needs network; first run may prompt for template variables.

hk (git hooks) — binary only

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/.

./doctor

cd ~/exe-bootstrap   # or wherever you cloned
./doctor             # exit 1 if required checks fail

Use after every fresh install and when something “feels wrong.”


P3 — Optional / as needed

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

Post-install checklist (copy/paste)

[ ] 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

Modules reference

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().


Add or remove a tool

Add

  1. Copy an existing file under modules/modules/XX-mytool.sh.
  2. Implement run() using helpers from lib/common.sh (apt_install, have, log, ok, skip, link_file, …).
  3. Add XX-mytool to config/modules.enabled.
  4. For mise-managed CLIs, add a line to config/packages.cli and/or config/mise.toml.
  5. Commit, push, run ./install or ./install --only XX-mytool.

Remove

  1. Comment out or delete the id in config/modules.enabled.
  2. Optionally delete the module file and drop tools from config/mise.toml / packages.cli.
  3. Re-run install (does not always uninstall packages already on disk — remove manually if you need disk space).

Personal vs shared

Kind Where
Shared aliases, git identity, starship home/ in this repo
This-VM-only exports, experiments ~/.zshrc.local
Secrets fnox + age, never git

Repository layout

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

Optional exe.dev first-boot script

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/stdin

Default 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).


Security

  • This repo is public — never commit secrets, even “temporarily.”
  • Never commit age.txt, .env, or API keys.
  • .gitignore blocks common secret paths; double-check before git 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/.gitconfig is personal (name/email); forks should edit it.

Troubleshooting

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

Related links


License

Personal bootstrap, public for easy cloning. Use/adapt as you like; no warranty.

About

Modular idempotent dev environment bootstrap for disposable exe.dev VMs

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages