Personal cross-platform (macOS + Linux) dotfiles managed with chezmoi.
Includes:
- zsh with antidote (static-cache) and Powerlevel10k —
~/.p10k.zshtracked in the repo so the prompt is byte-identical everywhere - vim with vim-plug (auto-bootstrapped on first launch)
- tmux with gpakosz/.tmux ("oh-my-tmux") vendored as a chezmoi external (pinned by commit SHA)
- iTerm2 preferences (macOS only, auto-configured to load from the repo)
- AI tooling (Claude Code + OpenAI Codex CLI) — global instructions, settings, rules, AGENTS.md, with tool-mutation-safe merging via
modify_scripts - mise (cross-platform) — declarative dev-tool list (4 language toolchains: Go/Python/Node/Rust, plus 29 aqua binaries +
op(direct-URL) +rtk(github) —claude-codeandcodexinstall as native binaries via aqua) indot_config/mise/config.toml.tmpl, tier-aware socore-profile machines only pull what dotfiles need - 3-tier install profiles —
core(dotfile baseline) /dev(+ CLI toolchain) /workstation(+ GUI apps per OS); always prompted on init with detected-env hint - rtk auto-installed via mise;
rtk initruns as a post-install step wiring Claude Code + Codex hooks
Supported: Intel + Apple Silicon, macOS recent versions.
One command:
sh -c "$(curl -fsLS get.chezmoi.io/lb)" -- init --apply vaintrubThat's it. chezmoi init prompts you for profile (core / dev / workstation — default core for safety; pick workstation for full Mac install with casks). hooks/ensure-prereqs.sh (registered via hooks.read-source-state.pre) installs Xcode CLT + Homebrew + mise on first run. Then run_onchange_after_50-install-packages.sh.tmpl reads .chezmoidata/packages.yaml, brew bundles packages.dev.brews (htop, tree, nmap, libpq, wireguard-tools — OS-native C-deps only, no formula in mise registry) + packages.gui.mac_casks if workstation (iterm2, docker-desktop, vscode, ngrok, 1password desktop, fonts), then runs mise install for the full dev toolchain — including op, rtk, claude-code, and codex as cross-platform native binaries. Post-installs (goimports, ssh-audit, rtk init for Claude+Codex hooks) run from the same lib. Plugins + caveman install automatically afterward.
The repo lands at chezmoi's default source location: ~/.local/share/chezmoi/. No --source flag, no install.sh, no convention overrides — chezmoi cd and chezmoi edit work seamlessly without per-machine setup.
After the first apply, quit iTerm2 once and relaunch (choose "Don't Save" if prompted) — iTerm2 reads its prefs at startup.
Supported: Debian 12+, Ubuntu 22.04+, Fedora 40+. Not supported: Alpine, Arch (the install-packages script's distro branch prints an explicit "unsupported, install manually" message in that case).
Bootstrap — install just enough to run chezmoi:
sudo apt install -y git curl sudo # Debian/Ubuntu
# or
sudo dnf install -y git curl sudo # FedoraThen the canonical one-liner:
sh -c "$(curl -fsLS get.chezmoi.io/lb)" -- init --apply vaintrubThe repo lands at ~/.local/share/chezmoi/ (chezmoi's default source location). chezmoi init ALWAYS prompts for profile (Codespaces / non-interactive runs fall back to core via --promptDefaults). From there chezmoi takes over:
hooks/ensure-prereqs.shapt/dnf installsgit zsh vim tmux curl ca-certificatesif not present + curl-pipes mise into~/.local/bin/(runs BEFORE chezmoi reads source state).run_onchange_after_50-install-packages.sh.tmplruns tier-cascade vialib/install-packages.sh: (a)apt-get/dnf installcore packages always; ifdev, also dev packages; (b)mise installfrom~/.config/mise/config.toml— atcorejust fzf+zoxide, atdevthe full toolchain (which now includesop,rtk,claude-codeandcodexas native binaries via mise's aqua/github/direct-URL backends); (c)dev-only post-installs:goimports(go install),ssh-audit(uv tool install),rtk init -gfor Claude+Codex hooks.70-install-pluginsregisters Claude Code + Codex plugins listed inpackages.yaml(caveman included — installed via canonicalclaude plugin install caveman@caveman, no bespoke installer).linux/run_once_after_install-fonts.sh.tmplrunsfc-cacheafter MesloLGS NF + Monaspace fonts are downloaded by.chezmoiexternal.toml.tmpl. Skipped on headless boxes (SSH with no$DISPLAY/$WAYLAND_DISPLAY).linux/run_once_after_chsh.sh.tmplsetszshas your login shell. Skipped ifchshwould block on non-interactive password prompt.
chezmoi init ALWAYS prompts for the install profile (no silent auto-decision). It detects the environment and shows it as a HINT in the prompt; you pick explicitly. The default is core (safe fallback for CI / non-interactive runs / accidental enter). Value cached in ~/.config/chezmoi/chezmoi.toml [data].profile. Three tiers, use-case-driven:
| Profile | What you get | Use case |
|---|---|---|
core |
OS-native baseline (zsh, vim, tmux, git, curl, ca-certificates; Linux only: ufw, tcpdump) + MesloLGS NF + Monaspace Neon fonts + mise + fzf + zoxide. ~50 MB total. |
Hetzner VPS, DigitalOcean droplet, jetson over SSH first-touch, Codespace, recovery. |
dev |
core + CLI dev toolchain: 4 language toolchains (Go/Python/Node/Rust) + 27 dev-tier aqua binaries (kubectl/helm/k9s/kustomize/stern/argocd/opentofu/awscli/rclone/jq/gh/delta/fd/yq/shellcheck/buf/golangci-lint/goreleaser/gotestsum/protoc-gen-go/pnpm/uv/cloudflared/websocat/gitleaks/claude-code/codex) + op (direct-URL) + rtk (github). Linux apt: htop, tree, wget, nmap, telnet, wireguard-tools, postgresql-client, build-essential, xsel/wl-clipboard (docker not installed — pick distro repo or workstation Docker Desktop). Mac brews (only tools without mise registry entry): htop, tree, wget, nmap, telnet, libpq, wireguard-tools. Post-installs (goimports, ssh-audit, rtk init). ~1.9 GB first apply. |
Headless dev box (jetson, Linux VM, VPS for real work, Mac SSH'd into headless). |
workstation |
dev + GUI apps. Mac: casks (iterm2, docker-desktop, visual-studio-code, ngrok, font-meslo-lg-nerd-font, font-monaspace). Linux: placeholder (empty for now — populate when I run a Linux desktop). |
Primary GUI machine (Mac laptop, Linux desktop). |
Hint format in prompt: Install profile [detected env: workstation (GUI=true, SSH=false, ephemeral=false)] — you pick (core/dev/workstation, default core)?. Detected env helps you pick; doesn't pre-select.
To change later: edit [data].profile in ~/.config/chezmoi/chezmoi.toml directly (value: "core", "dev", or "workstation"), then chezmoi apply. The install script's rendered output changes → run_onchange re-fires automatically. The mise config (~/.config/mise/config.toml) also re-renders profile-aware: switching to core removes language toolchains from mise; switching to dev or workstation adds them.
To force re-prompt: chezmoi init --prompt.
Mac firewall (ufw equivalent): not managed by dotfiles. Enable manually via
System Settings → Network → Firewall. Linux core tier installs ufw (disabled
by default — sudo ufw enable to activate).
For a fresh machine (Mac or Linux), step-by-step:
# 1. Bootstrap one-liner — installs chezmoi, clones source, runs apply.
sh -c "$(curl -fsLS get.chezmoi.io/lb)" -- init --apply vaintrub
# 2. At the prompt, pick your profile (default core; pick workstation on
# Mac laptop, dev on jetson / Linux server, core elsewhere).
# 3. After apply finishes, reload your shell so mise shims land on PATH:
exec zsh
# 4. Verify the install:
mise ls # all rows should be without "(missing)"
chezmoi diff # should be emptyIf step 4 shows any (missing) rows OR command-not-found errors, the
anonymous GitHub API rate limit was likely exhausted mid-install. Jump to
the Recovery section below.
# Anonymous GitHub rate-limit ran out → some mise tools missing.
# Pick one (see §"1Password integration → Install-time" for full options):
gh auth login # cache token (subsequent applies auto-pick via `gh auth token`)
chezmoi apply # picks up token, retries missing tools idempotently
# mise shims out of date / new tools not yet symlinked:
mise reshim
exec zsh
# `chezmoi.toml` cache has stale keys (old `personal`/`headless`):
chezmoi init --promptDefaults # rewrites from current template
chezmoi apply
# Touch ID for sudo accidentally rolled back by a macOS major update:
chezmoi state delete-bucket --bucket=scriptState
chezmoi apply # re-runs run_once_after_* scripts (incl. pam_tid)Drop a ~/.config/mise/config.local.toml (untracked, never overwritten) to
override specific tools per machine:
[tools]
# Skip Rust on a 2 GB VPS — no need for ~300 MB toolchain.
rust = "skip"
# Use older Python on this box (mise resolves to latest 3.11.x).
python = "3.11"
# Don't install gh on a box where you only need kubectl + jq.
"aqua:cli/cli" = "skip"mise merges config.toml (chezmoi-managed) and config.local.toml (user-
owned) at runtime — local wins on key conflicts. After editing, mise install
to apply.
Three distinct secret-handling concerns; each uses its own mechanism.
No prereq normally required — anonymous 60/hr GH API limit usually fits a
cold install on a fast link. The install-packages wrapper auto-picks
GITHUB_TOKEN from gh auth token if gh is authed; otherwise it
falls through to anonymous.
If you hit the rate limit (slow link, shared IP, multiple rebuilds in
one hour), mise_install_tools() surfaces a partial-install warning
with three recovery options:
gh auth login # cached token, recommended
export GITHUB_TOKEN=ghp_yourPAT # one-off env
export GITHUB_TOKEN=$(op read 'op://Personal/GitHub API Token/credential')Then chezmoi apply — mise retries missing tools idempotently. After
gh auth login no manual env-export is needed on subsequent applies.
Tools that read credentials from on-disk files (npm, aws, rclone, docker,
git HTTP creds) need the secret baked into the target. Use chezmoi-native
render-time funcs against a single catalog at .chezmoidata/secrets.yaml.
Workflow when a new render-time secret is needed:
-
Add the 1Password reference to the catalog:
# .chezmoidata/secrets.yaml op_refs: npm_token: "op://Personal/NPM Registry/token"
-
Create a
private_dot_<file>.tmplreferencing it://registry.npmjs.org/:_authToken={{ onepasswordRead .op_refs.npm_token }} -
chezmoi apply— file lands at~/.npmrcmode 0600 with the secret baked in. Touch ID prompts once per apply (1Password CLI 10-minute session covers subsequent reads).
Use 1Password Shell Plugins per CLI — biometric-gated, lazy, no disk exposure:
op plugin init gh
op plugin init aws
op plugin init npmNot in any catalog — op manages auth per CLI internally.
op itself is installed cross-platform by mise (Mac + Linux arm64/amd64)
at the dev / workstation profile — see dot_config/mise/config.toml.tmpl
dev block (unqualified op shortname → mise direct-URL backend against
1Password's CDN).
Mac workstation additionally installs the 1Password desktop app cask,
which enables Touch ID biometric unlock so op read works without op signin.
Linux headless boxes use OP_SERVICE_ACCOUNT_TOKEN instead of biometric.
Defined declaratively in dot_config/mise/config.toml.tmpl (profile-aware — rendered to ~/.config/mise/config.toml). Same tools install identically on Mac + Linux.
core profile installs only what dotfile integrations need:
| Category | Tools |
|---|---|
| Shell integrations | fzf (zshrc Ctrl-R, Ctrl-T bindings), zoxide (zshrc z/zi) |
dev profile adds the full dev toolchain (workstation cascades — includes everything in dev):
| Category | Tools |
|---|---|
| Languages | node (LTS), go (latest), python (3.12), rust (stable) |
| CLI utilities | jq, gh, delta, shellcheck, fd, yq, gitleaks |
| Cloud + K8s | helm, kubectl, k9s, kustomize, stern, argocd, tofu (opentofu), aws (awscli), rclone, cloudflared |
| Networking | websocat |
| Go ecosystem | buf, golangci-lint, goreleaser, gotestsum, protoc-gen-go |
| Package managers | pnpm, uv |
| Auth + secrets | op (1Password CLI, direct-URL backend) |
| Token-saving proxy | rtk (github:rtk-ai/rtk, native binary) |
| AI CLIs | claude-code (aqua:anthropics/claude-code, native), codex (aqua:openai/codex, native rust) |
| Post-install (after mise install) | goimports (go install), ssh-audit (uv tool install), rtk init (Claude+Codex hooks) |
Adding a tool: mise registry | grep -i <name> → use the aqua slug → add line to dot_config/mise/config.toml.tmpl (inside the $isDev block unless every tier needs it) → chezmoi apply (or mise install).
Removing a tool: delete line, optionally mise uninstall <name>.
Tools NOT in mise's aqua registry — C apps with libpcap/curses/PAM deps, system services, or platform-specific. Defined in .chezmoidata/packages.yaml under packages.{core,dev,gui} sections, installed cascade-style by run_onchange_after_50-install-packages.sh.tmpl.
| Tier | OS | Packages |
|---|---|---|
| core | Linux apt/dnf | zsh, vim, tmux, git, curl, ca-certificates, ufw, tcpdump |
| core | Mac brew | (none — Apple bundles zsh/vim/tmux/git/curl + system /usr/sbin/tcpdump; no Mac equivalent for ufw — use pfctl) |
| dev | Mac brew | htop, tree, wget, nmap, telnet, libpq (psql, force-linked), wireguard-tools |
| dev | Linux apt | build-essential, xsel, wl-clipboard, htop, tree, wget, nmap, inetutils-telnet, wireguard-tools, postgresql-client |
| dev | Linux dnf | (same as apt, modulo Fedora naming) |
| workstation | Mac casks | iterm2, docker-desktop, visual-studio-code, ngrok, 1password (desktop), font-meslo-lg-nerd-font, font-monaspace |
Docker is intentionally NOT in the dev tier: docker.io (Ubuntu repo) and docker-ce (Docker's official apt repo) can't coexist, and a pre-existing install would break apt-get install. Pick the flavour that fits the machine: sudo apt install docker.io (simple), Docker's official repo for docker-ce (production), or workstation profile on Mac (Docker Desktop cask).
Ad-hoc installs: brew/apt/dnf still primary on each platform. Want mongosh? brew install mongosh (Mac) or follow MongoDB's apt repo (Linux). Want kubectl debug version? mise use kubectl@1.34.2 (per-project) or edit the global config.
First-apply latency at dev or workstation: cold install downloads ~1.9 GB (4 language toolchains × ~300 MB + ~30 binaries via mise). Realistic ranges: 5-10 min on fast link, 15-30 min on residential, 30-60+ min on slow links / jetson. core profile only pulls fzf+zoxide (~5 MB).
GitHub API rate limit: mise's aqua + github backends hit api.github.com ~30-50 times during a cold dev install. Anonymous limit is 60/hr — usually fits. If hit, the install-packages partial-install warning surfaces; see §"1Password integration → Install-time" above for recovery options.
Troubleshooting slow zsh prompt: MISE_TIMINGS=1 exec zsh — total < 50ms per prompt is healthy. If consistently above, switch to mise shim mode in dot_zshrc:
command -v mise > /dev/null && eval "$(mise activate --shims zsh)"Per-machine mise overrides: skip a heavy toolchain (e.g. Rust on jetson) by creating untracked ~/.config/mise/config.local.toml with a [tools] block that overrides specific entries. mise auto-merges later files over earlier — don't commit, it's per-machine.
Source files in this repo use chezmoi's naming convention. The mapping is mechanical:
| Source name | Becomes in $HOME |
|---|---|
dot_zshrc |
~/.zshrc |
dot_claude/CLAUDE.md |
~/.claude/CLAUDE.md |
dot_codex/AGENTS.md |
~/.codex/AGENTS.md |
executable_X |
mode +x |
private_X |
mode 0600 |
symlink_X.tmpl |
symbolic link (body = target) |
modify_X |
script: stdin = existing file, stdout = new contents |
modify_X + #chezmoi:modify-template |
template: .chezmoi.stdin = existing; output = new contents |
.chezmoiscripts/run_* |
scripts that don't create $HOME files (per-OS subdirs darwin/ + linux/; root scripts use 50/70/99- numerical prefix for ordering — 50 installs packages, 70 installs plugins, 99 prints post-install hint) |
.chezmoihooks/* |
hook scripts registered in .chezmoi.toml.tmpl (e.g. read-source-state.pre) |
.chezmoiexternal.toml.tmpl |
vendored externals (archives, file downloads) |
.chezmoiignore |
exclude target paths from apply |
.chezmoiversion, .chezmoiremove.tmpl |
minimum-version pin + deprecation-tracking list |
Source of truth is ~/.local/share/chezmoi/ (chezmoi's default — XDG_DATA_HOME compliant). On my Mac there's also a back-compat symlink ~/dotfiles → ~/.local/share/chezmoi for muscle memory; not required.
Changes to .chezmoi.toml.tmpl (prompts, hooks, [data] keys) make the rendered local config drift from the cached template hash. On the next apply chezmoi prints:
chezmoi: warning: config file template has changed, run chezmoi init to regenerate config file
Clear it with:
chezmoi init --promptDefaultschezmoi init rewrites ~/.config/chezmoi/chezmoi.toml from the template. With no custom sourceDir override to lose, this is now idempotent — nothing manual to restore.
Day-to-day flow (use chezmoi cd instead of remembering the source path):
chezmoi cd # opens subshell in source dir
vim dot_zshrc # edit source
chezmoi diff # preview what would change in $HOME
chezmoi apply # apply to $HOME
git commit -am '...' && git push
exit # leave the chezmoi-cd subshellOn another machine: chezmoi update (= git pull + apply).
Prefix is C-a (screen-style). Status bar, bindings, theme inherit from gpakosz/.tmux unchanged — see their docs.
oh-my-tmux is vendored via .chezmoiexternal.toml.tmpl — pinned to a specific commit SHA (gpakosz/.tmux has no tagged releases). Bump cycle: edit SHA in URL → chezmoi apply -R → commit.
Customizations in dot_tmux.conf.local (becomes ~/.tmux.conf.local):
- Prefix
C-aas the sole prefix (verbatim from gpakosz's documented snippet) mouse on— wheel scrolls tmux scrollback, drag-resizes panes, click selectsmode-keys vi— copy-mode uses vim navigationset-clipboard on(OSC 52) — vim/nvim"+yinside tmux reaches the system clipboardtmux_conf_copy_to_os_clipboard=true— copy-modeywrites to OS clipboard. gpakosz default isfalse.tmux_conf_24b_colour=true— forces 24-bit colour so Powerlevel10k renders identically inside tmuxCOLORTERM=truecolorpropagated to inner shellshistory-limit 50000— gpakosz default 5000 is small
Gotchas:
- iTerm2 ≥3.5.11 required —
3.5.0beta6–3.5.0beta10had a regression where Nerd Font glyphs disappear inside tmux panes (iTerm2 #10879). - If Powerlevel10k complains about an instant-prompt warning inside tmux, add
typeset -g POWERLEVEL9K_INSTANT_PROMPT=quietto~/.p10k.zsh. - Linux without
xsel/xclip/wl-clipboard: tmux copy-modeysaves to the tmux paste buffer only, not the system clipboard.
The committed iterm/com.googlecode.iterm2.plist is loaded via PrefsCustomFolder. A run_once_after_* script tells iTerm2 to load prefs directly from the chezmoi source tree (~/.local/share/chezmoi/iterm/) — no ~/.config/iterm/ symlink layer.
GUI edits flow through to git: when you change settings in iTerm2 and quit (answer "Save" if prompted), iTerm2 writes directly into ~/.local/share/chezmoi/iterm/com.googlecode.iterm2.plist. git status (from chezmoi cd) shows the dirty plist — review and commit.
Trigger setup — the committed plist ships a vscode:// Trigger pre-installed on the default profile (used by the VSCode-from-any-terminal flow).
Gotcha — iTerm2's plist stores trigger actions as bare Objective-C class names. "Run Command…" in the UI maps to
"ScriptTrigger"in the plist, not"RunCommandAction". iTerm2 silently drops triggers whose action class doesn't exist.
Global, cross-machine config for both CLIs. Only user-curated settings are tracked; auth tokens, marketplace registrations, plugin caches, NUX state, project trust-levels — all stay machine-local.
| Source path | Becomes in $HOME |
Pattern | Purpose |
|---|---|---|---|
dot_claude/CLAUDE.md |
~/.claude/CLAUDE.md |
plain | global instructions (bootstrap + TL;DR rule index) |
dot_claude/modify_settings.json |
~/.claude/settings.json |
modify_ (jq merge) |
global settings, preserves tool-added keys |
.chezmoitemplates/claude-settings-base.json |
(not applied) | template partial | curated base, loaded via includeTemplate from modify_ |
dot_claude/executable_statusline-command.sh |
~/.claude/statusline-command.sh |
plain +x | custom status-line renderer |
dot_claude/agents/ |
~/.claude/agents/ |
plain dir | custom subagents |
dot_claude/skills/ |
~/.claude/skills/ |
plain dir | custom skills (one dir per skill, SKILL.md inside) |
dot_claude/rules/*.md |
~/.claude/rules/*.md |
plain | auto-loaded rules (optional paths: frontmatter for conditional load) |
dot_codex/AGENTS.md |
~/.codex/AGENTS.md |
plain | global instructions for Codex |
dot_codex/modify_config.toml |
~/.codex/config.toml |
modify_ (toml merge) |
model + plugin enables, drops runtime sections |
.chezmoitemplates/codex-config-base.toml |
(not applied) | template partial | curated base, loaded via includeTemplate from modify_ |
dot_codex/skills/save-to-dotfiles/symlink_SKILL.md |
~/.codex/skills/save-to-dotfiles/SKILL.md |
file-level symlink | → ~/.claude/skills/save-to-dotfiles/SKILL.md (single source for the only chezmoi-managed skill shared between both AI tools) |
We deliberately don't manage ~/.claude/commands/ (deprecated by skills per upstream docs) or ~/.claude/hooks/ (not a Claude directory convention — hooks live inline in settings.json).
Both files are mutated at runtime by tools:
~/.claude/settings.json—rtk init -gadds aPreToolUsehook entry;claude plugin installadds entries toenabledPlugins~/.codex/config.toml— Codex auto-writes[projects."<path>"],[notice],[tui.*],[tool_suggest]runtime sections
The modify_ pattern handles both gracefully:
- settings.json: jq additive merge between
.chezmoitemplates/claude-settings-base.json(our curated base) and the existing file. Our base sets canonical permissions/statusLine/etc.; tool-added keys (hooks.PreToolUse, extraenabledPlugins) get UNION-preserved. - config.toml: chezmoi-native template via
fromToml/toToml. Parse existing → drop runtime sections →mergeOverwritewith.chezmoitemplates/codex-config-base.toml(our curated base) → serialize. Idempotent.
Both bases live in .chezmoitemplates/ — chezmoi's canonical location for shared template partials, automatically excluded from $HOME. They're loaded via includeTemplate "name" . from the respective modify_ scripts.
This replaces a previous git-clean-filter mechanism (.gitattributes + per-clone git config filter.codex-strip.{clean,smudge,required}) — that was always-on for the entire source repo, fragile across clones, and required required=true + smudge=cat setup. modify_ is local to one file, no per-clone setup.
Files under dot_claude/rules/*.md are auto-discovered by Claude Code at session start. Each rule file is a small markdown doc with YAML frontmatter:
---
name: my-rule
description: One-line summary surfaced in the rule index.
paths: # OPTIONAL — conditional loading
- "**/*.tsx"
- "**/*.css"
---- Universal rules (no
paths:) load on every session. - Conditional rules (with
paths:glob array) only load when Claude is reading/editing files matching one of the globs.
Current set: three universal (read-codebase-first, no-code-without-go, verify-before-fix), two frontend-only (frontend-spec-first-workflow, visual-audit-mcp-gotchas). Codex doesn't have an auto-discovery equivalent — dot_codex/AGENTS.md references the rule files as advisory reading.
~/.claude/settings.local.json— per-machine permissions allowlist; Anthropic's documented convention is to gitignore it.~/.claude/plugins/{installed_plugins,known_marketplaces}.json— contain absolute install paths and per-user marketplace registrations.~/.claude.json— lives at$HOME, not in~/.claude/— holds OAuth tokens, MCP server credentials, NUXtipsHistorycounters, marketplace registrations. Never commit it.~/.claude/RTK.md,~/.codex/RTK.md— written fresh byrtk init -g/rtk init -g --codex(bundled rtk version's content, not ours to track).~/.codex/auth.json,installation_id— secrets and per-machine identifiers.~/.codex/{sessions,cache,log,tmp,history.jsonl,*.sqlite}— runtime state.~/.codex/memories/,~/.codex/plugins/— runtime state / cloned plugin repos.
Plugins listed in .chezmoidata/packages.yaml under plugins.{claude,codex} are installed automatically by .chezmoiscripts/run_onchange_after_70-install-plugins.sh.tmpl. The same YAML drives the enable flags in .chezmoitemplates/{claude-settings-base.json,codex-config-base.toml} — single source of truth, no drift between install and enable state.
Mechanics:
- Claude:
claude plugin install <plugin>@<marketplace> --scope user(idempotent), thenclaude plugin update <plugin>(auto-upgrade). Theclaude-plugins-officialmarketplace is auto-added defensively (it's the implicit default but Claude only auto-registers it on first interactive launch). Non-default marketplaces (listed underplugins.claude_marketplacesinpackages.yaml) are registered viaclaude plugin marketplace addbefore the install loop. - Codex: built-in
openai-curatedmarketplace is reserved — only the[plugins."x@y"] enabled = truetable inconfig.tomlis needed (generated from the YAML). Non-reserved marketplaces (listed underplugins.codex_marketplaces) are registered viacodex plugin marketplace add.
caveman is just another entry in plugins.claude (caveman@caveman) + plugins.claude_marketplaces (caveman:JuliusBrussee/caveman). Per its INSTALL.md per-agent table, that's the canonical Claude install path. The plugin self-registers its SessionStart + UserPromptSubmit hooks via plugin.json (no settings.json mutation). Caveman for Codex is a manual step (npx -y skills add JuliusBrussee/caveman -a codex) — the skills CLI doesn't currently create the per-agent symlinks Codex needs.
rtk is installed by mise as github:rtk-ai/rtk = "latest" — cross-platform, single source. On every chezmoi apply, mise refreshes its registry and pulls a newer rtk if upstream released one. The post_install_rtk_init function in lib/install-packages.sh runs rtk init -g --auto-patch (Claude PreToolUse hook) and rtk init -g --codex (Codex AGENTS.md reference) — both idempotent. RTK.md awareness files (~/.claude/RTK.md, ~/.codex/RTK.md) are bundled inside the binary and rewritten on each init.
Ubuntu 22.04 jammy (glibc 2.35) skips the init step gracefully — upstream ships glibc-2.39 builds only for linux-arm64 (Ubuntu 24.04+); claude/codex still work without the rtk hook.
code <path> works the same in any iTerm2 pane. Calling code without arguments opens the *.code-workspace file in cwd if one exists, otherwise opens cwd itself.
- Local Mac: opens VSCode at that path. Works after Brewfile installs the cask + zshrc adds it to PATH.
- Remote SSH (iTerm2/tmux into a Linux box): a zshrc function (active when
$SSH_CONNECTIONis set) decides what to do:- If you're already in VSCode's integrated Remote-SSH terminal — calls the real
codevia the injected env. - Else prints a
vscode://vscode-remote/ssh-remote+<host>/<path>URL. The iTerm2 Trigger matches the URL and runsopen <url>→ macOS launches Mac-side VSCode → it connects via Remote-SSH and opens the folder.
- If you're already in VSCode's integrated Remote-SSH terminal — calls the real
If the remote's hostname -s doesn't match the Host alias in your local ~/.ssh/config, override per-host:
export VSCODE_REMOTE_HOST=my-ssh-aliasTop-level dotfiles (dot_zshrc, dot_vimrc, etc.) are plain — they handle platform differences via runtime feature-detection (command -v X, case "$(uname -s)" in shell). Where chezmoi templates are used:
.chezmoiexternal.toml.tmplis templated for headless detection (skip fonts) + per-OS font destination.chezmoiscripts/{darwin,linux}/hold per-OS scripts; root holds cross-platform scripts with numerical prefix (50-,70-,99-) for ordering within therun_onchange_after_/run_once_after_groups.chezmoiscripts/run_onchange_after_50-install-packages.sh.tmplbranches on.chezmoi.os(Mac brew bundle vs Linux apt/dnf) +.chezmoi.osRelease.id(Debian vs Fedora).chezmoihooks/ensure-prereqs.shis NOT a template (chezmoi doesn't template hooks) — it detects OS inline viacase "$(uname -s)"
Used by Linux font install (which is skipped on remote machines):
[ -n "$SSH_CONNECTION" ] && [ -z "$DISPLAY" ] && [ -z "$WAYLAND_DISPLAY" ]Glyphs render on the local terminal emulator — remote box doesn't need the font.
tmux/oh-my-tmux is pulled by chezmoi from .chezmoiexternal.toml.tmpl:
[".config/tmux"]
type = "archive"
url = "https://github.com/gpakosz/.tmux/archive/<commit-sha>.tar.gz"
exact = true
stripComponents = 1
refreshPeriod = "720h"Pinning is via commit SHA in the URL (gpakosz/.tmux has no tagged releases). To bump: pick newer SHA from gpakosz commits, edit URL, chezmoi apply -R, commit.
Cache lives in ~/Library/Caches/chezmoi/ (Mac) — outside the source repo, so git status stays clean.
- mise-as-primary-installer migration (2026-05) — replaced bundle-based install (1200 LOC) with mise.toml declarative tool list + slim OS-glue install (~250 LOC). Cross-platform parity: same 28 dev tools install identically on Mac + Linux via mise's aqua backend.
- Linux bundle support — superseded by mise migration. Bundle scripts
- helpers all deleted.
dot_ssh/config.tmpl: 5 portable lines (AddKeysToAgent,UseKeychain,HashKnownHosts,ServerAliveInterval/CountMax). Per-host blocks stay in untracked~/.ssh/config.d/*.conf.dot_kube/config.tmpl: kubectl config + krew. Per-cluster credentials machine-local.dot_aws/config.tmpl: AWS CLI v2 named profiles + SSO sessions. Credentials machine-local.
- Renovate config for
dot_config/mise/config.toml— auto-PR bumps for pinned tool versions (replaces "track latest" with deterministic upgrades). - CI smoke tests (Docker matrix over Ubuntu 22.04 + Debian 12 arm64).
dot_p10k.zshtop-of-file generation note.