Custom Data Files

Lesson 3 · chezmoi · ~10 minutes

In Lesson 2 you used built-in variables like .chezmoi.hostname to detect which machine you're on. That works for simple cases, but what happens when you want to define your own categories — like "this is a work laptop" or "install these packages on home machines only"? That's what .chezmoidata files are for.

The Mental Model

Chezmoi merges data from multiple sources into a single template data dictionary. Your custom data files sit in the source state and inject variables that templates can reference — without touching the config file.

Data merge hierarchy (later wins)
Config File
[data] section in
~/.config/chezmoi/chezmoi.toml
.chezmoidata.toml
Single file in
source state root
.chezmoidata/
Directory of data files
(merged alphabetically)

Each source overrides the previous one. Dictionaries are deep-merged, but lists and scalar values are replaced entirely. This means your .chezmoidata.toml can override anything set in the config file's [data] section, and files in the .chezmoidata/ directory override the single file.

Why not just use the config file?

The config file (~/.config/chezmoi/chezmoi.toml) is machine-local — it doesn't get committed to your repo. Data files live inside the source state, so they're version-controlled and shared across machines. Use the config for machine-specific secrets (API keys, passwords). Use data files for shared structured data (package lists, feature flags, categories).

Defining Custom Variables

Create a .chezmoidata.toml file in the root of your source state. You can use TOML, YAML, or JSON — pick whichever you prefer. TOML is the most common choice:

# ~/.local/share/chezmoi/.chezmoidata.toml

machine_type = "work"

[features]
install_docker = true
install_node = true
use_1password = false

[packages]
common = ["git", "ripgrep", "fd", "fzf", "jq", "bat"]
work = ["awscli", "terraform", "kubectl"]
home = ["ffmpeg", "yt-dlp", "imagemagick"]

Every key becomes a top-level template variable. After adding this file, you can reference .machine_type, .features.install_docker, and .packages.common in any template.

The same data in YAML

If you prefer YAML, create .chezmoidata.yaml instead:

# ~/.local/share/chezmoi/.chezmoidata.yaml

machine_type: work

features:
  install_docker: true
  install_node: true
  use_1password: false

packages:
  common:
    - git
    - ripgrep
    - fd
    - fzf
    - jq
    - bat
  work:
    - awscli
    - terraform
    - kubectl
  home:
    - ffmpeg
    - yt-dlp
    - imagemagick
Data files cannot be templates

.chezmoidata files are loaded before the template engine starts. You cannot use {{ }} template syntax inside them. If you need dynamic data computed at apply-time, put it in the [data] section of your config file template (.chezmoi.toml.tmpl) instead.

Using Custom Data in Templates

Here's a practical example — a .zshrc template that conditionally loads tools based on machine_type:

# ~/.local/share/chezmoi/dot_zshrc.tmpl

# Common aliases
alias ll="ls -la"
alias gs="git status"

{{ if eq .machine_type "work" -}}
# Work-specific: AWS and Kubernetes
export AWS_PROFILE=default
alias k="kubectl"
alias tf="terraform"
source <(kubectl completion zsh)
{{ end -}}

{{ if eq .machine_type "home" -}}
# Home-specific: media tools
alias yt="yt-dlp"
alias resize="magick mogrify -resize"
{{ end -}}

# Packages this machine should have installed:
# {{ if eq .machine_type "work" }}{{ range .packages.work }}#   {{ . }}
# {{ end }}{{ else }}{{ range .packages.home }}#   {{ . }}
# {{ end }}{{ end }}

You can also iterate over lists. Here's a Brewfile template that installs different packages per machine type:

# ~/.local/share/chezmoi/dot_Brewfile.tmpl

{{ range .packages.common -}}
brew "{{ . }}"
{{ end -}}

{{ if eq .machine_type "work" -}}
{{ range .packages.work -}}
brew "{{ . }}"
{{ end -}}
{{ end -}}

{{ if eq .machine_type "home" -}}
{{ range .packages.home -}}
brew "{{ . }}"
{{ end -}}
{{ end -}}

Exercise: Create Your Own Data File

Step 1: Create the data file

Open your source state and create a .chezmoidata.toml:

chezmoi cd

Create the file with your machine classification:

cat > .chezmoidata.toml << 'EOF'
# Machine classification
machine_type = "home"

[features]
install_docker = true
use_1password = false
EOF

