Portable CLI/development configuration managed with GNU Stow. The bootstrap is interactive and rerunnable. It supports Arch/CachyOS (pacman), Debian/Ubuntu, Fedora, and openSUSE; macOS requires both Homebrew and Homebrew Bash 4.3 or newer.
For the safest flow, download and review the script before running it:
curl -fsSLO https://raw.githubusercontent.com/fjordnode/dotfiles/main/bootstrap.sh
less bootstrap.sh
bash bootstrap.shRunning it directly is also supported; the checklist reads from the terminal even when the script itself is piped:
curl -fsSL https://raw.githubusercontent.com/fjordnode/dotfiles/main/bootstrap.sh | bashmacOS ships an older Bash. Install Homebrew Bash first and use it explicitly, especially for a piped install:
brew install bash
curl -fsSL https://raw.githubusercontent.com/fjordnode/dotfiles/main/bootstrap.sh | "$(brew --prefix)/bin/bash"The first screen selects the setup type:
- CLI (the safe default) hides graphical desktop applications and configs.
- Desktop shows the complete catalog supported by the detected package manager, including available Wayland and compositor tools. Configs remain selectable for applications installed separately.
Then use Space to select items, ↑/↓ (or j/k) to move, Enter to accept, Esc to return to the previous step, and q to cancel. Applications are grouped by category and alphabetized within each category. The remaining checklists cover:
- system packages to install;
- config packages to link with Stow—the matching configs for selected applications are preselected automatically;
- optional setup actions such as an APT system upgrade, Oh My Zsh, changing the login shell, or removing obsolete links previously created by these dotfiles.
The config suggestions are only defaults: you can toggle any of them before continuing. An explicit --configs list remains authoritative in automated runs.
Before changing anything, the script validates required tools and checks selected packages against current APT or pacman metadata, prints the complete plan—including an APT metadata refresh when needed—and asks for confirmation. Fatal execution errors are repeated in the final installation summary. It does not:
- overwrite conflicting files or use
stow --adopt; optional conflict backups are explicit and preserved under~/.local/state; - pull or modify an existing Git checkout;
- install unselected package bundles;
- change the login shell unless selected;
- run plugin installers unless selected;
- run as root.
Stow first performs a simulation for each selected config. A conflicting config is skipped while the other selected configs continue.
Arch packages use pacman -S --needed without a separate pacman -Sy, avoiding an unsafe partial-upgrade database refresh. Update the machine normally before bootstrapping if its package database is stale:
sudo pacman -Syu
./bootstrap.shInteractive Pacman transactions are attached directly to /dev/tty, so provider choices and the final [Y/n] confirmation remain visible even when the bootstrap was piped or launched through a terminal harness. Selecting Yazi explicitly installs the small ttf-nerd-fonts-symbols provider instead of asking users to choose among every Nerd Font package.
APT is used for foundational and system-integrated packages. The optional APT system upgrade action runs apt-get update followed by apt-get upgrade -y before installing selected packages; it is disabled by default and preserves existing package configuration files. Fast-moving CLI tools—Starship, bat, eza, fd, fzf, Neovim, ripgrep, Yazi, and zoxide—come from their latest official GitHub release instead of Debian's older packages. They are installed version-by-version under ~/.local/share/dotfiles-tools/ and linked from ~/.local/bin/.
The bootstrap does not add third-party APT repositories or overwrite an unmanaged file in ~/.local/bin. Desktop applications unavailable from the standard APT metadata are omitted from the application selector, while their configs remain available. Release checksums are verified when an upstream release provides a matching <asset>.sha256 file. Supported release architectures are x86-64 and ARM64.
# Interactive preview; package installation and file changes are skipped
./bootstrap.sh --dry-run
# Fully explicit, non-interactive run
./bootstrap.sh --non-interactive \
--packages 'git stow zsh neovim fzf ripgrep fd' \
--configs 'git zsh nvim scripts' \
--actions 'oh-my-zsh zsh-plugins nvim-plugins'Use none for an empty list. Non-interactive mode requires all three lists, which prevents an omitted variable from unexpectedly selecting defaults; Pacman also receives --noconfirm in that mode. PACKAGES, CONFIGS, ACTIONS, NONINTERACTIVE=1, and DRY_RUN=1 are equivalent environment controls.
Run the regression smoke tests after changing the bootstrap. Destructive-path checks use temporary home directories and fake package-manager commands:
tests/bootstrap-smoke.shThe script requires Bash 4.3 or newer. Current Linux distributions satisfy this requirement. On macOS, install Homebrew Bash and invoke the script with "$(brew --prefix)/bin/bash"; stock macOS Bash 3.2 is unsupported.
For Termux on Android devices:
curl -fsSL https://raw.githubusercontent.com/fjordnode/dotfiles/main/bootstrap-android.sh | bashThis will:
- Install all required packages using
pkg(Termux package manager) - Set up Android storage access
- Clone this repository to
~/dotfiles - Create symlinks using GNU Stow
- Install oh-my-zsh with plugins (autosuggestions, syntax-highlighting, completions)
- Install Starship prompt
- Create zsh launcher script (since
chshis not available) - Configure Termux-specific optimizations
If you prefer to see what's happening:
# Download the bootstrap script
curl -fsSL https://raw.githubusercontent.com/fjordnode/dotfiles/main/bootstrap.sh > bootstrap.sh
# Review it
cat bootstrap.sh
# Run it
bash bootstrap.sh# Download the Termux bootstrap script
curl -fsSL https://raw.githubusercontent.com/fjordnode/dotfiles/main/bootstrap-android.sh > bootstrap-android.sh
# Review it
cat bootstrap-android.sh
# Run it
bash bootstrap-android.sh- zsh - Shell configuration with oh-my-zsh
- nvim - Neovim configuration with Lazy.nvim and plugins
- git - Git configuration and aliases
- bat/eza/yazi - CLI tool configuration
- starship - Cross-shell prompt
- herdr - Terminal workspace manager runtime and configuration
- agents - Shared
~/.agents/skillsused by Pi, Claude, and other compatible agents - claude - Claude configuration, available as an explicit config selection
- pi - Pi coding-agent runtime plus settings, extensions, themes, and pinned package declarations; credentials and runtime state remain local
- niri/hypr/noctalia - Desktop configs, each selected independently
- scripts - Small shell helpers for SSH forwarding, archives, OSC 52, safer removal, and directory listing
- vpn-split - Advanced Linux-specific VPN helpers, never selected by default
dotfiles/
├── zsh/
│ └── .zshrc
├── nvim/
│ └── .config/
│ └── nvim/
│ ├── init.lua
│ └── lua/
├── git/
│ └── .gitconfig
├── starship/
│ └── .config/
│ └── starship.toml
├── bat/
├── eza/
├── yazi/
├── herdr/
│ └── .config/
│ └── herdr/
│ └── config.toml
├── agents/ # shared agent skills
├── claude/ # Claude config
├── pi/ # Pi config and custom resources
├── niri/ # Niri desktop config
├── hypr/ # Hyprland desktop config
├── noctalia/ # Noctalia V5 config
├── scripts/ # portable helper scripts
└── vpn-split/ # asahi transitional profile
After installation, your config files are symlinked from ~/dotfiles. To update configs:
- Edit the files in
~/dotfiles/[package]/ - Commit and push changes:
cd ~/dotfiles
git add .
git commit -m "Update configs"
git pushDotfiles and applications update separately so a tool update never changes your configuration checkout unexpectedly.
Update the repository after reviewing its incoming changes:
cd ~/dotfiles
git pull --ff-only
./bootstrap.shUpdate applications installed under ~/.local by the bootstrap, plus uv, Herdr, and Pi:
./update-tools.sh --dry-run
./update-tools.shThe updater detects what this bootstrap previously installed, prints the plan, and asks for confirmation. It uses the same versioned release installer, updates Neovim plugins, and runs uv self update, herdr update, and pi update. Normal bootstrap runs only install missing Neovim plugins; they do not update existing plugins. It deliberately does not update OS-managed packages; use the native system workflow such as sudo apt update && sudo apt upgrade or sudo pacman -Syu for those.
Host-specific Git settings belong in ~/.gitconfig.local, which the tracked
Git config includes automatically. This keeps machine-only paths such as
safe.directory entries out of the portable repository.
For automation, SETUP=cli|desktop, PACKAGES, CONFIGS, and ACTIONS are supported; the lists accept comma- or space-separated IDs. Set all three lists together with NONINTERACTIVE=1. DRY_RUN=1, REPO, and DEST are also supported. Run ./bootstrap.sh --help for examples.
Select the herdr application to run Herdr's official curl -fsSL https://herdr.dev/install.sh | sh installer. Its config is selected automatically. When Herdr is already available, bootstrap uses the built-in herdr update command instead.
Select the pi application to run Pi's official curl -fsSL https://pi.dev/install.sh | sh installer. Its declarative pi config is selected automatically. The installer is skipped when pi is already available. Run pi and use /login separately on each machine; bootstrap never handles Pi authentication.
The Stow package deliberately uses --no-folding, keeping generated files such as auth.json, sessions, package checkouts, caches, and logs under the real ~/.pi/agent directory rather than inside this repository. Pi auto-mode safety controls also remain machine-local and are excluded from Stow/Git. Shared skills are managed by the separate agents Stow package for reuse across coding agents.
The Pi package also includes models.json, the extensions/codex-auth/ extension,
and account-import scripts with fake-credential tests. The current settings use
codex-auth; install its CLI separately and authenticate on each host. See
the extension README.
Do not enable packages/pi-accounts-local alongside it: the legacy account manager
can override the selected Codex credentials. Its source and migration scripts are
retained, but it is not loaded by the current settings.
Keep configuration and source files linked to this checkout with stow --no-folding.
Pi or other tools can replace symlinks with regular files when saving settings;
check both git status and the live links after updates. Preserve and compare
conflicting files before relinking—do not blindly use stow --adopt.
The legacy import script resolves dependencies relative to its real source path.
If needed, install them with a Pi-compatible Node/npm in
~/dotfiles/pi/.pi/agent/packages/pi-accounts-local using npm ci --ignore-scripts.
Dependencies are ignored by Git. Never run the importer against real credentials
just to validate an installation; use its .test.mjs file instead.
To add a new program's configuration:
- Create a new directory in
~/dotfiles - Mirror the expected structure from
$HOME - Add its ID and label to the config checklist in
bootstrap.sh
Example for adding vim config:
cd ~/dotfiles
mkdir -p vim
mv ~/.vimrc vim/.vimrc
stow -t "$HOME" vim
git add vim
git commit -m "Add vim configuration"The vpn-split package is Linux-specific and is not selected by default. It stows:
~/.local/bin/novpn~/.local/bin/wg-split-up~/.local/bin/wg-split-down~/.local/bin/wg-kill-switch-off~/.local/bin/wg-status-proton~/.local/bin/wg-status-home~/.config/noctalia/wireguard-widgets.tomland its scripted widget runtime~/.local/bin/wg-status-killswitch~/.local/bin/wg-toggle-proton~/.local/bin/wg-toggle-home~/.local/bin/wg-toggle-killswitch~/.config/systemd/user/novpn.slice~/.config/systemd/user/novpn-anchor.service~/.local/share/wg-split-tunnel/50-wg-split-tunnel~/.local/share/wg-split-tunnel/wg-split-tunnel.md
Root-managed files still need a manual install step:
sudo install -m 755 ~/.local/share/wg-split-tunnel/50-wg-split-tunnel \
/etc/NetworkManager/dispatcher.d/50-wg-split-tunnelYou may also want to name table 26642 in /etc/iproute2/rt_tables:
echo '26642 novpn' | sudo tee -a /etc/iproute2/rt_tablesWhen a selected config conflicts with an existing real file, interactive runs offer to move only the conflicting targets into:
~/.local/state/dotfiles-backups/<timestamp>/<stow-package>/
The bootstrap then repeats its Stow simulation before applying anything. It never uses stow --adopt. Declining the backup skips that config and records the exact conflict in the final installation summary.
For explicit non-interactive migrations:
BACKUP_CONFLICTS=1 NONINTERACTIVE=1 \
PACKAGES=none CONFIGS=pi ACTIONS=none ./bootstrap.shYou can also pass --backup-conflicts during an interactive or automated run.
If zsh plugins aren't working:
git clone https://github.com/zsh-users/zsh-completions ~/.oh-my-zsh/custom/plugins/zsh-completions
git clone https://github.com/zsh-users/zsh-autosuggestions ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-syntax-highlighting ~/.oh-my-zsh/custom/plugins/zsh-syntax-highlightingIf Neovim plugins aren't installed:
nvim --headless "+Lazy! sync" +qa- Linux (Debian/Ubuntu, Fedora/RHEL, Arch, openSUSE)
- macOS (with Homebrew and Homebrew Bash 4.3+)
- Termux (Android)
- Docker containers
- Unraid (via Docker container)
MIT