Claude Code Plugins

Community-maintained marketplace

Feedback

chezmoi-development

@fx/dotfiles
0
0

This skill should be used when developing or modifying dotfiles using chezmoi. Covers using .chezmoidata for configuration data and modify_ scripts (or run_onchange_after_ scripts for symlinked directories) for non-destructive file merging. Particularly useful when needing to configure application settings without overwriting user preferences.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name chezmoi-development
description This skill should be used when developing or modifying dotfiles using chezmoi. Covers using .chezmoidata for configuration data and modify_ scripts (or run_onchange_after_ scripts for symlinked directories) for non-destructive file merging. Particularly useful when needing to configure application settings without overwriting user preferences.

Chezmoi Development

Overview

Develop dotfiles using chezmoi's advanced features for non-destructive configuration management. This skill focuses on two key patterns:

  1. Using .chezmoidata/ to store structured configuration data
  2. Using modify_ scripts (or run_onchange_after_ scripts for symlinked directories) to merge configuration into existing files without overwriting user settings

When to Use This Skill

Use this skill when:

  • Developing dotfiles with chezmoi that need to configure applications
  • Need to enforce required settings while preserving user preferences
  • Managing JSON/YAML/TOML configuration files in dotfiles
  • Application config files already exist and shouldn't be overwritten
  • Working with dotfiles in a team where users may have custom settings

Core Concepts

.chezmoidata Directory

Store structured configuration as data files (YAML, JSON, or TOML) in .chezmoidata/ at the root of the chezmoi source directory.

Purpose: Separate configuration data from templates, making it reusable across multiple files and easier to maintain.

Location: ~/.local/share/chezmoi/.chezmoidata/

Access in templates: Reference data via dot notation (e.g., {{ .app.config.setting }})

modify_ Scripts vs run_onchange_ Scripts

There are two approaches for non-destructive configuration management in chezmoi:

modify_ Scripts (Preferred for regular directories)

Create executable scripts that transform existing files by reading them via stdin and outputting the modified version to stdout.

Purpose: Update existing files non-destructively by merging new config with existing content.

Naming: modify_ + target file path + .tmpl

  • Example: modify_dot_config/app/settings.json.tmpl → modifies ~/.config/app/settings.json

Execution: chezmoi runs the script, captures stdout, and writes it to the target file.

How it works: Chezmoi provides file contents via stdin (use cat - to read), script outputs merged result to stdout.

Limitation: Cannot be used when the target directory is a symlink (chezmoi requires managing the directory).

run_onchange_after_ Scripts (For symlinked directories)

Create executable scripts in .chezmoiscripts/ that read from disk and write back to disk.

Purpose: Update files in directories that may be symlinks (common in Coder workspaces).

Naming: run_onchange_after_<name>.sh.tmpl in .chezmoiscripts/

  • Example: .chezmoiscripts/run_onchange_after_update-claude-settings.sh.tmpl

Execution: Script runs after other chezmoi operations, whenever the rendered script content changes.

How it works: Script reads from disk (cat "$FILE"), merges config, writes back to disk (echo "$result" > "$FILE").

Use when: Target directory is or may be a symlink, preventing chezmoi from managing individual files within it.

Workflow: Implementing Non-Destructive Config Management

Follow this workflow when implementing configuration management for an application:

Step 1: Define Configuration Data

Create a data file in .chezmoidata/ with the configuration to enforce.

Choose file format based on data complexity:

  • YAML for nested structures with comments
  • JSON for simple data or when templates need JSON output
  • TOML for flat key-value pairs

Example - .chezmoidata/myapp.yaml:

---
# Application configuration to enforce
required_settings:
  feature_flag: true
  api_endpoint: "https://api.example.com"

optional_defaults:
  theme: "dark"
  timeout: 30

Access in templates:

{{ .myapp.required_settings.feature_flag }}
{{ .myapp.optional_defaults.theme }}

Step 2: Create modify_ Script

Create a script that merges configuration data into the target file.

Script requirements:

  1. Read existing file from stdin (NOT from disk!)
  2. Load configuration from .chezmoidata via template
  3. Merge configuration (required settings take precedence)
  4. Output merged result to stdout

File naming for modify_ scripts:

  • For ~/.config/myapp/settings.jsondot_config/myapp/modify_settings.json.tmpl
  • The directory structure mirrors the target path
  • The modify_ prefix goes on the filename (not the directory)
  • The file must be executable (chmod +x)

Example - dot_config/myapp/modify_settings.json.tmpl:

{{- if .include_defaults -}}
#!/bin/bash
set -e

# Read existing settings from stdin (chezmoi provides current file contents)
# If stdin is empty (file doesn't exist), use empty object
existing=$(cat - || echo '{}')
if [ -z "$existing" ]; then
    existing='{}'
