Managing Five AI Coding Agents from a Single Dotfiles Repo
If you use more than one AI coding agent, you’ve probably hit this: each tool wants its own config directory, its own instructions file, its own skills format. You edit one, forget to update the others, and drift silently accumulates.
I manage five agents — Claude Code, Cursor, Codex CLI, Gemini CLI, and GitHub Copilot — from a single chezmoi dotfiles repo with shared instructions, shared skills, and zero manual sync. Here’s how.
The core problem
AI agents edit their own config files. Claude Code writes to ~/.claude/settings.json when you approve permissions. Cursor appends to ~/.cursor/cli-config.json when you allow a shell command. Codex stores session state alongside its config.
chezmoi’s normal model is source → target copy: it renders a template and writes the result to $HOME. If an agent edits the target file, the chezmoi source is now stale. You’d need to chezmoi re-add constantly, or accept that your repo doesn’t reflect reality.
The fix is symlinks — but chezmoi can’t run Go templates through a symlink. So you need both models.
The dual-layer architecture
The repo splits files into two layers based on one question: does this file need templating?
| Layer | Files | chezmoi mode | Edit flow |
|---|---|---|---|
| Templates | .zshrc, .gitconfig, .aws/config | Rendered copy | chezmoi edit → chezmoi apply → commit |
| Agent configs | settings.json, CLAUDE.md, skills, hooks | Symlink | Edit at $HOME path → commit |
Templates need rendering because they contain OS-specific conditionals ({{ if eq .ostype "macos" }}), per-machine flags, or path substitutions. Agent configs are static content — they don’t vary by machine, and they need to be writable by the agents themselves.
How the symlinks work
All agent-editable content lives in agents-source/ inside the chezmoi repo:
agents-source/
├── AGENTS.md # shared global instructions
├── manifest.json # source of truth for generated links
├── claude/
│ ├── CLAUDE.md # Claude-specific instructions
│ └── settings.json # hooks, permissions, plugins
├── cursor/
│ ├── cli-config.json
│ └── hooks.json
├── codex/
│ ├── config.toml
│ └── hooks.json
├── skills/
│ ├── cognee/SKILL.md
│ ├── flo-aws/SKILL.md
│ └── obsidian-vault/SKILL.md
└── bin/
├── claude-statusline
├── dotfiles-doctor
└── dotfiles-validate
This directory is listed in .chezmoiignore so chezmoi never tries to apply it directly. Instead, chezmoi symlink templates point $HOME paths into it:
# ~/.claude/settings.json → agents-source/claude/settings.json
# Generated from: private_dot_claude/symlink_settings.json.tmpl
{{ .chezmoi.sourceDir }}/agents-source/claude/settings.json
When Claude Code edits ~/.claude/settings.json, the write follows the symlink and lands directly in the git-tracked file. No chezmoi re-add. No drift.
The symlink chain for skills
Skills are shared across agents via a directory symlink:
~/.agents → chezmoi-source/agents-source (one symlink)
~/.claude/skills → ../.agents/skills (relative symlink)
~/.cursor/skills → ../.agents/skills (relative symlink)
Claude Code and Cursor both discover skills from their skills directory. Since both point at the same agents-source/skills/ directory through the chain, a skill written once is available to both tools instantly.
Codex CLI uses per-skill symlinks (because it has built-in .system skills that would collide with a directory-level link):
~/.codex/skills/cognee → agents-source/skills/cognee
~/.codex/skills/flo-aws → agents-source/skills/flo-aws
~/.codex/skills/obsidian-vault → agents-source/skills/obsidian-vault
These are generated from a manifest file.
The manifest system
agents-source/manifest.json is the source of truth for two kinds of generated symlinks:
{
"codex_skills": [
"cognee",
"cognee-memory",
"flo-aws",
"obsidian-vault",
"using-datadog-cli"
],
"path_entries": [
{
"name": "claude-statusline",
"source": "agents-source/bin/claude-statusline"
},
{
"name": "dotfiles-doctor",
"source": "agents-source/bin/dotfiles-doctor"
}
]
}
codex_skills generates per-skill symlink templates under dot_codex/skills/. path_entries generates ~/.local/bin/<name> symlinks so helper scripts are on $PATH.
A Python script, dotfiles-sync-agent-links, reads the manifest and regenerates the chezmoi symlink templates:
dotfiles-sync-agent-links # regenerate from manifest.json
dotfiles-sync-agent-links --check # report drift without writing
It also cleans up stale templates for entries removed from the manifest. The workflow for adding a new skill:
mkdir -p ~/.agents/skills/my-skill
$EDITOR ~/.agents/skills/my-skill/SKILL.md
# Edit agents-source/manifest.json to add "my-skill" to codex_skills
dotfiles-sync-agent-links
chezmoi apply
chezmoi-agent-sync "add my-skill"
Shared instructions, tool-specific configs
All five agents read a shared AGENTS.md for global instructions — conventions, AWS account routing, hook patterns, memory layer config. Each agent gets it through a symlink appropriate to its tool:
~/AGENTS.md → agents-source/AGENTS.md
~/CLAUDE.md → AGENTS.md (relative)
~/.codex/AGENTS.md → ../AGENTS.md
~/.gemini/GEMINI.md → ../AGENTS.md
~/.copilot/AGENTS.md → ../AGENTS.md
Tool-specific config (Claude’s settings.json, Codex’s config.toml, Cursor’s hooks.json) lives in tool-specific subdirectories under agents-source/. Each is symlinked to the path the tool expects.
Hook conventions
Hooks reference binaries by name on $PATH, never by absolute path:
{
"type": "command",
"command": "command -v dcg >/dev/null && dcg"
}
The command -v ... && guard makes the hook a no-op if the binary isn’t installed yet. This matters during fresh-machine bootstrap — chezmoi applies the symlinks before the user has run brew bundle to install dependencies.
All hook binaries live in agents-source/bin/ with +x tracked in git (mode 100755), symlinked to ~/.local/bin/ via the manifest. The same dangerous-command-guard script serves Claude Code, Cursor, and Codex through their respective hook configs.
Helper scripts on $PATH
Scripts in agents-source/bin/ are executable in git and symlinked to ~/.local/bin/:
chezmoi-agent-sync — One command to commit and push agent edits:
chezmoi-agent-sync # commit "sync agent edits" + push
chezmoi-agent-sync "added k8s skill" # custom message + push
chezmoi-agent-sync --no-push # commit only
dotfiles-doctor — Health check that verifies symlinks point to correct targets, manifest sources exist and are executable, JSON is valid, and chezmoi data is populated.
dotfiles-validate — Pre-commit validation that renders all .tmpl files against multiple profiles (work-mac, personal-mac, cloud-linux), checks shell and Python syntax, and validates JSON/TOML.
claude-statusline — Custom status-line renderer that shows model, git branch, AWS profile/expiry, SST stage, and context usage in the Claude Code prompt footer.
What’s tracked vs. ignored
The .chezmoiignore file carefully separates versioned config from ephemeral runtime state:
| Tracked (symlinked) | Ignored |
|---|---|
~/.claude/{CLAUDE.md, settings.json, skills} | ~/.claude/{sessions, history.jsonl, plugins, cache} |
~/.cursor/{cli-config.json, hooks.json, skills} | ~/.cursor/{extensions, ai-tracking, ide_state.json} |
~/.codex/{config.toml, hooks.json, skills/*} | ~/.codex/{sessions, cache, auth.json, *.sqlite} |
Permission grants, hook configs, and instructions persist across machines. Session history, auth tokens, and caches stay local.
Wiring a new agent tool
When a new agent tool appears, it takes three symlinks to onboard:
# 1. Global instructions
echo '../AGENTS.md' > "$(chezmoi source-path)/dot_newtool/symlink_AGENTS.md"
# 2. Skills directory
echo '../.agents/skills' > "$(chezmoi source-path)/dot_newtool/symlink_skills"
# 3. Tool-specific config (if any)
mv ~/.newtool/config.toml agents-source/newtool/config.toml
echo '{{ .chezmoi.sourceDir }}/agents-source/newtool/config.toml' \
> "$(chezmoi source-path)/dot_newtool/symlink_config.toml.tmpl"
chezmoi apply
chezmoi-agent-sync "wire newtool"
The new tool immediately gets shared instructions, shared skills, and its own versioned config — with writes flowing back to git through the symlinks.
Why this works
The architecture solves a few things simultaneously:
-
Zero drift. Agent edits via symlinks land directly in the git-tracked source. There’s no sync step to forget.
-
Single source of truth. Skills, instructions, and hook binaries are defined once in
agents-source/. Adding a skill makes it available to all agents. -
Clean separation. Templates handle per-machine variation (OS type, work vs. personal). Symlinks handle agent-editable content. Each file uses the right mechanism for its nature.
-
Portable hooks.
$PATH-based binary names withcommand -vguards mean hooks work on any machine after bootstrap, fail gracefully before it. -
Easy onboarding. A new agent tool is three symlinks and one
chezmoi apply. The manifest system generates Codex-specific per-skill links automatically.
The repo is at github.com/jayantak/dotfiles if you want to see the full implementation.