Personal config files (.zshrc, .gitconfig, Claude Code settings/skills) tracked
in one repo and symlinked into $HOME. Editing the symlinked file edits the
repo — there's nothing to sync, just commit and push.
A symlink ("symbolic link") is a signpost that points at a file living somewhere else. When a program opens the signpost, it's really opening the file it points to — think of a shortcut on your desktop, or a mail-forwarding address that quietly redirects letters to where you actually live.
This repo uses that trick so every machine shares one set of config:
- The real config files live here in this repo (at
~/PersonalProjects/dotfiles). - In your home folder, files like
~/.zshrcaren't real files — they're signposts pointing back into this repo. - So when your shell reads
~/.zshrc, it's really reading this repo's copy. When you edit~/.zshrc, you're really editing this repo's copy.
Because the home-folder files are just signposts, there's nothing to copy back and forth:
edit the file where it normally lives → it's already changed in the repo →
git pushto publish it →git pullon your other computer to receive it.
One source of truth, no manual syncing. setup.sh is the one-time step that puts
those signposts in place on a new machine (next section).
Requires git ≥ 2.36 (needed for hasconfig-based identity routing, see below).
# On a fresh machine without SSH keys yet, use HTTPS:
git clone https://github.com/JRRS1982/dotfiles.git ~/PersonalProjects/dotfiles
# Or use SSH if you already have a GitHub SSH key:
# git clone git@github.com:JRRS1982/dotfiles.git ~/PersonalProjects/dotfiles
cd ~/PersonalProjects/dotfiles
./setup.shsetup.sh installs zsh, Oh My Zsh, and nvm, sets up an SSH key if needed, checks
your git version, creates the symlinks below, and creates empty ~/.zshrc.local
and ~/.claude/CLAUDE.local.md if they don't exist. It is safe to re-run.
setup.sh does not assume a brand-new machine. Before linking, any existing
real file it would replace (e.g. a pre-existing ~/.zshrc or ~/.gitconfig)
is moved to ~/.dotfiles-backup-<timestamp>/ first — nothing is overwritten in
place, and existing correct symlinks are simply repointed.
The backup is not auto-merged (symlinking makes the repo's version win). If those old files had anything worth keeping, fold it in afterwards:
- shared across machines → into the tracked repo files (then commit + push);
- specific to this machine → into
~/.zshrc.localor~/.claude/CLAUDE.local.md.
Because ~/.zshrc etc. are symlinks into this repo, just edit the file where
it normally lives — you're editing the repo. Then commit and push as usual:
cd ~/PersonalProjects/dotfiles
git add -A
git commit
git pushThe dotfiles shell alias runs git against this repo from anywhere (dotfiles status, dotfiles add ..., etc.), so you don't need to cd first. For commits,
prefer the /dotfiles-gc skill. On another machine, git pull in this repo to
get the change.
| Repo file | Symlinked to |
|---|---|
.zshrc |
~/.zshrc |
.zsh_aliases |
~/.zsh_aliases |
.gitconfig |
~/.gitconfig |
.gitconfig-personal |
~/.gitconfig-personal |
.gitconfig-work |
~/.gitconfig-work |
.claude/settings.json |
~/.claude/settings.json |
.claude/CLAUDE.md |
~/.claude/CLAUDE.md |
.claude/skills/ |
~/.claude/skills/ |
Two different mechanisms, reproduced two different ways:
-
Plugins are declared in
.claude/settings.json(enabledPlugins, plus any non-official marketplaces underextraKnownMarketplaces). The reproduction chain is:setup.shsymlinkssettings.jsoninto~/.claude/→ the next time you launch Claude Code it readsenabledPluginsand installs each one from its marketplace. Sosetup.shhas no plugin-specific step — the symlink it already creates is what carries them. Two caveats: Claude Code itself must be installed first (setup.shdoes not install it), and the first install from a non-official marketplace (thedotmack,warpdotdev) may prompt once to trust it. Currently enabled:Plugin Source What it is superpowersofficial Skills framework (brainstorming, TDD, systematic-debugging, writing-plans…) claude-memthedotmackPersistent cross-session memory; bundles the mcp-searchMCP servercontext7official MCP server for live, version-accurate library docs playwrightofficial MCP server for browser automation security-guidanceofficial Defensive-security guidance explanatory-output-styleofficial The "explanatory" output style warpwarpdotdevWarp terminal integration -
Standalone MCP servers (added via
claude mcp add) are not declared insettings.json— they live in~/.claude.json, which is machine-local and not symlinked (it also holds per-project history and auth). So they don't travel with the repo on their own.setup.shre-registers them on each machine instead:Server Registered by Needs chrome-devtoolssetup.sh(claude mcp add -s user)Node ≥ 22, a local Chrome Note that
context7,playwright, andclaude-mem'smcp-searchare also MCP servers, but they ride inside plugins (above), so they're already covered by the symlinkedsettings.json. Only servers with no plugin wrapper need asetup.shline.
~/.zshrc.local— machine-specific shell config, sourced at the end of.zshrc.~/.claude/CLAUDE.local.md— machine-specific Claude context, imported by.claude/CLAUDE.md.
Both are gitignored and created empty by setup.sh — they are not part of this repo and exist only on the machine you are on, so you will not find them in this checkout.
.gitconfig sets no default identity (fail-closed). Identity is chosen per
repo by remote URL via hasconfig (git ≥ 2.36):
- Remote matches
JRRS1982on GitHub → personal identity (.gitconfig-personal) - Remote matches
bitbucket.org:mvfglobal/**→ work identity (.gitconfig-work) - Anything else → git refuses to commit until you set
user.name/user.emailyourself
Drop it in ~/.claude/skills/<name>/ (i.e. .claude/skills/<name>/ in this
repo, since it's symlinked). It shows up as untracked in git status:
- Shareable → commit it, prefixing the name with
dotfiles-(e.g.dotfiles-gc) - Private/work-specific → add its path to
.gitignoreinstead (e.g.mvf-jira-writer)