fi

# Load configuration from .chezmoidata
required='{{ .myapp.required_settings | toJson }}'
defaults='{{ .myapp.optional_defaults | toJson }}'

# Merge: existing + defaults + required (right side wins)
echo "$existing" | jq --argjson defaults "$defaults" \
                       --argjson required "$required" \
                       '. * $defaults * $required'
{{- end }}

CRITICAL for modify_ scripts: Chezmoi provides file contents via stdin, not by file path. Always use cat - to read from stdin. (Note: run_onchange_ scripts read from disk instead - see the distinction in Core Concepts above.)

For YAML files, use yq instead of jq:

# Merge YAML files
existing=$(cat "$HOME/.config/app/config.yaml" || echo '{}')
required='{{ .app.config | toYaml }}'
echo "$existing" | yq eval-all '. as $item ireduce ({}; . * $item)' - <(echo "$required")

Step 3: Make Script Executable (Optional - if needed)

Ensure the modify script is executable in the source directory:

chmod +x ~/.local/share/chezmoi/dot_config/myapp/modify_settings.json.tmpl

Note: Chezmoi automatically creates parent directories when writing files, so you typically don't need run_before_ scripts just to create directories.

Only use run_before_ scripts when you need to:

  • Remove old symlinks that would conflict with new files
  • Set special directory permissions
  • Install dependencies (like jq for JSON processing)

Step 4: Test the Implementation

Preview changes before applying:

# View what would be written to the file
chezmoi cat ~/.config/myapp/settings.json

# Show diff between current and new state
chezmoi diff

# Apply with dry run
chezmoi apply --dry-run --verbose

Test merge logic manually:

# Extract and test the modify script
chezmoi execute-template < modify_dot_config/myapp/settings.json.tmpl > /tmp/test_modify.sh
chmod +x /tmp/test_modify.sh

# Test with sample input
echo '{"userSetting":"value"}' | /tmp/test_modify.sh

Common Patterns

Pattern: Merge with jq

Merge JSON objects where required settings override existing ones:

echo "$existing" | jq --argjson required "$required_settings" \
                       '. * $required'

The * operator performs recursive merge with right-side precedence.

Pattern: Conditional Configuration

Apply different config based on environment or profile:

# .chezmoidata/app.yaml
{{ if eq .profile "work" -}}
config:
  api_url: "https://work.api.com"
{{ else if eq .profile "personal" -}}
config:
  api_url: "https://personal.api.com"
{{ end -}}

Pattern: Environment Variable References

Include environment variables in configuration:

# .chezmoidata/app.yaml
config:
  api_key: "{{ env "APP_API_KEY" }}"
  debug: {{ env "DEBUG" | default "false" }}

Pattern: Multi-File Configuration

Use same data across multiple files:

.chezmoidata/
  brand.yaml                          # Logo paths, colors, fonts

modify_dot_config/app1/settings.json.tmpl   # References {{ .brand.logo }}
modify_dot_config/app2/config.toml.tmpl     # References {{ .brand.colors }}
dot_bashrc.tmpl                             # References {{ .brand.theme }}

Symlinks and Execution Order

Using symlink_ Prefix

Chezmoi creates symlinks declaratively using the symlink_ prefix in the source state.

Naming: symlink_ + target path + .tmpl (template optional)

  • Example: symlink_dot_config/app.tmpl → creates symlink at ~/.config/app

Content: The file content (with trailing newline stripped) becomes the symlink target.

Example - symlink_dot_config/myapp.tmpl:

{{ if eq .profile "work" -}}
/shared/work/.config/myapp
{{ else -}}
/shared/default/.config/myapp
{{ end -}}

Conditional symlinks:

{{- if .is_coder -}}
/shared/.config/app
{{- end -}}

If the content is empty or whitespace-only after template processing, the symlink is removed.

Execution Order

Understanding execution order is critical to avoid race conditions:

  1. Read source state - Parse all files in chezmoi source directory
  2. Read destination state - Check current state of target files
  3. Compute target state - Determine what changes are needed
  4. Run run_before_ scripts - Execute in alphabetical order
  5. Update entries - Process all entries in alphabetical order by target name:
    • Regular files (dot_, private_, etc.)
    • Symlinks (symlink_)
    • Modified files (modify_)
    • Directories
    • Scripts (run_)
  6. Run run_after_ scripts - Execute in alphabetical order

Key insight: All entry types (files, symlinks, modify scripts) are processed together in step 5, sorted alphabetically by their final target path.

Avoiding Race Conditions

❌ WRONG - Creating directory in run_before prevents symlinking:

# .chezmoiscripts/run_before_00_setup.sh.tmpl
mkdir -p "$HOME/.config/app"

# Later, this fails because ~/.config/app already exists as a directory
# symlink_dot_config/app.tmpl
/shared/.config/app

