This file provides guidance to AI coding agents when working with code in this repository.
- Essential Commands
- Verification Strategy
- Setup Entry Points
- Repository Architecture
- Editing Managed Files
- Chezmoi Patterns
- Documenting Architectural Decisions
- Script Conventions
- Troubleshooting
- Adding/Removing Software
- Platform-Specific Notes
# Inspect current template variables and flags
chezmoi data | less
# Preview what chezmoi would change
chezmoi diff
# Diagnose chezmoi configuration issues
chezmoi doctor
# Resolve the source file for a managed target
chezmoi source-path ~/.config/zsh/.zshrc
# Validate template rendering without applying
scripts/chezmoi-execute-template home/.chezmoiscripts/linux/run_onchange_after_10_install-apps.tmpl
# For managed files, render through chezmoi's built-in command
chezmoi cat --source-path ~/.config/zsh/.zshrcTest entrypoints:
# Run all automated tests (pytest + cram)
./script/test
# Run specific test types
./script/test -p # pytest only
./script/test -t # cram only
./script/test -p test/pytest/<test_file>.py # specific pytest file
./script/test -t test/cram/<suite> # specific cram suite
# Invoke runners directly (same as script/test delegates to)
./test/run-pytest # all tests under test/pytest/
./test/run-cram # all Cram suites under test/cram/
# Manually launch a container for interactive testing; requires Docker
./test/run-container arch # specific distro
./test/run-container -a # all distros with test container Dockerfiles
./test/run-container -i arch # preserve stdin/TTY for interactive setup prompts
./test/run-container -s arch # drop into user shell after setup (debug)
./test/run-container -r -s arch # stop after setup-root, then open a shell
./test/run-container -c 'chezmoi data' arch # run a command after the selected setup phase
./test/run-container -S arch # skip setup-system, only run setup
./test/run-container -b # build container images only, don't run
./test/run-container -n arch # dry run; show selected actions without running Docker
./test/run-container -w arch # workstation mode: DOTFILES_HEADLESS=0, DOTFILES_EPHEMERAL=0
./test/run-container -G arch # seed container GNUPGHOME from host ~/.gnupg
./test/run-container -C arch # skip shared nix store/cache volumes
./test/run-container debian ubuntu # multiple distros in one run (space-separated)State-changing commands. Run these only when the user explicitly asks for the corresponding operation, or when a task clearly requires it and you have stated the effect first.
# Apply dotfiles changes to home directory
chezmoi apply
# Apply home.nix changes, diff generations, commit, and switch
./script/home-manager-switch "Remove unused packages"
# Setup after cloning; applies dotfiles and user configuration
./script/setup
# System-level setup; may require sudo and install packages
./script/setup-systemHuman-owned update commands. Agents may mention these when relevant, but should not run them unless the user explicitly asks to perform the update workflow.
# Interactive review-before-update workflow for dotfiles, externals, and Home Manager inputs
./script/update
# Preview update workflow without applying changes
./script/update --dry-run
# Update dotfiles and external dependencies through chezmoi
chezmoi update -REnv vars for secrets-enabled testing (pass to ./test/run-container):
DOTFILES_SECRETS=1— mount host age key; enables chezmoi secret decryptionDOTFILES_PERSONAL=1— force personal profile (auto-detected from host if unset)DOTFILES_WORK=1— force work profileDOTFILES_SKIP_GPG_SECRET_IMPORT=1— skip interactive GPG secret key import
Pick the cheapest verification that covers the changed behavior. Prefer render checks and targeted tests before container-wide runs.
| Change type | Verification |
|---|---|
| Markdown-only docs | sed -n or rg -n review of the changed section |
| Chezmoi template syntax | scripts/chezmoi-execute-template <template> |
| Managed target rendering | chezmoi cat --source-path <source-path> |
| Final rendered dotfile diff | chezmoi diff |
| Shell scripts | sh -n <script> plus shellcheck when available |
| PowerShell scripts | PSScriptAnalyzer when available |
| Python helpers | ./test/run-pytest test/pytest/<test_file>.py |
| Cram scenarios | ./test/run-cram test/cram/<suite> |
| Linux setup or package-flow changes | ./test/run-container <distro> or ./test/run-container -w <distro> when GUI app installation is in scope |
| Cross-platform package changes | Combine the relevant render checks with the narrowest matching Cram or container test |
Use ./test/run-container without a distro only when a broad setup regression check is needed. It is slower and may require Docker setup.
Scripts form a progression from OS installation to dotfiles application:
-
script/{distro}/os-install— Install OS from live environment (Arch, Chimera) -
script/{distro}/setup-root— Root-level setup: create users, install base packages (Arch, Chimera, FreeBSD) -
script/bootstrap— Install prerequisites, clone repo, init chezmoi (run automatically by setup scripts) -
script/setup-system— System-level packages, Nix, prerequisites (non-root user with sudo) -
script/setup/chezmoi apply— Apply dotfiles and user-level packages -
script/setup-full— Runssetup-systemthensetup -
script/update— Human-owned interactive review-before-update workflow for dotfiles, externals, and Home Manager inputs. Agents may suggest it, but should not run it unless explicitly asked to perform that workflow. -
script/update --dry-run— Preview available updates without applying them. It checks chezmoi upgrade status, dotfiles repository updates, op CLI versions, container base image pins, Git/fetch externals, and Home Manager package updates. -
script/home-manager-switch [--dry-run] [--update-inputs "..."] ["subject"]— Apply home.nix changes, diff generations, commit all changes underhome/dot_config/home-manager/, and switch. Called byscript/update; also useful standalone when editinghome.nix. Dry-run mode builds a temporary candidate and reports the Home Manager generation diff without applying or committing.
This is a chezmoi-managed dotfiles repository supporting macOS, Linux (Arch, Debian, Ubuntu, Chimera, SteamOS), FreeBSD, and Windows.
home/- Chezmoi source directory (set via.chezmoiroot). Contains all managed dotfiles.script/- User-facing entry-point scripts (bootstrap,setup,setup-system,update,home-manager-switch)scripts/- Supplementary scripts, libraries, and extras (internal helpers, platform configs, and optional user-facing tools)common.sh- Shared bash functions (has,run,msg,die,need_sudo, package helpers)common.ps1- PowerShell equivalentchezmoi-*- Helper scripts for chezmoi operationsmacos/Brewfile*.tmpl- Homebrew package definitions
.github/- GitHub repository automation and metadataworkflows/- GitHub Actions workflows for repo maintenance and validationlabels.yml- Declarative GitHub label definitions synced by the labels workflow
settings/- Shared settings files (GnuPG, PowerShell, agent rules, etc.)agents/rules/- Private agent rule templates (personal, work) included byrender-agent-rules.md.tmplagents/- Encrypted data blobs for work-only agent configuration
test/- Test infrastructurecontainers/- Per-distro Dockerfiles. Arch has separateDockerfile.amd64andDockerfile.arm64; other distros useDockerfile.cram/- Structured Cram suites and helperscontainer/- Container-backed dotfiles scenario testsstatusline/- Statusline transcript tests
pytest/- Python unit tests for scripts and helpersrun-container- Container test driver scriptrun-cram- Wrapper arounduvx cramrun-pytest- Wrapper arounduvx pytest
docs/- Documentationdecisions/- Architectural Decision Records (MADR format)decisions/templates/- MADR templates; useadr-template.mdfor new ADRs
.chezmoi.toml.tmpl- Main config template with OS detection and feature flags.chezmoiexternal.toml.tmpl- External dependencies (tools, plugins, fonts).chezmoiignore.tmpl- Conditional file exclusions by OS/feature.chezmoidata/- YAML data files (fonts, paths, UI settings per host).chezmoiscripts/- Run scripts organized by platform:darwin/- macOS-specificlinux/- Linux-specificfreebsd/- FreeBSD-specificwindows/- Windows PowerShell scriptsposix/- Unix-like systems (shared)
.chezmoitemplates/- Reusable templatesexternal/- Modular external dependency templates
Edit source files in home/, scripts/, script/, settings/, or other repository paths. Do not edit rendered files in $HOME; chezmoi will overwrite them on the next apply.
For a managed target path, resolve the source before editing:
chezmoi source-path ~/.config/zsh/.zshrc
chezmoi cat --source-path ~/.config/zsh/.zshrcFor templates, render the source path directly before applying:
scripts/chezmoi-execute-template home/dot_config/zsh/dot_zshrc.tmplUse chezmoi diff to inspect the final rendered change. Run chezmoi apply only when applying to the live home directory is part of the requested task.
Key variables used throughout templates:
// OS Detection
$osid // "darwin", "linux", "linux-arch", "linux-chimera", "linux-debian", "linux-ubuntu", "linux-steamos", "freebsd", "windows"
$wsl2 // true if WSL2
$steamdeck // true if Steam Deck
$devpod // true if DevPod container
// Feature Flags
$ephemeral // Temporary machine (VM, container)
$headless // No GUI
$personal // Personal machine with secrets
$work // Work machine
$secrets // Has access to secrets (personal or work, non-ephemeral)
$coding // Development workstation
$container_runtime // Needs container runtime
$use_nix // Use Nix/home-manager
$user_setup // Full user setup vs dotfiles-onlyChezmoi run scripts follow this pattern:
run_onchange_before_*- Run before dotfiles, when content changesrun_onchange_after_*- Run after dotfiles, when content changesrun_once_*- Run once per machine (tracks state in chezmoi)- Numbered prefixes (00_, 10_, 20_, etc.) control execution order
Timing rule: CLI tools use before_ (dotfiles may detect them via find-tool); GUI apps use after_ (don't delay dotfiles for large installs). See docs/contributing-software.md for full timing guidance.
Chezmoi externals download non-package-manager resources. Templates under .chezmoitemplates/external/ cover tooling, agent content, editor assets, fonts, and other app data. Shared Git/fetch pinning supports the repo's review-first update flow, used by externals, install/update scripts, and any template consuming upstream content.
Sources and locks (in home/.chezmoidata/):
git-sources.yml/git-lock.yml— Git sources with resolution and review metadata; lock stores SHA (branch-tracked) or tag name (tagged).fetch-sources.yml/fetch-lock.yml— pinned single-file fetches; lock stores SHA-256 of bytes.container-sources.yml/container-lock.yml/container-targets.yml— test container base image sources, resolved digests, and Dockerfile targets. Lock entries storesha256:image digests.
Update tooling:
scripts/update-git-lock.py --dry-run --json— resolves candidate updates; emits structured metadata for downstream tooling.scripts/update-fetch-lock.py— resolves current bytes forfetch-sources.yml; writesfetch-lock.yml.scripts/update-container-pins.py --dry-run— resolves current test container base image digests viadocker buildx imagetools inspect; writescontainer-lock.ymland pinned Dockerfile references when run without--dry-run.script/update/script/update.ps1— orchestrate review-first flow: resolve updates, review candidates where applicable, prompt before apply, write locks, commit. Dry-run mode reports available changes without applying them.scripts/show-git-changes.py— review surface for a candidate: fetches old/new range, shows log/shortlog/diff, may run AI review. Forkind=tagGitHub sources, also includes release notes in the AI prompt.
Treat update execution as human-owned. Agents can explain the workflow, inspect lock files, and suggest running script/update, but should not run update commands unless the user explicitly requests that workflow.
When adding or changing test container images, update the container source, lock, target, and Dockerfile pin together. Prefer scripts/update-container-pins.py --dry-run to preview digest drift, then verify with the narrowest relevant container test or ./test/run-container -b <distro>.
For Arch container tests, test/run-container chooses test/containers/arch/Dockerfile.amd64 or test/containers/arch/Dockerfile.arm64 from the Docker server architecture. Use DOCKER_SERVER_ARCH only as a test override for that selection logic.
Per-source review metadata:
review_note— repo-specific instructions for AI review. Treat release notes as narrative context, not a substitute for diff review.review_paths— hard-scopes fetched log/diff; useful for large repos or release-tracking sources where only part of the tree matters.
- Age encryption with identity key from 1Password
- Encrypted files use
.ageextension chezmoi-edit-encryptedscript for editing encrypted files- Bootstrap scripts retrieve age key from 1Password during setup
- Encrypted fragment inclusion: non-managed
.agefiles can be spliced into templates viainclude | decrypt(e.g.,joinPath .chezmoi.sourceDir ".chezmoitemplates/external/agent-content-work.toml.age" | include | decrypt). Used for work-only chezmoi externals where the content (repo URLs) must stay encrypted in the public repo.
Agent rules, skills, and subagent configs are managed through a shared pipeline. Edit the source templates in this repository, not generated files under ~/.claude, ~/.codex, ~/.cursor, or ~/.agents.
- Rules: edit
home/dot_agents/rules/*.md.tmpl. These are sorted alphabetically and rendered byhome/.chezmoitemplates/render-agent-rules.md.tmplwith anagentparameter.- Claude output:
~/.claude/CLAUDE.md, rendered fromhome/dot_claude/CLAUDE.md.tmpl - Codex output:
~/.codex/AGENTS.md, rendered fromhome/dot_codex/AGENTS.md.tmpl - Cursor output:
~/.cursor/rules/agent-rules.mdc, rendered fromhome/dot_cursor/rules/agent-rules.mdc.tmpl
- Claude output:
- Skills: all agent tools symlink to
~/.agents/skills/. Source content comes from external archives, local skills inhome/dot_agents/skills/, and work-only encrypted repos linked byrun_onchange_after_40_link-work-agent-content.tmpl. - Agents (Claude Code only):
~/.claude/agentspoints at~/.agents/agents/. Static source links live inhome/dot_agents/agents/; work-only agents are linked into repo-name subdirectories byrun_onchange_after_40_link-work-agent-content.tmpl. - Agent-specific conditionals: templates can branch on the
agentparameter, for example{{ eq $agent "claude" }}.
Useful render checks after changing agent rules:
scripts/chezmoi-execute-template home/dot_claude/CLAUDE.md.tmpl
scripts/chezmoi-execute-template home/dot_codex/AGENTS.md.tmpl
scripts/chezmoi-execute-template home/dot_cursor/rules/agent-rules.mdc.tmpl- Create the directory under
home/with chezmoi naming (e.g.,home/dot_local/share/wget/) — chezmoi will create it on apply - Add a
.keepfile inside so git tracks the empty directory (git cannot track empty directories) - If the directory holds unmanaged runtime files (state, caches, tool-generated data), whitelist the directory in
.chezmoiignore.tmplwith!pathand ignore contents withpath/*— this prevents chezmoi from trying to manage files created by the tool at runtime - See
.local/state/zshand.local/share/wgetfor examples - Do NOT use
mkdir -pin run scripts for directories chezmoi should manage
.chezmoiremove.tmpltriggers interactive confirmation prompts — avoid for files that may reappear (e.g., tool-generated state files)- Prefer
rm -finrun_onchange_after_00_migrate-xdg-pathsfor cleaning up old XDG paths — runs non-interactively - Reserve
.chezmoiremovefor one-time removal of obsolete managed files that won't be recreated
Two-phase: template header computes $need_install via find-tool checks (renders script empty if nothing to do); body emits install commands only when needed. New GUI apps need entries in both the header (tool detection + $need_install trigger) and the body (flatpak install command).
Chimera uses musl libc — glibc-linked binaries (1Password, Vivaldi, Zed) can't run natively. The solution is an Ubuntu 22.04 distrobox: run_onchange_after_20_setup-distrobox.tmpl creates/updates the container from the host; scripts/setup-distrobox-chimera.sh runs inside to install apps and export .desktop files via distrobox-export. Only runs when .container_runtime, not .headless, not .ephemeral, and not already inside a container. Use this for glibc GUI apps on Chimera that are unavailable on Flathub or where Flatpak sandboxing is inappropriate (cross-app IPC, DE biometric integration, unrestricted filesystem access).
- All three accept
home_paths(bool, default true) andsystem_paths(bool, default true) to control which path sets are searched. Either is appropriate for user or system package detection. - None use
lookPath— search is path-list-only (frompaths.ymldata) for consistent behavior independent of shell$PATH. packagesForMissingToolswrapsavailableTools: takes a dict ofcmd→install-spec(bare pkg name, or full arg string like--git https://...for cargo), returns only the specs whose commands are missing.- Individual
find-toolcalls scale fine for 1–3 tools; preferpackagesForMissingToolsfor larger sets. Multi-package install scripts will likely migrate to this pattern.
Significant architectural decisions belong in docs/decisions/ as MADR-format ADRs.
When to create an ADR: When a decision is non-obvious, shapes how future work is done, has real alternatives that were considered, and would be valuable for someone to understand without reading the full git history. Implementation details, task plans, and temporary design notes do not need ADRs.
How to create one:
- Copy
docs/decisions/templates/adr-template.md(full) oradr-template-minimal.md(simple cases) - Name it
NNNN-kebab-case-title.mdwith the next available number - Set
status:toproposedinitially; update toacceptedonce the decision is made - Focus on why — the context, alternatives considered, and reasoning. The implementation lives in the code and commit history.
Status vocabulary: proposed → accepted → deprecated or superseded by ADR-NNNN
Agents should suggest creating ADRs when they make or implement a significant architectural decision, and may draft an ADR as part of completing such work.
For maintained shell scripts, read and apply the shell-script-style skill before substantial edits. Source scripts/common.sh instead of reimplementing shared helpers when writing repository scripts.
Common helpers from scripts/common.sh:
has <command> # Check if command exists
run <command> # Run command with logging
msg "text" # Print message
die "error" # Print error and exit
need_sudo # Ensure sudo access
is_mac / is_freebsd # OS detection
pacman_install <pkg> # Install via pacman
brewfile_install <file> # Install from Brewfile
uv_check <pkg> # Install Python tool via uv
cargo_check <pkg> # Install Rust tool via cargoPowerShell helpers live in scripts/common.ps1. Prefer existing helpers such as Install-WinGetPackageIfNotInstalled over one-off installer logic.
Template errors during chezmoi apply:
# Render a template to see exact output (catches syntax errors, missing vars)
scripts/chezmoi-execute-template home/.chezmoiscripts/linux/run_onchange_after_10_install-apps.tmpl
# For managed files, use chezmoi's built-in command
chezmoi cat --source-path ~/.config/zsh/.zshrcFind where a variable is set:
chezmoi data | grep -A2 'ephemeral' # Check current value and source
# Variables come from: .chezmoi.toml.tmpl, .chezmoidata/*.yml, or environmentScript fails during apply:
# Re-run a non-templated script manually with verbose output
bash -x "$(chezmoi source-path)/home/.chezmoiscripts/posix/run_onchange_after_50_setup-shell"
# Render and run a templated script with debug output
scripts/chezmoi-execute-template home/.chezmoiscripts/posix/run_onchange_before_25_install-tools.tmpl | bash -x
# Check whether chezmoi is skipping an unchanged run script
chezmoi status | grep run_onchangeExternal dependency fails to download:
# Check what externals chezmoi would fetch
chezmoi managed --include=externals
# Force re-fetch of externals
chezmoi apply --refresh-externalsSee docs/contributing-software.md for the full guide: platform paths (Brewfile, home.nix, install-tools, scoop, system-setup), package-manager preference order, timing rules (before_ vs after_), conditional flags, README entry formatting, and the verification checklist. On removal, README entries move from "Installed" to "Formerly-Used".
For software names and README summary lines, do not infer wording. Use verified source text from official project pages, official app-store listings, or upstream docs/README files. Preserve official capitalization in package or mas entry names. If reliable sources conflict or are unavailable, use neutral factual wording and explicitly flag the uncertainty instead of guessing.
- macOS: Uses Homebrew (prefix:
$HOME/.brew), optional split admin user setup - macOS admin Homebrew: Separate
Brewfile-admin.tmplfor shared/Users/Shared/homebrewprefix (Mac App Store apps, admin-requiring casks) - Linux: Nix/home-manager for packages when available; eget for Chimera gaps (no Nix); cargo/uv very limited
- Windows: Uses Scoop and winget, PowerShell scripts (
.ps1.tmpl).Install-Scoop-IfNotPresentis a legacy winget-compat shim being phased out — new GUI app installs usefind-toolin the template header + barescoop installin the body (seerun_onchange_before_25_install-tools.ps1.tmplfor the pattern). - FreeBSD: pkg/ports for tool installation