dotfiles

skill
Security Audit
Fail
Health Warn
  • No license — Repository has no license file
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Fail
  • rm -rf — Recursive force deletion command in .github/actions/setup-shellcheck/action.yml
Permissions Pass
  • Permissions — No dangerous permissions requested

No AI report is available for this listing yet.

SUMMARY

More AI-augmented dev environment than config store: a git-crypt-secured, bats-tested setup for macOS and headless GPU servers, with interactive and non-interactive bash/zsh scaffolding, a local knowledge stack (qmd, gbrain), Claude Code session archival and redaction, supply-chain-gated package managers, Tailscale routing, and systemd services.

README.md

Dotfiles

Cross-platform dotfiles for macOS and headless Ubuntu servers — managed with
GNU Stow, secured with git-crypt.

Quick Start

# 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 [email protected]: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. Use Homebrew/Linuxbrew.

For detailed platform-specific setup (oh-my-zsh, Ghostty, Cursor extensions, iCloud sync), see
BOOTSTRAP.md.

Repository Layout

dotfiles/
├── stow/                  Stow packages (symlinked into $HOME; ollama targets /etc)
├── config/
│   ├── shell/             Shell fragments auto-sourced by .profile
│   ├── git/               Per-platform git config templates
│   ├── qmd/               Per-platform qmd collections templates
│   ├── apparmor.d/        System-level AppArmor profiles (deployed via apparmor-deploy.sh)
│   └── systemd/system/    System-level units (NAS mounts via nas-deploy.sh, apparmor-playwright via apparmor-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 + boot unit (Playwright/Chromium)
│   ├── sshd-locale-deploy.sh  Strip LANG/LC_* from sshd AcceptEnv so sessions use the server locale
│   ├── playwright-browsers-deploy.sh  Playwright browser binaries into the shared cache (curl + unzip)
│   ├── playwright-deps-deploy.sh  Playwright browser launch provisioning (binaries + apparmor + opt-in browser deps)
│   ├── *-enable.sh        Service enablers (qmd-serve, qmd-launchd, opendataloader-pdf)
│   ├── tailscale-serve-setup.sh   Reproducible tailnet serve config (svc:ollama)
│   ├── macos-gpu-monitor.sh       Metal GPU residency/power trace around any command (macOS)
│   ├── lint-shell, lint-workflows, run-tests   CI gate dispatchers, shared by CI and the git hooks
│   ├── generate-changelog.py      Release changelog extraction from merged PR bodies
│   ├── sync-dev-after-release.sh  Backport the released CHANGELOG.md to dev by PR
│   ├── release/           Release gates (drift.sh, guarded-paths.sh)
│   ├── tools-atime/       Multi-package-manager unused-tool audit + reclaim
│   └── sync/              iCloud and Box sync
├── .githooks/             Repo-local git hooks (core.hooksPath)
├── .github/
│   ├── workflows/         CI: release, shellcheck (+ actionlint), bats, and three main-PR guards
│   └── rulesets/          Branch protection rules (protect-dev, protect-main)
├── tests/                 bats-core test suites
└── docs/
    ├── solutions/         Solved problems and patterns (symlink to a separate private repo)
    ├── runbooks/          Operational runbooks (GPU driver drift, Playwright launch)
    ├── plans/             Implementation plans (dev only)
    └── brainstorms/       Design explorations (dev only)

The engineering-doc directories marked dev only are inert planning text that never ships. They live on dev, the
release recipe strips them from the release branch, and guard-main-docs.yml blocks them from any PR to main — so a
checkout of main carries docs/runbooks/ and the docs/solutions/ symlink but none of the planning trees.

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)
caddy .config/caddy/Caddyfile, caddy.service — loopback proxy fronting Ollama for svc:ollama (Host rewrite) (Linux only)
cargo .cargo/config.toml — git dependency fetches routed through the git CLI (Linux only)
claude .claude/ (settings, hooks, statusline, templates), .markdownlint-cli2.yaml
codex .codex/config.toml
codex-proxy systemd user unit for the docker-compose codex-proxy backend (Linux only)
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/ — signing, prose, session, qmd, and commit helpers (see Local Binaries)
micro .config/micro/settings.json — editor settings
obsidian .config/obsidian/, systemd service, CLI wrapper (Linux only)
ollama systemd service override binding Ollama to loopback 127.0.0.1:11434; system-level, stowed into /etc with sudo (Linux only)
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 (both platforms) + systemd user units for the qmd-serve daemon and index timers (Linux)
rclone .config/rclone/, Box bisync systemd service + timer (Linux only)
rust rustup-update.service + .timer (nightly rustup update stable --no-self-update; units land on Linux only)
secrets .secrets (git-crypt encrypted)
shell .profile — the shell config chain's entry file, which auto-sources config/shell/
ssh .ssh/config (git-crypt encrypted)
tmux .config/tmux/tmux.conf
tmuxinator Session configs read in place from the repo; never stowed (see Tmuxinator Sessions)
yazi .config/yazi/ — file manager config, keymaps, theme, packages
zsh .zshrc, .zshenv, .zprofile, .p10k.zsh