✅ CORRECT - Use symlink_ declaratively:

# symlink_dot_config/app.tmpl
{{ if .is_coder -}}
/shared/.config/app
{{ end -}}

# modify_dot_config/app/settings.json.tmpl
# This works because symlink is created first (alphabetically)

✅ ALSO CORRECT - Let chezmoi create directories automatically:

# No run_before script needed!
# modify_dot_config/app/settings.json.tmpl
# Chezmoi automatically creates ~/.config/app/ when writing the file

When to Use Each Approach

Approach Use When Example
symlink_ Entire directory should point elsewhere Link ~/.config/app/shared/.config/app
modify_ Merge config into existing file Merge marketplace config into settings.json
dot_ regular file Fully manage file content Template ~/.bashrc from scratch
run_before_ Install dependencies, clean up old state Install jq, remove old symlinks
run_after_ Post-install tasks, restart services Run systemctl --user daemon-reload

IMPORTANT: Chezmoi automatically creates parent directories when writing files. You do NOT need run_before_ scripts to create directories for modify_ scripts or regular files.

Pattern: Conditional Symlinking in Coder

For Coder workspaces with persistent /shared/ storage:

# symlink_dot_config/gh.tmpl - Link to shared GitHub CLI config
{{- if .is_coder -}}
/shared/.config/gh
{{- end -}}

If .is_coder is false, the symlink won't be created. If it's true, the symlink points to persistent storage.

Pattern: Symlink vs Modify Decision

Use symlink when:

  • Entire directory managed externally (e.g., /shared/)
  • Content is already in a persistent location
  • No need to merge with existing content

Use modify when:

  • Need to merge with existing user settings
  • Want to preserve user customizations
  • Enforcing required settings while allowing optional ones

Example scenario - Claude Code config:

# ❌ BAD - Symlink loses user settings
symlink_dot_claude.tmpl → /shared/.claude

# ✅ GOOD - Modify merges marketplace config with user settings
modify_dot_claude/settings.json.tmpl → merges settings

Best Practices

Parent Directories Are Created Automatically

Chezmoi creates parent directories automatically. Do NOT create directories in run_before_ scripts unless you have a specific reason (like setting permissions).

❌ Unnecessary:

# .chezmoiscripts/run_before_setup.sh.tmpl
mkdir -p "$HOME/.config/app"  # Chezmoi will do this!

✅ Only when needed:

# .chezmoiscripts/run_before_setup.sh.tmpl
# Only if you need special permissions
mkdir -p "$HOME/.config/app"
chmod 700 "$HOME/.config/app"

Always Handle Missing Files

Check if target file exists before reading:

if [ -f "$HOME/.config/app/settings.json" ]; then
    existing=$(cat "$HOME/.config/app/settings.json")
else
    existing='{}'  # Sensible default
fi

Validate JSON/YAML Before Writing

Ensure output is valid before chezmoi writes it:

# Validate JSON
result=$(echo "$existing" | jq --argjson required "$required" '. * $required')
echo "$result" | jq empty  # Will fail if invalid
echo "$result"

Use Template Guards

Control when scripts execute based on configuration:

{{ if .include_defaults -}}
# Only execute when include_defaults is true
{{ end -}}

{{ if eq .profile "work" -}}
# Only execute for work profile
{{ end -}}

Separate Data from Logic

❌ Bad - Hardcode config in template:

echo '{"api":"https://api.com","timeout":30}' > ~/.app/config.json

✅ Good - Reference .chezmoidata:

# .chezmoidata/app.yaml
config:
  api: "https://api.com"
  timeout: 30
# modify_dot_app/config.json.tmpl
echo '{{ .app.config | toJson }}'

Document Configuration Structure

Add comments to data files explaining what each setting does:

---
# Database configuration for application
database:
  # Maximum number of connections in the pool
  max_connections: 100

  # Connection timeout in seconds
  timeout: 30

  # Enable query logging (set to false in production)
  log_queries: true

Script Ordering Matters

Use clear numeric prefixes to control execution order:

.chezmoiscripts/
  run_before_00_install-dependencies.sh.tmpl
  run_before_10_setup-directories.sh.tmpl
  run_before_20_remove-old-symlinks.sh.tmpl
  run_onchange_after_50_configure-apps.sh.tmpl

Troubleshooting

Race Condition: Directory Created Before Symlink

Problem: Want to symlink a directory, but it already exists as a real directory.

Cause: A run_before_ script or another entry creates the directory before the symlink is processed.

Solution 1 - Remove directory creation:

# Delete the run_before script that creates the directory
# Let chezmoi handle it via symlink_ or modify_

Solution 2 - Use symlink_ declaratively:

# symlink_dot_config/app.tmpl
/shared/.config/app

# Don't create ~/.config/app anywhere else!

