Skip to main content

Architecture

This setup is a chezmoi source directory. chezmoi renders templates and copies files into your $HOME.

Repository Layout

Documentation Hygiene

This repo treats docs/ as part of the configuration:

  • If you change dotfiles behavior (anything under home/ that affects commands or workflows), update docs/ in the same change.
  • If a change truly has no user-facing impact, record that in the PR/commit context so the docs/code divergence is explicit.

Repo-Only Assets (Not Installed Into $HOME)

Some directories in home/ are intentionally ignored by chezmoi and are used as "repo-local data" for scripts.

Ignore rules live in:

Examples in this setup:

Chezmoi Naming Conventions (How Source Maps To Installed Files)

Chezmoi uses filename conventions to decide where things land.

Common patterns in this setup:

  • home/dot_config/... -> ~/.config/...
  • home/dot_* -> ~/.<name> (for example: home/dot_zsh/ -> ~/.zsh/)
  • home/private_dot_ssh/... -> ~/.ssh/... (and treated as private by chezmoi)
  • executable_foo -> installs as an executable file named foo
  • readonly_foo -> installs as foo with 0444 permissions (read-only); used for config files that should not be modified by external tools at runtime
  • exact_ prefix in a directory name means "exact directory" (chezmoi does not merge it with existing contents)

This is why you may see paths like:

The Data Flow

This setup is intentionally declarative:

  1. You answer prompts in home/.chezmoi.toml.tmpl.
  2. Templates in home/ render differently depending on those values.
  3. Hooks in home/.chezmoiscripts/ install / update tools based on the rendered config.
  4. Re-running chezmoi apply converges you back to the intended state.

Dynamic AI Context Merging

Because AI tools (like OpenCode, Cursor, Gemini, and Pi) often rewrite their config files during runtime, rendering templates directly into those files causes conflicts. Instead, this architecture uses Profile-Based Merging:

  • MCP server definitions share a single canonical registry at home/.chezmoidata/mcp_servers.yaml. Each entry declares a work_only flag so work-specific servers are filtered at generation time.
  • During chezmoi apply, the unified script run_onchange_after_07-generate-mcp-configs.sh.tmpl calls scripts/generate_mcp_configs.py once and writes the result to Cursor, Claude Code, Pi, and any other tool that consumes the standard mcpServers JSON shape.
  • Tools with different MCP schemas (OpenCode, Codex) still derive from the same registry via small inject scripts in scripts/ that transform the canonical registry into the tool-specific config shape.
  • Gemini keeps its own settings file, but the mcpServers section is injected from the same registry at apply time.
  • This creates a hard boundary between work contexts (which load work-specific MCP servers) and personal contexts.

The same pattern applies to model definitions. For the full picture see MCP servers, Model registry & routing, and Tool configs.

Shared Library (scripts/chezmoi_lib.sh)

All run_onchange_after_07-merge-* scripts source a shared shell library at scripts/chezmoi_lib.sh for common operations:

FunctionPurpose
chezmoi_pick_srcResolve work vs personal source path
chezmoi_write_if_changedAtomic string write, skip if content unchanged
chezmoi_install_if_changedFile copy via install(1), skip if content unchanged
chezmoi_get_litellm_api_baseFetch and normalize LiteLLM URL from pass
chezmoi_record_checksumRecord file sha256 in the managed-configs manifest

After each write, the helpers record the target file's sha256 checksum in ~/.local/state/chezmoi/managed_configs.tsv. The ,doctor command reads this manifest to detect config drift — files modified externally by AI tools at runtime.

To add a new AI tool config, create work/personal source files and a merge script that sources the library — typically 5–10 lines of tool-specific logic.

Hooks (Automation)

The most important concept for understanding "what happens" is the hook naming:

  • run_once_before_* runs once before apply work.
  • run_once_after_* runs once after apply work.
  • run_onchange_after_* runs after apply when the tracked inputs change.

Examples in this repo:

HookPurpose
home/.chezmoiscripts/run_once_before_00-install-xcode.shXcode CLT
home/.chezmoiscripts/run_once_after_01-install-brew.shHomebrew install
home/.chezmoiscripts/run_once_after_02-install-fish.shFish install
home/.chezmoiscripts/run_onchange_after_03-install-brew-packages.fish.tmplBrew bundle
home/.chezmoiscripts/run_onchange_after_05-install-mise-runtimes.sh.tmplmise runtimes + shims
home/.chezmoiscripts/run_onchange_after_05-install-uv-versions.sh.tmplUV Python versions

Many hooks embed sha256sum comments that reference template content. That is how the "run on change" behavior is tied to specific files.

The Reference map lists every hook and helper script and the file each one drives.

Work vs Personal Split

The primary decision point is the isWork prompt in home/.chezmoi.toml.tmpl. It is used to:

  • conditionally include certain tools/plugins
  • choose which identity config is rendered
  • choose which secrets/setup steps run

External Assets

home/.chezmoiexternal.toml is used for things you want updated regularly but don't want to vendor into your dotfiles repo.

Current externals:

ExternalPurpose
tpmtmux plugin manager
EmmyLua.spoonHammerspoon Lua annotations
lowfi data filesBackground music tracklists
bat themesSyntax highlighting themes