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,nextlevelbuilder) may prompt once to trust it. Theclaude-plugins-officialmarketplace is built into Claude Code, so it is deliberately not listed underextraKnownMarketplaces— only the others are. Declared inenabledPlugins— note that a declaration is not the same as being on; the ones marked ❌ stay declared so they are one flag-flip away (the MCP-backed ones were switched off in13d7647to cut token usage):Plugin Source On What it is superpowersofficial ✅ Skills framework (brainstorming, TDD, systematic-debugging, writing-plans…) claude-memthedotmack✅ Persistent cross-session memory; bundles the mcp-searchMCP serverplaywrightofficial ✅ MCP server for browser automation code-simplifierofficial ✅ Simplification/refactor agent ui-ux-pro-maxnextlevelbuilder✅ UI/UX design skills (styles, palettes, design systems, slides) context7official ❌ MCP server for live, version-accurate library docs security-guidanceofficial ❌ Defensive-security guidance explanatory-output-styleofficial ❌ The "explanatory" output style warpwarpdotdev❌ Warp 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. -
Third-party hooks.
settings.jsonalso carries ahooksblock written by Orca (~/.orca/agent-hooks/claude-hook.sh), wired intoUserPromptSubmit,Stop/StopFailure,Subagent{Start,Stop},TeammateIdle,Pre/PostToolUse,PostToolUseFailureandPermissionRequest. It is committed as-is rather than stripped, so the file stays a faithful copy of what Claude Code actually runs. Two things to know: the commands hardcode absolute/home/jeremyadmin/...paths (unlike the rest of this repo, which is~-relative), and each is guarded by[ -f ] && [ -r ] && [ -x ], so on a machine without Orca they simply no-op.setup.shdoes not install Orca.⚠️ Orca rewrites~/.claude/settings.jsonand has replaced the symlink with a real file before, silently detaching it from this repo. If the two look out of sync, checkls -l ~/.claude/settings.json; re-runningsetup.shrestores the link and backs the detached copy up to~/.dotfiles-backup-*(fold anything you want to keep back into the repo — the backup is not merged for you).
~/.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)