Two directories under stow/ are not in any package set and never deploy through stow-deploy: ollama targets /etc
rather than $HOME and is stowed with sudo stow -t /etc -d stow ollama (see
stow/ollama/README.md), and tmuxinator is read in place rather than symlinked. Both are
recorded as exemptions in tests/stow-deploy-packages.bats, which fails when any other stow/ directory is missing
from a package set.

Local Binaries

The local package deploys stow/local/dot-local/bin/ to ~/.local/bin/. These are machine-level entry points: repo
hooks, systemd units, and agent tooling call them by name, so each one resolves the same way on every host without
knowing where the underlying implementation lives.

Binary Purpose
brave-search Query the Brave Search API, printing ranked results as plain text or raw JSON
charcount Count characters against platform length caps, exiting non-zero so it gates in scripts
env Put ~/.local/bin on PATH for user-installed binaries
gh-revision-audit List your issues and PRs that still carry prior edit revisions on GitHub
gstack-config-apply Converge ~/.gstack/config.yaml, which gstack rewrites and so cannot be a stow package
op-ssh-sign-wrapper Cross-platform commit signing: 1Password agent on macOS, ssh-keygen -Y sign headless
qmd-gpu-verify Prove the qmd-serve daemon runs its model work on the GPU rather than the CPU
qmd-ollama-unload-all Evict resident Ollama models, but only when VRAM is too low for qmd embed to load safely
sd-commit-doc Commit and push docs/solutions/ files from the shared clone without racing another agent
tmux-new-session Write a tmuxinator config for a repo and start its 3-pane session
transcribe-diarize Local GPU speaker-diarized transcription (ffmpeg → whisperx → pyannote)
unslop Machine-level entry point for the prose scorer, resolved through ~/.claude/skills
unslop-gate Git-hook prose gate over unslop, scoring only the markdown in the change being pushed
uuidv7 Print a time-ordered UUIDv7, used for collision-proof /tmp artifact names
xurl Real xurl on PATH that execs xurl-rs's xr, for callers that load no interactive alias

Tmuxinator Sessions

Session configs live in stow/tmuxinator/dot-config/tmuxinator/ and are not stowed. config/shell/tmuxinator.sh
exports TMUXINATOR_CONFIG to point at that directory, so tmuxinator reads, writes, and lists projects in the repo
itself — tmuxinator new and tmuxinator copy land on the source of truth with no deploy step.

Keep ~/.config/tmuxinator empty. tmuxinator searches that path in start and stop but not in list, so a config
sitting there shadows the repo: it starts a session that never appears in tmuxinator list. sudo tmuxinator … is the
usual way one gets there, because sudo scrubs TMUXINATOR_CONFIG and falls back to the XDG path — and it strands the
tmux server at UID 0, invisible to your own tmux ls. Run tmuxinator as yourself.

Every 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 <name> — 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:

tmuxinator start anc                          # local terminal
mux start anc                                 # zsh shell alias (same thing)
ssh <dev-host> -t tmuxinator start anc        # over SSH (preferred connection idiom)

Raw tmux attach -t <name> 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 + first start in one shot), use tmux-new-session <name> <repo-path> — it
writes a new tmuxinator config into stow/tmuxinator/dot-config/tmuxinator/, then runs tmuxinator start.

