← Writing

Managing Five AI Coding Agents from a Single Dotfiles Repo

  • chezmoi
  • dotfiles
  • ai-agents
  • developer-tools

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?

LayerFileschezmoi modeEdit flow
Templates.zshrc, .gitconfig, .aws/configRendered copychezmoi editchezmoi apply → commit
Agent configssettings.json, CLAUDE.md, skills, hooksSymlinkEdit 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.

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.

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:

  1. Zero drift. Agent edits via symlinks land directly in the git-tracked source. There’s no sync step to forget.

  2. Single source of truth. Skills, instructions, and hook binaries are defined once in agents-source/. Adding a skill makes it available to all agents.

  3. 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.

  4. Portable hooks. $PATH-based binary names with command -v guards mean hooks work on any machine after bootstrap, fail gracefully before it.

  5. 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.