Solution 3 - Remove existing directory:

# .chezmoiscripts/run_before_00_cleanup.sh.tmpl
if [ -d "$HOME/.config/app" ] && [ ! -L "$HOME/.config/app" ]; then
    # Backup if needed
    [ -n "$(ls -A "$HOME/.config/app")" ] && \
        mv "$HOME/.config/app" "$HOME/.config/app.backup.$(date +%s)"
    rm -rf "$HOME/.config/app"
fi

modify_ Script Not Running

Check template guard:

# View rendered script to see if template guard blocked it
chezmoi execute-template < modify_dot_app/settings.json.tmpl

Verify script is executable:

chmod +x ~/.local/share/chezmoi/modify_dot_app/settings.json.tmpl

Data Not Available in Template

List all available template data:

chezmoi data | jq

Check .chezmoidata file is valid:

# For YAML
yq eval .chezmoidata/app.yaml

# For JSON
jq . .chezmoidata/app.json

Merge Produces Incorrect Result

Test jq merge manually:

existing='{"user":"setting"}'
required='{"new":"value"}'

echo "$existing" | jq --argjson required "$required" '. * $required'

Check operator precedence:

  • * recursive merge (right side wins)
  • + concatenate (arrays append, objects merge)

Script Fails with "command not found"

Ensure dependencies are installed in run_before script:

# .chezmoiscripts/run_before_00_install-jq.sh.tmpl
#!/bin/bash
if ! command -v jq &> /dev/null; then
    if [ "$(uname)" = "Darwin" ]; then
        brew install jq
    else
        sudo apt-get install -y jq
    fi
fi

Declarative Package Installation

Chezmoi can install packages declaratively using a combination of .chezmoidata/packages.yaml and run_onchange_ scripts. This pattern ensures packages are installed when the package list changes.

Pattern: npm Package Installation

1. Declare packages in .chezmoidata/packages.yaml:

---
# Package declarations for declarative installation
# Top-level keys become template variables (e.g., .npm, .apt, .brew)
npm:
  global:
    - "@anthropic-ai/claude-code"
    - "typescript"

2. Create installation script .chezmoiscripts/run_onchange_after_install-npm-packages.sh.tmpl:

#!/bin/bash
# Install npm packages declaratively based on .chezmoidata/packages.yaml
# This script runs when the package list changes

{{ if .include_defaults -}}
set -e

# Function to run npm commands via mise
run_npm() {
    if command -v mise >/dev/null 2>&1; then
        mise exec -- npm "$@"
    else
        npm "$@"
    fi
}

# Check if npm is available (via mise or directly)
if command -v mise >/dev/null 2>&1; then
    if ! mise exec -- npm --version >/dev/null 2>&1; then
        echo "⚠️  Node.js/npm not available via mise. Skipping npm package installation."
        exit 0
    fi
elif ! command -v npm >/dev/null 2>&1; then
    echo "⚠️  npm not found. Skipping npm package installation."
    exit 0
fi

# Install global npm packages
{{ if .npm.global -}}
{{ range .npm.global -}}
if ! run_npm list -g "{{ . }}" >/dev/null 2>&1; then
    echo "📦 Installing {{ . }}..."
    run_npm install -g "{{ . }}"
    echo "✓ Installed {{ . }}"
else
    echo "✓ {{ . }} already installed"
fi
{{ end -}}
{{ end -}}

{{ end -}}

How it works:

  • The script template references .npm.global from .chezmoidata/packages.yaml
  • run_onchange_ prefix means the script executes when its rendered content changes
  • When you add/remove packages in packages.yaml, the rendered script changes, triggering re-execution
  • Each package is checked before installation to avoid redundant installs
  • Uses mise exec to ensure npm is available from mise-managed Node.js

Adaptable to other package managers:

  • apt: Create .chezmoidata/packages.yaml with apt: [...] and use {{ range .apt }}
  • brew: Create with brew: [...] and use {{ range .brew }}
  • pip: Create with pip: [...] and use {{ range .pip }}

Reference Documentation

For a complete, working example of this pattern, see:

  • references/chezmoidata-modify-example.md - Real-world Claude Code marketplace configuration

Quick Reference

Task Command
Preview file output chezmoi cat ~/.config/app/settings.json
Show changes chezmoi diff
Test template chezmoi execute-template < file.tmpl
View template data chezmoi data
Apply changes chezmoi apply
Dry run chezmoi apply --dry-run --verbose
Pattern Purpose
.chezmoidata/app.yaml Store structured configuration data
modify_dot_app/config.json.tmpl Merge config into existing file
run_before_00_setup.sh.tmpl Ensure prerequisites before applying
{{ .app.setting | toJson }} Convert data to JSON in template
jq '. * $required' Merge JSON with right-side precedence