guest@6admin.io:~$

myflec: a Bash dotfiles framework for people who deploy their home directory

My $HOME is version-controlled, deployed from templates and pushed to three forges. Not because I enjoy yak-shaving, but because I have broken too many machines to trust “it’s probably fine”. myflec (“My Favorite Linux Environment Configuration”) is the Bash framework that grew out of that habit: a loader, a backup model, a deploy procedure and an identity model. All of it boring on purpose, all of it shell-native.

The old base

This is not a young project. The GitHub account underneath it was created in September 2011 and the habits are older than the name. The clearest surviving ancestor is vim-apthogen (2016): a curl | bash netinstall that bootstrapped an entire ~/.vim from symlinks, driven by declarative lists with dependency tiers (.apthogen.list, then .required.list, .minimal.list, .highly.list, .recomended.list, .whynot.list) and install menus. The author signature of the era is visible in every script: a heredoc header reading Version / Authors / Description / Dependencies and a .vimrc that already carried command W w !sudo tee and command Q qa, the same aliases the 2018 config kept and the current one still has.

myflec is that lineage’s current form: declare what you want in lists, let a script bootstrap it, degrade gracefully when something is missing. What changed is the target (~/.bashrc.d and $HOME instead of ~/.vim), the backup model (a bare git repo instead of symlinks) and fifteen years of accumulated deploy discipline. The principles did not need inventing; they needed an adult supervision pass.

The loader: core first, tools after

~/.bashrc.d/ is a directory of modules and the loader sources them in an order that matters:

for _myflec_file in "$HOME"/.bashrc.d/_*.bash; do
    _myflec_load "$_myflec_file"
done
for _myflec_file in "$HOME"/.bashrc.d/*.bash; do
    case "$(basename "$_myflec_file")" in _*) continue ;; esac
    _myflec_load "$_myflec_file"
done

Underscore-prefixed modules are core: helpers, environment, the loading function itself. They load first so every later module can use them. Then tool modules load alphabetically and every optional tool is guarded by command -v starship >/dev/null 2>&1 so a machine without the tool degrades gracefully instead of printing four errors at every prompt. No framework variables to leak: the loader unsets its own function when done.

The backup: a bare repo in your home

The classic trick, formalized: git --git-dir=$HOME/.dotfiles --work-tree=$HOME. Your dotfiles live in a bare repo whose work tree is your home directory; git status shows only tracked files, nothing moves. On top of that, myflec ships the commands I actually wanted:

  • dotfiles-backup "message": add, commit, push in one step.
  • dotfiles-addbackup <url> / dotfiles-removebackup: push URLs for multiple forges (the repo is replicated to GitHub, GitLab and NotABug; losing one forge does not lose the dotfiles).
  • dotfiles-init <url> [extra-push-urls]: bootstrap a fresh machine from nothing.

The deploy: dry run, backup, verify

Deploying a dotfiles template onto a machine is a destructive operation, so the myflec deploy procedure is structured like the global rules I use everywhere: dry run first against the real target with the full transfer list read unfiltered, automatic backup of anything that will be overwritten (rsync --backup into a backup dir), then post-deployment verification that the function still works: ssh -G <host> after an SSH config change, git config user.email after a git config change. Overwriting a live .ssh/config without a rollback path is not a deploy, it is a bet.

Secrets never travel: identity files and templates (.gitconfig, .gitconfig.d) stay out via myflec.exclude.lst and secret material lives in .ssh/ plus a fnox -managed env, never in the tracked tree.

The identity model: one identity per project tree

The part I use most is also the part with the longest README section: juggling work and personal identities. The stack:

  • git includeIf "gitdir:~/projets/company/" in .gitconfig picks the identity by directory prefix; a .gitconfig.d/ directory holds one file per identity.
  • SSH host aliases (github.com-work) route each remote to the right key.
  • The README documents the full matrix (project directory, git identity, SSH remote) with a Mermaid diagram, because the day you have three forges and two employers, “which key signed this push” needs a picture.

Housekeeping as CI

Two small touches that keep the framework honest: a VHS assets/demo.tape regenerates the README demo GIF in CI (demo.yml) whenever the config changes, so the docs never lie and lint.yml keeps the shell scripts shellcheck-clean. A dotfiles repo without CI is a repo you stop trusting in year two.

My home, version-controlled

  • Order is a feature. Core modules loading before tool modules is what makes the rest composable.
  • The bare-git trick is the only backup model I never abandoned: no symlinks dance, no install scripts, git semantics all the way.
  • Deploy = dry run + backup + functional check. File-level success is not success.
  • Identities belong to trees, not to machines. includeIf scales to projects; .gitconfig global identity does not scale to real life.

[edit: 2026] The Ansible implementation of all this: the myflec role .