tmuxinator copy duplicates a config verbatim, including the source project's name: and its on_project_first_start
resize targets. Edit both after copying, or the new session resizes panes in the project it was copied from.
tests/tmuxinator-configs.bats enforces that every config's resize targets match its own name:.

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 (//<nas-host>/openclaw)
mnt-nas.automount On-demand automount, solves WiFi boot race

Deploy: sudo scripts/nas-deploy.sh (requires /root/.smbcredentials-<nas-host> from 1Password).

Three more system-level configs deploy through their own paths: apparmor-playwright.service (with the AppArmor
profile, via scripts/apparmor-deploy.sh, below), the ollama loopback override (sudo stow -t /etc, see the ollama
package), and the sshd locale change (sudo scripts/sshd-locale-deploy.sh, which edits /etc/ssh/sshd_config in place;
see BOOTSTRAP.md § SSH session locale).

Playwright / browse browser launch (scripts/playwright-deps-deploy.sh)

On Linux the browse tool and Playwright e2e need three things to launch browsers: the browser binaries in the shared
cache, an AppArmor profile (for Chromium's sandbox), and, for Safari/iOS testing, WebKit system libraries. One script
provisions all three, run as your normal user (it escalates to sudo only where needed):

scripts/playwright-deps-deploy.sh            # browser binaries + AppArmor profile + boot persistence (Chromium / browse)
scripts/playwright-deps-deploy.sh --webkit   # + Safari/iOS system libs (WebKit, heavy ~380 MB)
scripts/playwright-deps-deploy.sh --all       # + Chromium and WebKit system libs

The browser binaries are provisioned into the shared cache ($PLAYWRIGHT_BROWSERS_PATH) by
scripts/playwright-browsers-deploy.sh (run directly, or via the script above) using curl + unzip rather than
playwright install — node's extractor deadlocks on this host's io_uring/kernel combo. One canonical version serves
every repo, so per-repo playwright install becomes a no-op; bumping it is a dotfiles edit (the revision map in that
script). WebKit deps are opt-in because they pull ~180 packages and are only needed for Safari/iOS e2e (the mobile-ios
/ tablet projects). See docs/runbooks/playwright-browser-launch.md for
failure signatures, the io_uring root cause, and recovery.

AppArmor profiles (config/apparmor.d/) are deployed by scripts/apparmor-deploy.sh (called by the script above,
or run standalone as sudo scripts/apparmor-deploy.sh), which copies each file to /etc/apparmor.d/, loads it with
apparmor_parser -r, and installs apparmor-playwright.service to reload it at boot. The boot unit is required because
Ubuntu's own apparmor.service is skipped at boot on minimized server images, so /etc/apparmor.d/ is otherwise never
reloaded and the profile drops on reboot.

Profile Purpose
playwright Grants userns to Playwright's bundled Chromium so the browse tool works on Ubuntu 24.04

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.

The loop runs after .profile finishes assembling PATH (Homebrew, ~/.local/bin, bun, cargo). Files here routinely
gate their contents on command -v <tool>, and that guard is evaluated at source time: sourced any earlier, every such
file would silently no-op in a shell that did not inherit a populated PATH — a launchd-spawned terminal, cron, or ssh host cmd. tests/shell-startup-shapes.bats pins the ordering and exercises each shell shape;
tests/shell-path-matrix.bats checks PATH assembly across all eight supported invocation shapes, tabulated in
AGENTS.md.

Shell startup latency budgets live in tests/perf/, outside the tests/*.bats glob that the pre-push hook and CI use.
The hook runs that directory first, on a quiet machine, and each measurement is a best-of-N minimum: run at the tail of
the full suite, the several hundred shells it spawns push interactive zsh past its budget with nothing about the config
having changed.

File Purpose
build-flags.sh Native-CPU build flags (-march=native) for local compilation
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
languagetool.sh LanguageTool wrapper for the shared prose-lint stage
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_REMOTE_URL export (qmd-serve daemon URL)
run-flags.sh Runtime performance env vars (CUDA, io_uring, PyTorch) — Linux
supply-chain.sh Supply-chain safety (package age gates)
taildrive.sh Taildrive mount helpers (macOS)
telemetry.sh Telemetry opt-out environment variables
tmuxinator.sh mux and mux-all tmuxinator wrappers
xurl.sh Alias xurl to the xr binary (xurl-rs)
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
Hook Purpose
--------------- --------------------------------------------------------------------
pre-commit Blocks commits on main, verifies commit.gpgsign, gates staged paths
post-checkout Auto-unlocks git-crypt, chains Git LFS
post-merge Auto-unlocks git-crypt, chains Git LFS
pre-push Mirrors CI (shellcheck, actionlint, bats), chains Git LFS

Both gates call the same dispatchers CI calls — scripts/lint-shell, scripts/lint-workflows, scripts/run-tests — so
the three cannot drift. pre-push runs them over the whole repo; pre-commit runs them over staged paths only. See
AGENTS.md § Local gates mirror CI.

CI and Testing

Workflow Trigger Purpose
release.yml Push to main CalVer tag + GitHub Release, notes read from CHANGELOG.md
shellcheck.yml Pull request Lints shell scripts and hooks, then workflows with actionlint
bats.yml Pull request Runs the bats-core test suites
guard-main-docs.yml Pull request to main Blocks engineering docs from reaching main
guard-main-provenance.yml Pull request to main Requires every commit to carry a PR reference
guard-release-branch.yml Pull request to main Rejects any head branch that is not release/*

shellcheck.yml and bats.yml are required status checks on both dev and main; main additionally requires the
three guard callers. They run on every pull request with no path filter so the required context is always reported. The
pre-push hook runs the same checks locally and skips them for markdown-only pushes.

release.yml does not generate the changelog. CHANGELOG.md is generated on the release branch by
scripts/generate-changelog.py and committed through the release PR; the workflow only extracts its topmost section as
the GitHub Release body. See RELEASES.md.

Shell scripts are tested with bats-core (scripts/run-tests --all). Suites
cover stow-deploy and its package sets, git hooks and the gate dispatchers, the shell config chain and its PATH
matrix, supply-chain age gates, symlinks, the qmd-serve and opendataloader-pdf units, tmuxinator configs, and the 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 for the end-to-end flow (feature branch → dev → release/* overlay 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:

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 https://github.com/settings/personal-access-tokens/new
  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

Project vocabulary is defined in CONCEPTS.md. Operational runbooks live in docs/runbooks/ (headless GPU
driver drift, Playwright browser launch). Past solutions and design decisions live in docs/solutions/ (a symlink to a
separate private repo):

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

Reviews (0)

No results found