https://github.com/program-the-brain-not-the-heartbeat/dotfiles
π‘dotfiles - Organize the chaos
https://github.com/program-the-brain-not-the-heartbeat/dotfiles
bash dotfiles shell ubuntu
Last synced: 3 months ago
JSON representation
π‘dotfiles - Organize the chaos
- Host: GitHub
- URL: https://github.com/program-the-brain-not-the-heartbeat/dotfiles
- Owner: program-the-brain-not-the-heartbeat
- Created: 2025-11-16T13:07:54.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2026-03-31T01:43:22.000Z (3 months ago)
- Last Synced: 2026-03-31T04:50:56.181Z (3 months ago)
- Topics: bash, dotfiles, shell, ubuntu
- Language: Shell
- Homepage:
- Size: 1.22 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: .github/CONTRIBUTING.md
Awesome Lists containing this project
README
# Modular Dotfiles System
A robust, pure-Bash, symlink-based dotfiles system designed for modularity and role-based configuration across Linux, macOS, and WSL2.
## Quickstart
To quickly download and install these dotfiles into a new server, copy/paste the following command:
`bash <(curl -fsSL https://raw.githubusercontent.com/program-the-brain-not-the-heartbeat/dotfiles/main/bootstrap.sh)`
The bootstrap command will clone the repo and run [install.sh](install.sh).
## Platform setup
### Ubuntu / Debian (bare metal or VM)
Prerequisites: `git` and `curl`.
```bash
sudo apt update && sudo apt install -y git curl
bash <(curl -fsSL https://raw.githubusercontent.com/program-the-brain-not-the-heartbeat/dotfiles/main/bootstrap.sh)
```
### macOS
Requires [Homebrew](https://brew.sh). Xcode Command Line Tools (includes `git`) will be prompted automatically.
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
bash <(curl -fsSL https://raw.githubusercontent.com/program-the-brain-not-the-heartbeat/dotfiles/main/bootstrap.sh)
```
**Notes:**
- `brew` is used as the package manager (`DOTFILES_PKG_MANAGER=brew`)
- systemd features (timers, services) are skipped β `has_systemd` returns false
- GNU coreutils are **not** required; the scripts use POSIX-compatible flags where possible
### Windows 10/11 (WSL2)
These dotfiles run inside WSL2, not native Windows. Requires Windows 10 version 2004+ or Windows 11.
**1. Install WSL2 with Ubuntu**
Open PowerShell as Administrator:
```powershell
wsl --install -d Ubuntu
```
Restart if prompted, then launch Ubuntu from the Start menu.
**2. Enable systemd in WSL2**
systemd is required for timers and services (e.g. gmail-backup). Create or edit `/etc/wsl.conf` inside WSL:
```bash
sudo tee /etc/wsl.conf > /dev/null << 'EOF'
[boot]
systemd=true
EOF
```
Then restart WSL from PowerShell:
```powershell
wsl --shutdown
```
Reopen Ubuntu and verify:
```bash
systemctl --version # should print the systemd version
```
**3. Install dotfiles**
```bash
sudo apt update && sudo apt install -y git curl
bash <(curl -fsSL https://raw.githubusercontent.com/program-the-brain-not-the-heartbeat/dotfiles/main/bootstrap.sh)
```
**WSL-specific notes:**
- The installer auto-detects WSL and warns if systemd is not enabled
- [sbin/](sbin/) scripts (disk mount/unmount/scrub) are not applicable in WSL
- Paths use the Linux filesystem (`/home/user/`), not `/mnt/c/`; keep dotfiles on the Linux side for performance
- `~/.bashrc.local` is the right place for WSL-specific PATH entries (e.g. adding `/mnt/c/Windows/System32` for interop)
### Windows 10/11 (Native)
For native Windows machines, [bootstrap-windows.ps1](bootstrap-windows.ps1) automates the entire setup:
- **Phase 1**: Installs WSL2 with Ubuntu (if not present)
- **Phase 2**: Installs Windows apps via `winget` and boots the dotfiles installer inside WSL
**1. Open PowerShell (not elevated initially β the script self-elevates)**
The script will request admin privileges when needed.
**2. Set execution policy and run the bootstrap**
PowerShell execution policies may prevent scripts from running. Download the script first, then bypass the policy for the current session:
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force
irm https://raw.githubusercontent.com/program-the-brain-not-the-heartbeat/dotfiles/main/bootstrap-windows.ps1 -OutFile .\bootstrap-windows.ps1
.\bootstrap-windows.ps1
```
> **Note:** `-Scope Process` means the execution policy change lasts **only for the current PowerShell session** and doesn't modify system-wide settings. If you close and reopen PowerShell, the default policy is restored.
The two-step approach (download first, then execute) is safer and more reliable than piping through `iex`.
**Alternative 1: Fully automated (no prompts, auto-reboot if needed)**
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force
irm https://raw.githubusercontent.com/program-the-brain-not-the-heartbeat/dotfiles/main/bootstrap-windows.ps1 -OutFile .\bootstrap-windows.ps1
.\bootstrap-windows.ps1 -AutoConfirm -Role workstation
```
**Alternative 2: Run in one line from Downloads folder**
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force; irm https://raw.githubusercontent.com/program-the-brain-not-the-heartbeat/dotfiles/main/bootstrap-windows.ps1 -OutFile $env:TEMP\bootstrap-windows.ps1; & $env:TEMP\bootstrap-windows.ps1
```
**Available parameters:**
- `-Phase 1|2` β Run only phase 1 (WSL install) or phase 2 (apps + bootstrap defaults to phase 1)
- `-SkipWsl` β Skip WSL installation (useful if WSL is already present)
- `-SkipApps` β Skip winget app installation
- `-SkipBootstrap` β Don't launch dotfiles bootstrap inside WSL
- `-AutoConfirm` β Skip all confirmation prompts
- `-Role base|personal|workstation|development-server|production-server` β Dotfiles role to use in WSL (defaults to `workstation`)
**What happens**
1. On first run, WSL2 + Ubuntu 24.04 are installed, and the machine reboots automatically
2. After restart, the script resumes (Phase 2) to install Windows apps and optionally bootstrap dotfiles inside WSL
3. Inside WSL, the standard Linux dotfiles bootstrap runs with your selected role
**Windows execution policy explained**
If you encounter the error:
```
File .\bootstrap-windows.ps1 cannot be loaded because running scripts is disabled on this system.
```
This is the PowerShell execution policy preventing the script from running. Solutions:
- **For current session only (recommended):** `Set-ExecutionPolicy Bypass -Scope Process -Force` β changes are temporary
- **Per-user (persistent):** `Set-ExecutionPolicy RemoteSigned -CurrentUser` β allows local scripts + signed remote scripts
- **System-wide (all users):** `Set-ExecutionPolicy RemoteSigned` β requires admin, affects the entire machine
The bootstrap commands above use `-Scope Process`, which is safest and doesn't require persistent policy changes.
### Platform compatibility matrix
| Feature | Ubuntu/Debian | macOS | WSL2 (systemd on) | WSL2 (systemd off) | Windows native (via bootstrap-windows.ps1) |
| -------------------------- | ------------- | ----- | ----------------- | ------------------ | ------------------------------------------- |
| Bash config + aliases | Yes | Yes | Yes | Yes | Yes (in WSL2) |
| Git, SSH, tmux, nano, htop | Yes | Yes | Yes | Yes | Yes (in WSL2) |
| Claude Code config | Yes | Yes | Yes | Yes | Yes (in WSL2) |
| systemd timers/services | Yes | No | Yes | No (skipped) | Yes (in WSL2) |
| sbin scripts (disk mgmt) | Yes | No | No | No | No (WSL2) |
| Windows apps (winget) | β | β | β | β | Yes (Windows side) |
## Troubleshooting
### Windows: winget installation fails with 0x80073D02
**Error:**
```
Error code: Wsl/InstallDistro/Service/RegisterDistro/ERROR_ALREADY_EXISTS
error 0x80073D02: The package could not be installed because resources it modifies are currently in use.
```
This occurs when `Microsoft.DesktopAppInstaller` is being updated or locked by running processes. **Solution:**
**Option 1: Re-run with automatic retry (recommended)**
The updated bootstrap script automatically detects this error and retries up to 3 times with process cleanup. Simply run:
```powershell
.\bootstrap-windows.ps1 -Phase 2
```
**Option 2: Manual process cleanup and retry**
If automatic retry doesn't work, manually close blocking processes:
```powershell
# Close apps that lock the installer
Stop-Process -Name "Microsoft.DesktopAppInstaller" -Force -ErrorAction SilentlyContinue
Stop-Process -Name "msstore" -Force -ErrorAction SilentlyContinue
Stop-Process -Name "winget" -Force -ErrorAction SilentlyContinue
# Wait for cleanup
Start-Sleep -Seconds 3
# Retry bootstrap
.\bootstrap-windows.ps1 -Phase 2
```
**Option 3: Register winget manually**
If retries and process cleanup don't work, register winget directly:
```powershell
Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe
```
**Option 4: Install via Microsoft Store**
As a final fallback, install **App Installer** from the [Microsoft Store](https://www.microsoft.com/store/productId/9NBLGGH4NNS1) manually, then re-run the bootstrap.
---
### Windows: PowerShell execution policy error
**Error:**
```
File .\bootstrap-windows.ps1 cannot be loaded because running scripts is disabled on this system.
```
**Solution:**
Bypass the execution policy for the current session only (safe, temporary):
```powershell
Set-ExecutionPolicy Bypass -Scope Process -Force
.\bootstrap-windows.ps1
```
This is already included in the download + run approach β no additional steps needed.
---
### Linux: `curl` or `git` not available
**Error:**
```
curl: command not found
```
**Solution:**
Install prerequisites before running the bootstrap:
```bash
# Ubuntu / Debian
sudo apt update && sudo apt install -y git curl
# CentOS / RHEL
sudo yum install -y git curl
# Fedora
sudo dnf install -y git curl
```
---
## Selecting a Role
Every machine selects exactly one role. The role controls which packages are installed, which bashrc extensions are loaded, and which systemd units are enabled.
Available roles: `base`, `personal`, `workstation`, `development-server`, `production-server`.
### Option 1 β Bootstrap (one-liner, recommended for fresh machines)
Pass `--role` directly to the bootstrap command. No interactive prompt will appear.
```bash
bash <(curl -fsSL https://raw.githubusercontent.com/program-the-brain-not-the-heartbeat/dotfiles/main/bootstrap.sh) --role production-server
```
### Option 2 β Install flag (repo already cloned)
```bash
bash install.sh --role production-server
```
### Option 3 β Environment variable
```bash
DOTFILES_ROLE=production-server bash install.sh
```
### Option 4 β Pre-set without running install
To persist the role on a machine _before_ running the installer (e.g. to export it in an existing shell session or to pre-stage `~/.bashrc.local`), add it to `~/.bashrc.local`:
```bash
echo 'export DOTFILES_ROLE=production-server' >> ~/.bashrc.local
```
`~/.bashrc.local` is loaded last by `.bashrc` and is gitignored β it is the correct place for any machine-specific overrides. The role set here will be picked up automatically on every subsequent shell start and by the installer if it is run afterwards without a `--role` flag.
> **Note:** There is no standalone "set role only" command. Options 1β3 all run the full install. Option 4 is the right approach when you want to stamp the role onto a machine without triggering a full install.
## Post-install checklist
Several tools are installed but cannot be fully configured automatically.
After `install.sh` finishes, work through **[docs/post-install.md](docs/post-install.md)**.
Quick summary of what needs manual follow-up:
| Item | All roles | `development-server` | `personal` |
| -------------------------------------------- | :-------: | :------------------: | :--------: |
| `tailscale up` (authenticate) | Yes | Yes | Yes |
| Distribute SSH public key to remote hosts | Yes | Yes | Yes |
| Git identity (`~/.gitconfig.local`) | Yes | Yes | Yes |
| Create `~/.bashrc.local` for local overrides | Yes | Yes | Yes |
| Re-login to activate group memberships | β | Yes | β |
| `mysql_secure_installation` | β | Yes | β |
| Nginx vhosts (`host-manage add`) | β | Yes | β |
| Certbot certificate provisioning | β | Yes | β |
| `stripe login` | β | Yes | β |
| `gh auth login` | β | Yes | Yes |
| Claude Code API key / `claude` auth | β | Yes | β |
| Populate `roles/personal/secrets.d/` | β | β | Yes |
| Windows manual-download tools (WSL) | WSL | β | β |
See [docs/post-install.md](docs/post-install.md) for commands and details.
## Architecture & Design
This system is built on the philosophy of **"No Ansible, No Puppetβjust Shell."** It prioritizes transparency, speed, and ease of maintenance.
### Pure Bash & Symlink-based Strategy
The core of the system relies on standard Bash scripts and symbolic links. Configuration files are symlinked from the [config/](config/) directory to their respective XDG or home locations. This ensures that changes made in the repository are immediately reflected in the system.
### Role-Based Configuration
Machines are classified by a `DOTFILES_ROLE`, allowing for tailored configurations without bloat:
- **base**: Minimum configuration required for all machines (Bash, core utilities).
- **personal**: Extends base with private secrets and backup timers (e.g., Gmail backup).
- **workstation**: Optimized for interactive use, including desktop keybindings and WSL integration.
- **development-server**: Pre-configured with Node (NVM), PHP, and WP-CLI.
- **production-server**: Hardened for safety with interactive safeguards and restricted permissions.
### Helper-Driven Modularity
The [lib/install-helpers.sh](lib/install-helpers.sh) library provides a robust API for common tasks such as package management, symlinking, and OS detection. This modularity ensures the main [install.sh](install.sh) remains clean and readable.
### Idempotent Execution
Every script is designed to be safe to re-run. The system checks for existing configurations, manages file backups with timestamps, and skips redundant steps, making upgrades as simple as running `bash install.sh`.
## Key Features
### Automated Desktop Configuration
For Ubuntu and Linux Mint environments, the system automatically configures desktop keybindings via `gsettings`. This includes setting `Super+L` for workstation locking and disabling conflicting defaults like Cinnamon's Looking Glass shortcut.
### Git Identity & Editor Management
The installer includes an interactive Git configuration helper that sets up `~/.gitconfig.local`. It intelligently selects appropriate editors, defaulting to VS Code (`code --wait`) on desktop roles and native editors on servers, ensuring a consistent developer experience.
### System Tuning
- **Inotify Limits**: Automatically increases `fs.inotify` limits to support large-scale development projects and file watching.
- **SSHFS Configuration**: Automated installation and setup of `sshfs`, including `SSHFS-Win` integration for WSL2 environments.
- **Modern Paging**: Configures `batcat` as the default paging engine for `git`, `man` pages, and `systemd` logs where available.
### Security & Hardening
The `production-server` role implements several safety measures:
- **Interactive Safe-guards**: Aliases `rm`, `cp`, and `mv` to their interactive (`-i`) counterparts.
- **Permission Enforcement**: Explicitly blocks `chmod 777` via a shell function wrapper.
- **Restricted Pathing**: Enforces a strict, minimal `PATH` to prevent execution of unauthorized binaries in the shell.
- **Secret Management**: Role-specific `secrets.d/` directories combined with `.env` files for secure deployment of non-committed credentials.
### Claude Code Integration
Deep integration for @anthropic-ai/claude-code, with automated installation via `npm`, JSONC-to-JSON rendering for [settings.json](config/claude/settings.json), and symlinked persistence of [keybindings.json](config/claude/keybindings.json).
## Audit & Authenticity
This repository undergoes periodic manual audits to ensure it remains a "trust-nothing" system:
- **Strict Bash Mode**: Every script uses `set -Eeuo pipefail` to ensure fast failure and predictable behavior.
- **Dependency Minimalism**: No external runners or heavy runtimes are required; the system relies on standard POSIX tools and the native package manager.
- **Secure Permissions**: The installer enforces `0700` or `0600` permissions on sensitive directories (like `.ssh` or `secrets.d`) and manages `/opt` structures with appropriate group access.
- **Fast Failure**: The `die` helper ensures that if a critical step fails (like a failed sudo acquisition), the script terminates immediately rather than proceeding in an undefined state.
## Development
### Contributing
Contributions are welcome! Before submitting a PR:
1. **Install code quality tools** (required for validating PRs)
```bash
# Linux (Ubuntu/Debian)
sudo apt-get install -y shellcheck shfmt
# macOS
brew install shellcheck shfmt
# Python tools (all platforms)
pip install black flake8 mypy
```
2. **Install Task runner** (optional but recommended)
```bash
# macOS
brew install go-task
# Linux
curl -1sLf 'https://dl.cloudsmith.io/public/task/task/setup.deb.sh' | sudo -E bash
# Windows
winget install Task.Task
```
3. **Validate code locally**
```bash
# Check code quality (ShellCheck, shfmt, Black, flake8, mypy)
bash scripts/validate
# Or using Task runner
task validate
# Auto-fix formatting issues
bash scripts/validate --fix
# Or
task validate:fix
```
4. **Useful utility script examples**
```bash
# Download and install all variants of a Google Font family
install-google-font "Fira Code"
# System-wide install (uses sudo)
install-google-font "Inter" --system
```
5. **Review contribution guidelines**
See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for detailed recommendations on:
- Code style and conventions
- Commit message format (conventional commits)
- PR process and expectations
- Testing recommendations
## Gmail backup (offlineimap + encrypted snapshots)
Automated Gmail backup via OfflineIMAP sync + encrypted tar snapshots, managed by a systemd user timer. Part of the `personal` role.
### How it works
1. **Hourly**: `gmail-backup.timer` fires `gmail-backup.service`
2. **Sync**: offlineimap incrementally syncs each account's IMAP to local Maildir
3. **Snapshot** (daily): tar + AES-256-GCM encrypt the Maildir trees, prune old snapshots
### Path contract
The service declares systemd managed directories. For user units these resolve to XDG paths:
| Purpose | Path | Env var |
| --------------------- | --------------------------------------- | -------------------------- |
| Maildir trees | `~/.local/share/gmail-backup/mail/` | `$STATE_DIRECTORY` |
| Encrypted snapshots | `~/.local/share/gmail-backup/archives/` | `$STATE_DIRECTORY` |
| Logs | `~/.local/state/log/gmail-backup/` | `$LOGS_DIRECTORY` |
| Lock file | `/run/user/$UID/gmail-backup/lock` | `$RUNTIME_DIRECTORY` |
| Env overrides | `~/.config/gmail-backup/env` | `$CONFIGURATION_DIRECTORY` |
| Encryption passphrase | `~/.config/gmail-backup/passphrase` | `$CONFIGURATION_DIRECTORY` |
### Setup after install
After running `bash install.sh --role personal` (or `DOTFILES_ROLE=personal bash install.sh`), follow these steps:
**1. Create the encryption passphrase**
```bash
# Generate a strong random passphrase
openssl rand -base64 48 > ~/.config/gmail-backup/passphrase
chmod 600 ~/.config/gmail-backup/passphrase
```
Back this passphrase up somewhere safe (password manager, offline USB). Without it, encrypted snapshots are unrecoverable.
**2. Configure OfflineIMAP**
Copy the example and edit it with your real account details:
```bash
cp config/.offlineimaprc.example ~/.offlineimaprc
chmod 600 ~/.offlineimaprc
$EDITOR ~/.offlineimaprc
```
**3. Create the environment file (optional)**
```bash
cat > ~/.config/gmail-backup/env << 'EOF'
# Space-separated account names (must match [Account ...] in .offlineimaprc)
ACCOUNTS_ENV="account1 account2"
EOF
chmod 600 ~/.config/gmail-backup/env
```
**4. Enable the timer**
The installer enables the timer automatically. Verify with `systemctl --user status gmail-backup.timer`.
### Rotating your Google App Password
offlineimap authenticates with a [Google App Password](https://myaccount.google.com/apppasswords), not your main Google account password. Changing your Google account password revokes all existing App Passwords automatically.
**1. Generate a new App Password**
In your Google Account: Security β 2-Step Verification β App passwords. Create one named "offlineimap" (or similar) and copy the 16-character code.
**2. Update `~/.offlineimaprc`**
Find the `remotepass` line for the affected account and replace it with the new App Password:
```bash
$EDITOR ~/.offlineimaprc
```
If you use `passwordeval` instead (e.g. reading from a secrets file), update that file rather than `.offlineimaprc` directly.
**3. Update the dotfiles secrets copy**
Keep `roles/personal/secrets.d/.offlineimaprc` in sync so the next `install.sh` run deploys the correct value:
```bash
cp ~/.offlineimaprc roles/personal/secrets.d/.offlineimaprc
```
**4. Run a manual test**
Confirm offlineimap can authenticate before the next timer fires:
```bash
systemctl --user start gmail-backup.service
journalctl --user -u gmail-backup.service -f
```
A successful run ends with `=== Run end ===` in the journal.