https://github.com/brettdavies/dotfiles
Cross-platform dotfiles for macOS and headless Ubuntu servers — managed with GNU Stow, secured with git-crypt
https://github.com/brettdavies/dotfiles
cross-platform dotfiles git-crypt gnu-stow macos shell-configuration stow ubuntu zsh
Last synced: 5 days ago
JSON representation
Cross-platform dotfiles for macOS and headless Ubuntu servers — managed with GNU Stow, secured with git-crypt
- Host: GitHub
- URL: https://github.com/brettdavies/dotfiles
- Owner: brettdavies
- Created: 2025-11-06T19:42:04.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2026-04-15T23:13:51.000Z (about 1 month ago)
- Last Synced: 2026-04-15T23:15:04.080Z (about 1 month ago)
- Topics: cross-platform, dotfiles, git-crypt, gnu-stow, macos, shell-configuration, stow, ubuntu, zsh
- Language: Shell
- Homepage:
- Size: 764 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# Dotfiles
Cross-platform dotfiles for macOS and headless Ubuntu servers — managed with
[GNU Stow](https://www.gnu.org/software/stow/), secured with [git-crypt](https://github.com/AGWA/git-crypt).
## Quick Start
```bash
# 1. Install Homebrew (skip if already installed)
# macOS
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
eval "$(/opt/homebrew/bin/brew shellenv)"
# Linux
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
# 2. Install core tools
brew install stow git-crypt
# 3. Clone and unlock
git clone git@github.com:brettdavies/dotfiles.git ~/dotfiles
cd ~/dotfiles
git-crypt unlock ~/.config/git-crypt/key
# 4. Deploy
scripts/stow-deploy --all # macOS: shared + desktop packages
scripts/stow-deploy --headless --all # Linux: shared packages only
```
> **Stow >= 2.4.0 required.** Ubuntu 24.04 apt only has 2.3.1, which has a
> [bug with nested `dot-` directories][stow-bug]. Use Homebrew/Linuxbrew.
For detailed platform-specific setup (oh-my-zsh, Ghostty, Cursor extensions, iCloud sync), see
[BOOTSTRAP.md](BOOTSTRAP.md).
[stow-bug]: https://github.com/aspiers/stow/issues/33
## Repository Layout
```text
dotfiles/
├── stow/ Stow packages (symlinked into $HOME)
├── config/
│ ├── shell/ Shell fragments auto-sourced by .profile
│ ├── git/ Per-platform git config templates
│ ├── apparmor.d/ System-level AppArmor profiles (deployed via apparmor-deploy.sh)
│ └── systemd/system/ System-level units (deployed via nas-deploy.sh)
├── scripts/
│ ├── stow-deploy Stow wrapper with conflict resolution
│ ├── nas-deploy.sh System-level NAS mount/automount deploy
│ ├── apparmor-deploy.sh System-level AppArmor profile deploy (Playwright/Chromium)
│ └── sync/ iCloud sync scripts
├── .githooks/ Repo-local git hooks (core.hooksPath)
├── .github/
│ ├── workflows/ CI: release.yml, shellcheck.yml
│ └── rulesets/ Branch protection rules
├── tests/ bats-core test suites
└── docs/
├── solutions/ Solved problems and patterns
├── plans/ Implementation plans
└── brainstorms/ Design explorations
```
### Stow Packages
Each directory under `stow/` is a package. Files prefixed with `dot-` become dotfiles (`.` prefix) when symlinked via
`stow --dotfiles`.
| Package | What it manages |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| `bash` | `.bashrc`, `.bash_profile`, `.bash_aliases` |
| `brew` | `Brewfile`, `Brewfile.optional` |
| `bun` | `.bunfig.toml` |
| `caam` | `.caam/` (Claude account rotation config + vault, git-crypt encrypted) |
| `claude` | `.claude/` (settings, hooks, statusline, templates), `.markdownlint-cli2.yaml` |
| `codex` | `.codex/config.toml` |
| `cursor` | `.cursor/rules/`, `extensions.txt` |
| `gh` | `.config/gh/` (GitHub CLI config), `.local/bin/gh` (merge guard wrapper) |
| `ghostty` | `.config/ghostty/config` |
| `git` | `.gitconfig`, `.config/git/` (ignore, allowed\_signers) |
| `github` | `.config/github/` (PR template and other repo-workflow assets) |
| `gogcli` | `.config/gogcli/config.json` — Google Workspace CLI config |
| `launchagent` | `~/Library/LaunchAgents/` (macOS only) |
| `lazygit` | `.config/lazygit/config.yml` — clipboard over SSH via OSC 52 |
| `local` | `.local/bin/` (env, op-ssh-sign-wrapper, tmux-new-session) |
| `micro` | `.config/micro/settings.json` — editor settings |
| `obsidian` | `.config/obsidian/`, systemd service, CLI wrapper (Linux only) |
| `openclaw` | systemd user units for memory extract/distill and morning briefing (Linux, opt-in) |
| `opencode` | `.config/opencode/config.json` |
| `opendataloader-pdf` | Socket-activated hybrid PDF server with idle-exit, systemd user units (Linux only) |
| `pip` | `.config/pip/pip.conf` |
| `qmd` | `.local/bin/qmd` wrapper + systemd user units (qmd-serve daemon, embed + update timers, Linux only) |
| `rclone` | `.config/rclone/`, Box bisync systemd service + timer (Linux only) |
| `rust` | `rustup-update.service` + `.timer` (nightly rustup self-update, Linux, opt-in) |
| `secrets` | `.secrets` (git-crypt encrypted) |
| `shell` | `.profile` |
| `ssh` | `.ssh/config` (git-crypt encrypted) |
| `tmux` | `.config/tmux/tmux.conf` |
| `tmuxinator` | `.config/tmuxinator/*.yml` — declarative session configs (16 projects, see below) |
| `yazi` | `.config/yazi/` — file manager config, keymaps, theme, packages |
| `zsh` | `.zshrc`, `.zshenv`, `.zprofile`, `.p10k.zsh` |
### Tmuxinator Sessions
Every `.config/tmuxinator/*.yml` config defines the same 3-pane working layout: yazi on the left (1/3 width, full
height), a bare shell top-right (2/3 × 2/3), and lazygit bottom-right (2/3 × 1/3). All three panes start in the
project's root.
Start or attach to a configured session with `tmuxinator start ` — it creates the session on the first call and
attaches on every subsequent call, so the same command works whether or not the session is already running:
```bash
tmuxinator start anc # local terminal
mux start anc # zsh shell alias (same thing)
ssh -t tmuxinator start anc # over SSH (preferred connection idiom)
```
Raw `tmux attach -t ` only works after the session has already been started, which makes it the wrong default for
SSH.
To create a new session from scratch (config + symlink + first start in one shot), use `tmux-new-session
` — it writes a new tmuxinator config into `stow/tmuxinator/dot-config/tmuxinator/`, re-stows the package,
then runs `tmuxinator start`.
### System-Level Units (`config/systemd/system/`)
System-level systemd units are **not** managed by stow (which targets `$HOME`). They live in `config/systemd/system/`
and are deployed via `scripts/nas-deploy.sh`, which copies them to `/etc/systemd/system/` and activates them.
| Unit | Purpose |
| ------------------- | ----------------------------------------------- |
| `mnt-nas.mount` | SMB mount for the NAS (`///openclaw`) |
| `mnt-nas.automount` | On-demand automount, solves WiFi boot race |
**Deploy:** `sudo scripts/nas-deploy.sh` (requires `/root/.smbcredentials-` from 1Password).
### AppArmor Profiles (`config/apparmor.d/`)
System-level AppArmor profiles live in `config/apparmor.d/` and are deployed via `scripts/apparmor-deploy.sh`, which
copies every file to `/etc/apparmor.d/` and reloads it with `apparmor_parser -r`. Profiles persist across reboots
because AppArmor loads `/etc/apparmor.d/` at boot.
| Profile | Purpose |
| ------------ | ----------------------------------------------------------------------------------------- |
| `playwright` | Grants `userns` to Playwright's bundled Chromium so the browse tool works on Ubuntu 24.04 |
**Deploy:** `sudo scripts/apparmor-deploy.sh` (Linux only; requires the `apparmor` package).
### Shell Environment (`config/shell/`)
`.profile` sources every `*.sh` file in `config/shell/` automatically — drop a file in and it's picked up, no manifest
needed.
| File | Purpose |
| ------------------- | ----------------------------------------------------- |
| `caam.sh` | Claude account rotation wrapper + daemon |
| `caches.sh` | XDG cache directory locations |
| `claude-code.sh` | Claude Code environment variables |
| `github.sh` | GitHub CLI aliases |
| `gogcli.sh` | Google Workspace CLI keyring password injection |
| `litellm.sh` | LiteLLM proxy configuration |
| `lm-studio.sh` | LM Studio PATH setup |
| `local-paths.sh` | Custom local PATH additions |
| `models.sh` | AI/ML model storage locations |
| `platform-linux.sh` | Linux-specific platform checks and config |
| `python.sh` | Python tooling config |
| `qmd.sh` | `QMD_SERVER` export (qmd-serve daemon URL) |
| `supply-chain.sh` | Supply-chain safety (package age gates) |
| `telemetry.sh` | Telemetry opt-out environment variables |
| `tmuxinator.sh` | `mux` and `mux-all` tmuxinator wrappers |
| `shell-functions` | Interactive shell utilities (sourced by bashrc/zshrc) |
## Secrets Management
Sensitive files are encrypted with git-crypt:
- `stow/secrets/dot-secrets` — API keys and tokens
- `stow/ssh/dot-ssh/config` — SSH host configurations
- `stow/git/dot-config/git/allowed_signers` — SSH allowed signers
Git hooks auto-unlock on checkout and merge. Back up `~/.config/git-crypt/key` in a password manager — if lost,
encrypted files cannot be recovered.
## Git Hooks
Activated via `core.hooksPath` (set automatically by `stow-deploy`):
| Hook | Purpose |
| --------------- | --------------------------------------------------- |
| `pre-commit` | Blocks commits on `main`, verifies `commit.gpgsign` |
| `post-checkout` | Auto-unlocks git-crypt, chains Git LFS |
| `post-merge` | Auto-unlocks git-crypt, chains Git LFS |
| `pre-push` | Chains Git LFS pre-push |
## CI and Testing
| Workflow | Trigger | Purpose |
| ---------------- | -------------------- | -------------------------------------------- |
| `release.yml` | Squash merge to main | CalVer tag, changelog via git-cliff, release |
| `shellcheck.yml` | Push / PR | Lints shell scripts |
Shell scripts are tested with [bats-core](https://github.com/bats-core/bats-core) (`bats tests/`). Suites cover
stow-deploy, git hooks, shell config, symlinks, and CLI wrappers.
## Performance
Shell startup budgets are enforced — non-interactive shells must start under 200ms, interactive shells under 500ms.
See `docs/solutions/performance-issues/` for optimization details.
## Cross-Platform Notes
- `$OSTYPE` checks gate macOS-specific features; `$HOME` used everywhere
- Homebrew paths: `/opt/homebrew` (macOS) vs `/home/linuxbrew/.linuxbrew` (Linux)
- Git signing: 1Password on macOS, `ssh-keygen` fallback on Linux (via `op-ssh-sign-wrapper`)
- SSH config uses `Match exec` for platform-conditional 1Password agent paths
- All GitHub/Gist URLs rewritten to SSH via `url.insteadOf` in `.gitconfig`
- SSH key: `~/.ssh/brett_ed25519` on all machines
- oh-my-zsh plugins: brew symlinks on macOS, git clones on Linux
## Release Automation
Every squash merge to `main` triggers a GitHub Action that computes a CalVer version (`YYYY.MM.DD`), tags the commit,
and creates a GitHub Release. Release notes are extracted from the topmost section of the committed `CHANGELOG.md`. See
[RELEASES.md](RELEASES.md) for the end-to-end flow (feature branch → dev → `release/*` cherry-pick branch → main).
### CI_RELEASE_TOKEN Secret
The workflow pushes back to protected `main`, which requires a fine-grained PAT stored as the `CI_RELEASE_TOKEN` repo
secret.
**Create / rotate the token:**
```bash
op read "op://secrets-dev/dotfiles_RELEASE_TOKEN/credential" \
| gh secret set CI_RELEASE_TOKEN
```
**Creating a new PAT** (if the 1Password entry doesn't exist):
1. Go to
2. **Repository access:** `brettdavies/dotfiles` only
3. **Permissions:** Contents: Read and write
4. Save to 1Password at `secrets-dev/dotfiles_RELEASE_TOKEN`
5. Run the `gh secret set` command above
## Documentation
Past solutions and design decisions live in `docs/solutions/`:
- **Deployment** — cross-platform stow deployment, shell config fixes, headless git signing, conflict resolution
- **Configuration** — branch divergence reconciliation, workflow enforcement
- **Performance** — shell startup optimization, zsh interactive startup tuning
## License
Personal dotfiles — use at your own risk.