Then return to your shell:

exit

Step 2: Verify the data is loaded

Run chezmoi data to see the merged data dictionary. Your custom variables should appear alongside the built-in .chezmoi object:

chezmoi data | grep -A 2 machine_type

You should see:

"machine_type": "home",

To see everything (it's a lot of output):

chezmoi data | less

You'll see your machine_type and features keys at the top level, alongside chezmoi (the built-in data object with hostname, OS, architecture, etc.).

Step 3: Use it in a template

Create a simple template that uses your new variable. Let's make a ~/.machine-info file that displays what kind of machine this is:

chezmoi cd
cat > dot_machine-info.tmpl << 'EOF'
Machine: {{ .chezmoi.hostname }}
Type: {{ .machine_type }}
Docker: {{ if .features.install_docker }}yes{{ else }}no{{ end }}
1Password: {{ if .features.use_1password }}yes{{ else }}no{{ end }}
EOF
exit

Step 4: Preview and apply

chezmoi cat ~/.machine-info

This shows what chezmoi would write without actually writing it. You should see your hostname and "home" filled in. If it looks right:

chezmoi apply ~/.machine-info

Step 5: Commit

chezmoi cd
git add .chezmoidata.toml dot_machine-info.tmpl
git commit -m "feat: add custom data file and machine-info template"
git push
exit

The .chezmoidata/ Directory

For more complex setups, you can split your data across multiple files using a .chezmoidata/ directory instead of (or in addition to) a single file:

~/.local/share/chezmoi/
├── .chezmoidata.toml          # base data
├── .chezmoidata/
│   ├── packages.toml          # package lists
│   └── work-tools.yaml        # work-specific config
├── dot_zshrc.tmpl
└── ...

Files inside .chezmoidata/ are merged in alphabetical order. This is useful when you want to organize data by concern — keep package lists separate from feature flags, for example.

Merge rules

Dictionaries (maps/objects) are deep-merged — keys from later files are added to or override keys from earlier files. But lists (arrays) are replaced entirely, not appended. If packages.toml defines packages.common and work-tools.yaml also defines packages.common, the YAML version wins completely.

What just happened

ConceptWhat it does
.chezmoidata.tomlDefines custom template variables in the source state
.chezmoidata/ directorySplits data across multiple files, merged alphabetically
chezmoi dataShows the full merged data dictionary (built-in + custom)
Merge hierarchyconfig [data] < .chezmoidata file < .chezmoidata/ directory
Lists vs dictsDicts deep-merge; lists and scalars are replaced entirely

Knowledge Check

Where does .chezmoidata.toml live?
Correct. Data files live inside the source state at ~/.local/share/chezmoi/. They're version-controlled alongside your templates.
Not quite. .chezmoidata.toml lives in the source state root — ~/.local/share/chezmoi/.chezmoidata.toml. It's committed to your repo alongside your templates.
If both your config file's [data] section and .chezmoidata.toml define machine_type, which value wins?
Right. The merge hierarchy is: config file data < .chezmoidata file < .chezmoidata/ directory. Later sources override earlier ones for scalar values.
The merge hierarchy means .chezmoidata.toml overrides the config file's [data] section. The order is: config data < .chezmoidata file < .chezmoidata/ directory.
Can you use {{ .chezmoi.hostname }} inside .chezmoidata.toml?
Exactly. Data files cannot be templates because they must be parsed before the template engine initializes. If you need dynamic data (computed from the environment at apply-time), put it in the [data] section of .chezmoi.toml.tmpl instead.
Data files are loaded before the template engine starts, so template syntax isn't available. For dynamic data, use the config file template (.chezmoi.toml.tmpl) with its [data] section.

Next up

You now have custom per-machine variables driving your templates. But dotfiles are only part of machine setup — you also need to install packages, configure macOS defaults, and run setup scripts. In Lesson 4: Scripts & Brew Bundles, you'll learn how chezmoi runs scripts on apply and how to declaratively manage your Homebrew packages.

Recommended Reading

chezmoi Reference: .chezmoidata.<format> — The official docs for data files, including multi-file merge behavior and format support. ~3 minute read.

Questions? Ask me anything that's unclear. I can explain the merge hierarchy with more examples, help you decide what belongs in data files vs. the config template, or troubleshoot why your variables aren't showing up in chezmoi data.
← Prev Next →