Skip to content
NewTwo Macs, one Claude Code setup: skills as plugins, not symlinks
Logbook
Oct 30, 2026·4 MIN READ
Two Macs, one Claude Code setup: skills as plugins, not symlinks

Two Macs, one Claude Code setup: skills as plugins, not symlinks

I moved 36 Claude Code skills out of 17 tiny repos and a pile of symlinks into two plugins, one public and one private, with a make pull on the other machine. Here is what changed and why.

I work on two Macs, and my Claude Code setup had drifted apart. Today I pulled a week of tidying from the old machine onto the new one and found the real problem: the setup was not something you could pull. The ~/.claude folder was a git repo, but 17 of my skills lived in their own repos, linked in with symlinks that git never saw. The list of MCP servers sat in ~/.claude.json, outside the repo. A rebrand I did last week meant 17 identical commits, one per skill repo.

So I reorganised it the way Claude Code wants it, not the way it grew.

Skills are a plugin now

Claude Code already has a convention for "whose skill is this": plugin skills show up as plugin:skill. Figma's are /figma:figma-use, Anthropic's are /anthropic-skills:docx. I had been about to rename every folder to mtl-sumup, mtl-worktrees and so on to get the same effect. The plugin gives it for free.

All 36 skills moved into one folder, ~/.claude/marketplace/, which is a marketplace with two plugins:

.claude-plugin/marketplace.json   lists both plugins
mtl/                              24 public skills, a git submodule of msmenzyk/mtl-claude-skills
mtl-private/                      12 skills tied to my sites, brands and disk paths

Now /mtl:sumup is a public skill and /mtl-private:logbook is a private one. The split is by content, not by quality: anything with my paths, my domains or my brand rules stays private. The public plugin has its own README and MIT license, so anyone can run /plugin marketplace add msmenzyk/mtl-claude-skills and get the 24 generic ones.

A marketplace added from a local directory is loaded in place. I edit a SKILL.md, run /reload-plugins, and it is live. No version bump, no reinstall. That was the detail that made me stop renaming folders.

One command per direction

Two make targets replace the manual dance. make push commits the submodule first, then the dotfiles, and exports the MCP server list to a tracked mcp-servers.json with every secret-looking value blanked. make pull fast-forwards everything, clones a plugin repo that is missing, runs poetry install in an MCP server whose lock file changed, and merges the MCP list back into ~/.claude.json while keeping the secrets already on that machine.

A third target, make drift, runs every hour from launchd. It fetches all 45 repos involved and sends a macOS notification only when the picture changes: something behind, something not pushed, something with uncommitted files. It goes quiet again when all is in sync.

Two things that bit me

The secret scrubber started as a regex on env names ending in KEY or TOKEN. An automated review pointed out that this is allow-by-default. It is now deny-by-default: every env value is blanked unless the name is clearly a setting such as *_BASE_URL, headers are always blanked, URLs lose their user info and query string, and the export aborts if any known credential shape survives.

The second one was git. I moved a skill into the public plugin with git mv from the parent repo. Moving a file into a submodule path from the parent turns the submodule into plain copied files in the parent's index, silently. Two commits went out that way before git submodule status came back empty and I noticed. git rm -r --cached on the path and git add again fixed it.

Do not git mv across a submodule boundary. Move the file with plain mv, commit in the submodule, then commit the pointer in the parent.

It took one long session, most of it spent on the sync script and on checking that 17 old repos were clean before archiving them. If I did it again I would start with the plugin layout on day one, keep one repo per visibility level rather than one per skill, and write the drift watcher before the sync script, because knowing you are out of sync matters more than fixing it in one command.