A catalog-driven config for coding agents. MCP servers (custom and third-party),
skills, plugins, hooks, per-tool instructions, and pi packages are declared once in
catalog/ and synced into each agent by one command.
git clone git@github.com:msetsma/.dotfiles-agents.git
cd .dotfiles-agents
make # list targets (same as `make help`)
# the server repos are private: clone with the personal key (see below)
export GIT_SSH_COMMAND="ssh -F /dev/null -i ~/.ssh/id_ed25519_personal -o IdentitiesOnly=yes -o HostName=ssh.github.com -p 443"
make agent-update # clone the custom MCP server repos into vendor/
make agent-check # dry-run
make sync # render + merge into every agent
# one time: entra-mcp runs as a uv tool that also ships the `entra` CLI
uv tool install --editable ~/.dotfiles-agents/vendor/entra-mcp --with fastmcpThe custom MCP servers and Claude Code mods live in their own repos under
github.com/msetsma. This repo pins them the same
way it pins third-party tools: a catalog entry per server names its repo, and
make agent-update clones any that are missing into vendor/ (gitignored).
Machines whose profile excludes a server never clone it.
The repos are private, and plain git@github.com SSH here logs in as the work
account. ~/.gitconfig-personal only applies inside an existing clone, so the
first make agent-update needs the personal key in GIT_SSH_COMMAND. Clones use
git@github.com:msetsma/<repo>.git, so later pulls pick the personal key up
from the includeIf on their own.
.dotfiles-agents/
├── catalog/ # single source of truth: one file per resource
│ ├── mcp/*.toml # MCP servers (custom + third-party)
│ ├── skills/<name>.toml # skills (content in <name>/SKILL.md)
│ ├── plugins/<name>.toml # opencode plugins (<name>.ts) + Claude Code marketplace plugins
│ ├── hooks/<name>.toml # Claude Code / Codex event hooks
│ ├── instructions/*.toml # per-tool prompts (content in <name>.md)
│ ├── packages/*.toml # pi packages (npm / git / local source)
│ ├── clients.toml # where each agent keeps each kind
│ ├── paths.toml # machine paths + ${VARS}
│ └── retired.toml # resources to scrub from every client
├── bin/ # agent-sync, agent-update, clean-python, peek, demo-gif
├── mods/ # msetsma-mods marketplace: lists each mod's GitHub repo
├── mcp/
│ └── databricks-mcp/ # pins ai-dev-kit's MCP from git
└── vendor/ # gitignored; server repos cloned by `make agent-update`
├── notes-mcp/
├── sharepoint-mcp/
├── teams-mcp/
├── entra-mcp/ # also ships the `entra` CLI
└── spotify-mcp-server/ # third-party; build with Node >= 26
flowchart LR
C[catalog/] --> S[bin/agent-sync]
S -- merge --> M[MCP + hooks in shared client configs]
S -- symlink --> P[skills/ and plugins/ dirs]
S -- block --> I[global AGENTS.md]
catalog/ is the single source of truth. bin/agent-sync renders it into each
client with one of three strategies:
| Strategy | Used for | Behaviour |
|---|---|---|
| merge | MCP servers, hooks, pi packages, Claude Code plugins | rewrite only the managed subtree; leave the rest of the file alone |
| symlink | skills, opencode plugins | link one item per file/dir into the client's directory |
| block | instructions | replace one marked region in the client's global file |
A client declares one [<client>.<kind>] block per kind it supports; a kind with
no block is skipped.
| Client | Kind | Location | Handling |
|---|---|---|---|
| opencode | mcp | ~/.config/opencode/opencode.jsonc |
merge |
| opencode | plugins | ~/.config/opencode/plugins/ |
symlink |
| opencode | instructions | ~/.config/opencode/AGENTS.md |
block |
| Claude Code | mcp | ~/.claude.json → mcpServers |
merge |
| Claude Code | skills | ~/.claude/skills/ |
symlink |
| Claude Code | plugins | ~/.claude/settings.json → enabledPlugins + extraKnownMarketplaces |
merge |
| Claude Code | hooks | ~/.claude/settings.json → hooks |
merge |
| Claude Desktop | mcp | ~/Library/Application Support/Claude/claude_desktop_config.json |
merge |
| Codex | mcp | ~/.codex/config.toml → [mcp_servers] |
merge |
| Codex | hooks | ~/.codex/hooks.json → hooks |
merge |
| pi | packages | ~/.pi/agent/settings.json → packages |
merge |
opencode has no skills row; it inherits ~/.claude/skills/.
catalog/skills/<name>.toml # + <name>/SKILL.md
catalog/plugins/<name>.toml # + <name>.ts, or a [marketplace] block + plugin id
catalog/hooks/<name>.toml # event + matcher + command
catalog/instructions/<name>.toml # + <name>.md
catalog/packages/<name>.toml # source + optional resource filters
- Skills / opencode plugins: symlinked per item.
- Claude Code plugins: a plugin TOML with
plugin = "<name>@<marketplace>"and a[marketplace]block (source = "github"+repo,"git"+url, or"directory"+path). Sync enables it and registers the marketplace in~/.claude/settings.json, then runsclaude plugin install. Listing a plugin underpluginsinretired.tomldisables and uninstalls it and removes a marketplace nothing else uses. Install plugins here, not withclaude plugin install. - Hooks: merged additively; retire one via
hook_commandsincatalog/retired.toml. - Instructions: per-client, wrapped in a
<!-- dotfiles-agents:begin/end -->block. - Packages (pi): merged into the
packageslist by identity (source minus@version), so packages youpi installby hand stay put. Optionalextensions/skills/prompts/themesarrays become pi's resource filters. Sync runspi update --extensionswhen a package is added or changed. Edits made withpi configto a catalog package are overwritten on the next sync, so copy them into the TOML.
catalog/skills/github-glowup/, catalog/skills/peek/ and catalog/skills/demo-gif/ are real skills;
example-skill/ and hooks/example-hook.toml are templates.
bin/peek runs a CLI or TUI in a hidden tmux pane and prints its screen as
text, or with -o shot.png saves a real Ghostty screenshot, so an agent can
check terminal output itself (peek --help;
the peek skill tells agents when to use it). Link it onto your PATH once:
ln -s ~/.dotfiles-agents/bin/peek ~/.local/bin/peek.
bin/demo-gif records a terminal demo GIF with vhs: demo-gif new writes a
starter tape with an isolated HOME, and demo-gif renders it and extracts
frames to check (the demo-gif skill covers the full process). Link it the same
way: ln -s ~/.dotfiles-agents/bin/demo-gif ~/.local/bin/demo-gif.
Full table of what this repo manages. custom means written by me, with the
source in its own repo under github.com/msetsma, cloned into vendor/.
external means a third-party server (npx, uv tool, app, git source).
Show the table
| Server | Origin | Launch | Clients |
|---|---|---|---|
sharepoint-mcp |
custom | uv run --directory ~/.dotfiles-agents/vendor/sharepoint-mcp server.py |
opencode, Claude Code, Claude Desktop |
notes |
custom | uv run --directory ~/.dotfiles-agents/vendor/notes-mcp notes-mcp |
opencode, Claude Code, Claude Desktop |
teams-browser |
custom | uv run --directory ~/.dotfiles-agents/vendor/teams-mcp --extra mcp teams-browser-mcp |
opencode, Claude Code, Claude Desktop |
entra-mcp |
custom | entra-mcp (uv tool) |
opencode, Claude Code, Claude Desktop, Codex |
obscura |
external | ~/.local/bin/obscura mcp --stealth |
opencode, Claude Code, Claude Desktop |
apple-mail |
external | apple-mail-mcp (uv tool) |
opencode, Claude Desktop |
databricks |
external | uv run --project ~/.dotfiles-agents/mcp/databricks-mcp databricks-mcp |
opencode, Claude Code, Claude Desktop, Codex |
azure |
external | npx @azure/mcp@3.0.0-beta.29 server start |
opencode, Claude Code, Claude Desktop |
azure-devops |
external | npx @azure-devops/mcp ${ADO_ORG} |
opencode, Claude Code, Claude Desktop |
github |
external | remote api.githubcopilot.com/mcp/ |
opencode, Claude Code |
iMCP |
external | /Applications/iMCP.app/…/imcp-server |
opencode, Claude Code, Claude Desktop, Codex |
playwright |
external | npx @playwright/mcp@latest |
opencode |
spotify |
external | node ~/.dotfiles-agents/vendor/spotify-mcp-server/build/index.js |
Claude Code, Claude Desktop |
solidcraft |
external | solidcraft-cli mcp (ships with the SolidCraft app) |
Claude Code (Windows box only, via local.toml) |
| Plugin | Marketplace | What it does |
|---|---|---|
caveman |
JuliusBrussee/caveman | Terse reply mode plus commit, review, and memory-compress skills |
context7 |
anthropics/claude-plugins-official | Context7 MCP server for current library docs |
No pi packages are currently managed — the whole set was retired (see
catalog/retired.toml) and catalog/packages/ is empty.
The mechanism is unchanged: drop a catalog/packages/<name>.toml (see
Resource kinds), remove its source from retired.toml, then
make sync.
No secrets live here: github uses ${GITHUB_TOKEN}; machine-specific
values live in the untracked catalog/local.toml (catalog/local.toml.example).
The Mac is the primary box: with no [profile] in catalog/local.toml,
everything in the catalog syncs to every client. A secondary machine (e.g.
Windows) opts into a subset in its own untracked catalog/local.toml:
[profile]
clients = ["claude-code", "claude-desktop"] # or "auto"
exclude_tags = ["work", "macos"]
exclude = ["obscura"] # by name, any kind
[commands]
npx = ["cmd", "/c", "npx"] # list = command + arg prefix
[clients.claude-desktop.mcp] # over catalog/clients.toml
path = "~/AppData/Roaming/Claude/claude_desktop_config.json"
inject_path = ""
[mcp.playwright] # over catalog/mcp/playwright.toml
clients = ["claude-code", "claude-desktop"]Catalog entries carry tags: work (azure, azure-devops, databricks,
entra-mcp, teams-browser, sharepoint-mcp) and macos (apple-mail, iMCP,
sharepoint-mcp). Excluded resources are simply not synced; nothing is scrubbed.
"auto" picks clients whose config directory exists. Selection logic lives in
bin/agent_profile.py; full example in
catalog/local.toml.example.
On Windows: install uv (irm https://astral.sh/uv/install.ps1 | iex), enable
Developer Mode (skill symlinks), and run make from Git Bash. The
python-clean hooks are bash-only, so exclude them there.
Servers carry a [package] block; npm launch args use a {{package}}
placeholder resolved at render time.
make agent-outdated # report
make agent-update # apply, then re-sync| manager | how it updates |
|---|---|
npm |
bump the pinned ref; floating tags left alone |
uv-tool |
uv tool upgrade <tool> |
uv-project |
bump the pinned git tag + re-lock |
git |
git pull --ff-only; clones a missing repo into path; tag-pinned / no-upstream reported as manual |
source |
always current (runs from a working tree) |
manual / remote |
a binary/app or hosted endpoint; nothing to do |
- Server: add/edit
catalog/mcp/<name>.toml(setclients = [...]; addtags = ["work"]/["macos"]if it shouldn't reach every machine). For a custom server, create its repo under github.com/msetsma, point[package]at it (manager = "git",repo,path = "${AGENT_HOME}/vendor/<repo>") and runmake agent-updateto clone it. - Skill: add
catalog/skills/<name>.toml+catalog/skills/<name>/SKILL.md. - Hook: add
catalog/hooks/<name>.toml. - pi package: add
catalog/packages/<name>.toml(source = "npm:...",clients = ["pi"]) instead of runningpi install. - Retire: add the name (for hooks the command, for packages the source) to
catalog/retired.toml. make agent-check, thenmake sync.- New server or package? Add a row to the Inventory tables above.