Environment config and dotfiles, kept consistent across machines.
Well, maybe. But if you work in a technical job and in Unix-based systems, over the years the accumulation of little tips and tricks in your dotfiles becomes so personalized that not having them feels like rock-climbing with one arm tied behind your back. Maybe I can make it up the rock face, but it'd be a hell of a lot easier if I had both arms. When i read of about the bare git repo technique, it clicked. I could make my personal IP portable. And I've been improving it ever since.
The goal has always been to minimize the amount of time it takes for me to get started on a new machine, be it personal or otherwise, and to do so securely. In the most recent version, I've added ~/.claude config with API helpers to allow for folder-based seperation of my various Claude contexts - work and personal. If you don't need all of this, fork it and take what you want. I hope it's useful.
Uses the bare git repo technique: a git repo whose working tree is $HOME itself, tracked via a config alias. No symlinks, no extra tooling, no install framework — files live where the shell expects them.
alias config='git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME' GitHub $HOME
┌─────────────────┐ ┌──────────────────────────────┐
│ you/dotfiles │──clone──▶ │ ~/.dotfiles/ (git only) │
│ (this repo) │ │ ~/.zshrc │
└─────────────────┘ │ ~/.gitconfig │
│ ~/.aliases │
│ ~/.ssh/config │
│ ~/.claude/ │
└──────────────────────────────┘
No symlinks. No install framework. Files just live where they always did.
The only difference: `config status` instead of `git status`.
Most dotfiles tutorials end up either fully public (and full of personal details) or fully private (and useless as a reference). This repo solves that with a two-repo overlay pattern:
- This repo — generic and shareable. Shell config, tool setup, structural patterns. Safe to clone on any machine or show to anyone.
dotfiles-private(private repo) — personal identity, employer- or contract-related config, private host entries. Layers on top via.localincludes that each public file sources if present.
GitHub (public) GitHub (private)
┌─────────────────┐ ┌──────────────────────┐
│ you/dotfiles │ │ you/dotfiles-private │
│ │ │ │
│ .zshrc │ │ .gitconfig.local │
│ .gitconfig │ │ .gitconfig-work │
│ .ssh/config │ │ .ssh/config.local │
│ .aliases │ │ statusline-colors.sh │
│ statusline... │ │ api-accounts.json │
│ │ │ │
│ ← show anyone │ │ ← never public │
└────────┬────────┘ └──────────┬────────────┘
│ │
└──────────┬───────────────────┘
▼
$HOME
The key design principle: every file in this repo works standalone. The private overlay only adds — it never requires the public files to be aware of what's in it. Clone just this repo and you get a functional environment; clone both and you get the full personal setup.
The .local pattern is the seam between them:
.gitconfig .gitconfig.local (private)
┌────────────────────────────┐ ┌───────────────────────────────┐
│ [credential "github.com"] │ │ [user] │
│ helper = gh auth ... │ │ name = Your Name │
│ │ │ email = you@personal.com │
│ [include] │ │ │
│ path = ~/.gitconfig.local│─────▶│ [includeIf "gitdir:~/work/"] │
└────────────────────────────┘ │ path = ~/.gitconfig-work │
└───────────────────────────────┘
.ssh/config .ssh/config.local (private)
┌────────────────────────────┐ ┌───────────────────────────────┐
│ Include ~/.ssh/config.local│─────▶│ Host work-gitlab │
│ │ │ HostName gitlab.com │
│ Host github.com │ │ │
│ IdentityFile ~/.ssh/... │ │ host *.local │
│ │ │ User yourname │
│ Host * │ └───────────────────────────────┘
│ IdentityAgent 1password │
└────────────────────────────┘
| File | Purpose |
|---|---|
.zshrc, .zprofile, .aliases |
Shell config |
.gitconfig |
Global git config + credential helpers. Identity via ~/.gitconfig.local (private overlay) |
.ssh/config |
SSH config skeleton. Private hosts via ~/.ssh/config.local (private overlay) |
.claude/credential-helper.sh |
Claude Code API key helper (1Password-backed, per-directory) |
.claude/statusline-command.sh |
Claude Code status line — model, context %, account (color-coded), git branch, dir, session cost |
1. Clone public 2. Clone private 3. Done
───────────────── ────────────────── ──────────────────
git clone --bare git clone config checkout
.../dotfiles .../dotfiles-private install.sh
~/.dotfiles ~/.dotfiles-private
│
└── install.sh creates symlinks
for all .local files
git clone --bare https://github.com/jehanalvani/dotfiles $HOME/.dotfiles
alias config='git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'config checkout
config config --local status.showUntrackedFiles noIf checkout fails due to existing files, back them up first:
mkdir -p ~/.config-backup && \
config checkout 2>&1 | grep -E "^\s+\." | awk '{print $1}' | \
xargs -I{} sh -c 'mkdir -p ~/.config-backup/$(dirname {}) && mv $HOME/{} ~/.config-backup/{}'
config checkoutgit clone git@github.com:jehanalvani/dotfiles-private $HOME/.dotfiles-private
bash ~/.dotfiles-private/install.shbrew install pyenv direnv starship zsh-autosuggestions zsh-syntax-highlighting glab
gh auth setup-git # GitHub HTTPS credential helper
python3 -m pip install --user argcompletepip install --user may put register-python-argcomplete somewhere not on PATH (e.g. ~/Library/Python/<version>/bin on macOS system Python). If command -v register-python-argcomplete comes up empty, symlink it into ~/.local/bin (already on PATH via .zshrc):
ln -sf ~/Library/Python/*/bin/register-python-argcomplete ~/.local/bin/register-python-argcompletePer-project API keys live in .claude/auth-keys.json within each project directory:
{
"key": "op://Vault/Item/credential",
"op_account": "account.1password.com"
}For directories without a local auth-keys.json, the global ~/.claude/api-accounts.json is used as fallback (provided by the private overlay). An empty key falls back to Claude Pro OAuth.
bash ~/.claude/credential-helper.sh --testconfig status
config add ~/.some/new/file
config commit -m "Add some config"
config push