From cdd292fedecc21bbf064f55b7d07ed9e4ddfa512 Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Tue, 14 Apr 2026 12:48:32 +0700 Subject: [PATCH 01/10] feat(secrets): auto-load CLOUDFLARE_API_TOKEN from 1Password Inject the R2 API credential into every interactive fish shell at chezmoi apply time so skills and scripts that need Cloudflare access pick it up automatically. Co-Authored-By: Claude Opus 4.6 (1M context) --- home/dot_config/fish/conf.d/secrets.fish.tmpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/home/dot_config/fish/conf.d/secrets.fish.tmpl b/home/dot_config/fish/conf.d/secrets.fish.tmpl index 11f78fc..b9a608b 100644 --- a/home/dot_config/fish/conf.d/secrets.fish.tmpl +++ b/home/dot_config/fish/conf.d/secrets.fish.tmpl @@ -23,6 +23,8 @@ if status is-interactive # set -gx OPENAI_API_KEY "{{"{{"}} onepasswordRead (printf "op://%s/OpenAI/credential" .op_vault) {{"}}"}}" # # The onepasswordRead call runs at `chezmoi apply` time and injects the real value. + + set -gx CLOUDFLARE_API_TOKEN "{{ onepasswordRead (printf "op://%s/Cloudflare R2/credential" .op_vault) }}" {{ else }} # 1Password not configured. Enable: chezmoi init (answer "yes" to 1Password) {{ end }} From f4515ec6f47b8ad52e1315f1a15d8afe95d4c386 Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Tue, 14 Apr 2026 12:55:28 +0700 Subject: [PATCH 02/10] feat(secrets): data-driven add-secret workflow Replace hand-edited template entries with a single registry in .chezmoidata/secrets.toml. The template iterates the registry and emits one onepasswordRead per entry at apply time. Adds three fish helpers: - add-secret VAR "op://..." validates via op read, appends, applies, optionally commits - rm-secret VAR reverses the above - list-secrets dumps current bindings Updates README "Adding secrets" and CLAUDE.md secret-injection note to point at the new workflow. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 7 ++- README.md | 23 ++++++-- home/.chezmoidata/secrets.toml | 5 ++ home/dot_config/fish/conf.d/secrets.fish.tmpl | 26 ++++---- .../dot_config/fish/functions/add-secret.fish | 59 +++++++++++++++++++ .../fish/functions/list-secrets.fish | 8 +++ home/dot_config/fish/functions/rm-secret.fish | 30 ++++++++++ 7 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 home/.chezmoidata/secrets.toml create mode 100644 home/dot_config/fish/functions/add-secret.fish create mode 100644 home/dot_config/fish/functions/list-secrets.fish create mode 100644 home/dot_config/fish/functions/rm-secret.fish diff --git a/CLAUDE.md b/CLAUDE.md index ad16a8a..4208ed6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,12 @@ chezmoi uses filename prefixes to encode target attributes: ``` {{ onepasswordRead (printf "op://%s/ItemName/credential" .op_vault) }} ``` -Used in: `secrets.fish.tmpl`, `dot_gitconfig.tmpl`, `dot_config/zed/settings.json.tmpl` +Used directly in: `dot_gitconfig.tmpl`, `dot_config/zed/settings.json.tmpl`. + +For auto-loaded shell env vars, prefer the data-driven workflow: register +entries in `.chezmoidata/secrets.toml` via `add-secret VAR op://...` and let +`secrets.fish.tmpl` iterate. Do not hand-edit the template to add new env +vars — use `add-secret` / `rm-secret` / `list-secrets`. **macOS Keychain** — via `keyring` template function or runtime fish functions: ``` diff --git a/README.md b/README.md index 1cbeba2..1512a3c 100644 --- a/README.md +++ b/README.md @@ -197,13 +197,24 @@ chezmoi apply --refresh-externals Secrets are injected at `chezmoi apply` time and never stored in git. -**With 1Password** (recommended): -```bash -# Store the secret -op item create --vault=Developer --category=api_credential --title="OpenAI" password="sk-..." +**With 1Password — one-command workflow** (recommended): +```fish +# 1. Store the secret in 1Password (any vault) +op item create --vault=Private --category="API Credential" \ + --title="OpenAI" credential="sk-..." + +# 2. Register it as an auto-loaded env var +add-secret OPENAI_API_KEY "op://Private/OpenAI/credential" +``` + +`add-secret` validates the ref with `op read`, appends it to +`.chezmoidata/secrets.toml`, runs `chezmoi apply`, and (with `--commit`) +creates the git commit for you. Every new fish shell then has +`$OPENAI_API_KEY` set. -# Reference it in a template (e.g., secrets.fish.tmpl) -set -gx OPENAI_API_KEY "{{ onepasswordRead "op://Developer/OpenAI/password" }}" +```fish +list-secrets # show current bindings +rm-secret OPENAI_API_KEY # unregister (optional --commit) ``` **With macOS Keychain:** diff --git a/home/.chezmoidata/secrets.toml b/home/.chezmoidata/secrets.toml new file mode 100644 index 0000000..c550423 --- /dev/null +++ b/home/.chezmoidata/secrets.toml @@ -0,0 +1,5 @@ +[secrets] +# Key = "op://vault/item/field" — one line per secret. +# Managed by the `add-secret` / `rm-secret` fish functions. +# Values are 1Password references, not secrets, so this file is safe to commit. +CLOUDFLARE_API_TOKEN = "op://Private/Cloudflare R2/credential" diff --git a/home/dot_config/fish/conf.d/secrets.fish.tmpl b/home/dot_config/fish/conf.d/secrets.fish.tmpl index b9a608b..e5585fb 100644 --- a/home/dot_config/fish/conf.d/secrets.fish.tmpl +++ b/home/dot_config/fish/conf.d/secrets.fish.tmpl @@ -3,28 +3,22 @@ {{- fail (printf "\n\n%s[38;5;204m ✗ Missing config variable: use_1password%s[0m\n Run: %s[38;5;78mchezmoi init%s[0m\n" $e $e $e $e) -}} {{- end -}} # secrets.fish — Secret loading for fish shell. -# Edit: chezmoi edit ~/.config/fish/conf.d/secrets.fish -# Apply: chezmoi apply # -# Load secrets on demand via fish functions: -# op-env GITHUB_TOKEN "op://Vault/GitHub Token/password" -# keychain-env MY_TOKEN [service-name] -# web3-env [vault-name] +# Auto-loaded secrets live in .chezmoidata/secrets.toml. Use the fish helpers +# to manage them — never hand-edit the template: # -# For apply-time injection, uncomment and edit lines below. +# add-secret VAR "op://Vault/Item/field" # append and apply +# rm-secret VAR # remove and apply +# list-secrets # show current bindings +# +# For one-off loads without persistence: op-env / keychain-env. if status is-interactive {{ if .use_1password }} # ── 1Password (resolved at `chezmoi apply` time) ───────────────── - # To inject secrets at apply time, create the item in 1Password first: - # op item create --vault="{{ .op_vault }}" --category=api_credential --title="OpenAI" password=sk-... - # - # Then add lines like this (replace ITEM and FIELD with your 1Password item): - # set -gx OPENAI_API_KEY "{{"{{"}} onepasswordRead (printf "op://%s/OpenAI/credential" .op_vault) {{"}}"}}" - # - # The onepasswordRead call runs at `chezmoi apply` time and injects the real value. - - set -gx CLOUDFLARE_API_TOKEN "{{ onepasswordRead (printf "op://%s/Cloudflare R2/credential" .op_vault) }}" +{{- range $var, $ref := .secrets }} + set -gx {{ $var }} "{{ onepasswordRead $ref }}" +{{- end }} {{ else }} # 1Password not configured. Enable: chezmoi init (answer "yes" to 1Password) {{ end }} diff --git a/home/dot_config/fish/functions/add-secret.fish b/home/dot_config/fish/functions/add-secret.fish new file mode 100644 index 0000000..80ceae1 --- /dev/null +++ b/home/dot_config/fish/functions/add-secret.fish @@ -0,0 +1,59 @@ +function add-secret --description "Register a 1Password secret as an auto-loaded env var" + if test (count $argv) -lt 2 + echo "Usage: add-secret VAR_NAME \"op://Vault/Item/field\" [--commit]" + echo "Example: add-secret OPENAI_API_KEY \"op://Private/OpenAI/credential\"" + return 1 + end + + set -l var $argv[1] + set -l ref $argv[2] + set -l do_commit 0 + contains -- --commit $argv; and set do_commit 1 + + if not string match -qr '^[A-Z_][A-Z0-9_]*$' -- $var + echo "✗ VAR_NAME must be UPPER_SNAKE_CASE, got: $var" + return 1 + end + + if not string match -q 'op://*' -- $ref + echo "✗ reference must start with op:// , got: $ref" + return 1 + end + + if not op read "$ref" >/dev/null 2>&1 + echo "✗ op read failed — sign in with `eval (op signin)` or check the ref" + return 1 + end + + set -l data (chezmoi source-path)/.chezmoidata/secrets.toml + if test ! -f $data + echo "✗ $data missing; dotfiles may need reinstall" + return 1 + end + + if grep -q "^$var = " $data + echo "⚠ $var already registered; use rm-secret first to replace" + return 1 + end + + printf '%s = "%s"\n' "$var" "$ref" >> $data + echo "✓ added $var → $ref" + + echo "→ chezmoi apply" + chezmoi apply; or begin + echo "✗ chezmoi apply failed; reverting data file" + sed -i '' "/^$var = /d" $data + return 1 + end + + echo "✓ applied. Open a new shell (or `exec fish`) to load \$$var." + + if test $do_commit -eq 1 + set -l repo (dirname (chezmoi source-path)) + git -C $repo add .chezmoidata/secrets.toml + git -C $repo commit -m "feat(secrets): register $var" >/dev/null + echo "✓ committed. Push with: git -C "(dirname (chezmoi source-path))" push" + else + echo "Commit: git -C "(dirname (chezmoi source-path))" add .chezmoidata/secrets.toml && git commit -m 'feat(secrets): register $var'" + end +end diff --git a/home/dot_config/fish/functions/list-secrets.fish b/home/dot_config/fish/functions/list-secrets.fish new file mode 100644 index 0000000..3264fc0 --- /dev/null +++ b/home/dot_config/fish/functions/list-secrets.fish @@ -0,0 +1,8 @@ +function list-secrets --description "List auto-loaded 1Password secret bindings" + set -l data (chezmoi source-path)/.chezmoidata/secrets.toml + if test ! -f $data + echo "(no secrets registered)" + return 0 + end + grep -E '^[A-Z_][A-Z0-9_]* = ' $data | sed 's/ = / → /; s/"//g' +end diff --git a/home/dot_config/fish/functions/rm-secret.fish b/home/dot_config/fish/functions/rm-secret.fish new file mode 100644 index 0000000..f2f1c6a --- /dev/null +++ b/home/dot_config/fish/functions/rm-secret.fish @@ -0,0 +1,30 @@ +function rm-secret --description "Unregister an auto-loaded 1Password secret" + if test (count $argv) -lt 1 + echo "Usage: rm-secret VAR_NAME [--commit]" + return 1 + end + + set -l var $argv[1] + set -l do_commit 0 + contains -- --commit $argv; and set do_commit 1 + + set -l data (chezmoi source-path)/.chezmoidata/secrets.toml + if not grep -q "^$var = " $data + echo "⚠ $var not registered" + return 1 + end + + sed -i '' "/^$var = /d" $data + echo "✓ removed $var" + + echo "→ chezmoi apply" + chezmoi apply; or return 1 + echo "✓ applied. \$$var will be absent from new shells." + + if test $do_commit -eq 1 + set -l repo (dirname (chezmoi source-path)) + git -C $repo add .chezmoidata/secrets.toml + git -C $repo commit -m "chore(secrets): unregister $var" >/dev/null + echo "✓ committed." + end +end From 8bac06a45b933779952638097af7bee28a84c9ba Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Tue, 14 Apr 2026 13:00:00 +0700 Subject: [PATCH 03/10] feat(secrets): auto-create 1Password item in add-secret When the op:// reference doesn't resolve, prompt for the value (hidden input) and run `op item create` before registering. Reduces the flow for a brand-new secret to a single command. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 20 ++++----- .../dot_config/fish/functions/add-secret.fish | 44 +++++++++++++++---- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 1512a3c..3cf0b0e 100644 --- a/README.md +++ b/README.md @@ -199,18 +199,18 @@ Secrets are injected at `chezmoi apply` time and never stored in git. **With 1Password — one-command workflow** (recommended): ```fish -# 1. Store the secret in 1Password (any vault) -op item create --vault=Private --category="API Credential" \ - --title="OpenAI" credential="sk-..." - -# 2. Register it as an auto-loaded env var -add-secret OPENAI_API_KEY "op://Private/OpenAI/credential" +add-secret OPENAI_API_KEY "op://Private/OpenAI/credential" --commit ``` -`add-secret` validates the ref with `op read`, appends it to -`.chezmoidata/secrets.toml`, runs `chezmoi apply`, and (with `--commit`) -creates the git commit for you. Every new fish shell then has -`$OPENAI_API_KEY` set. +That's it. `add-secret` will: +1. Check the 1Password item at `op://Private/OpenAI/credential`. If it + doesn't exist, prompt you (hidden input) for the value and create the + item automatically. +2. Append the binding to `.chezmoidata/secrets.toml`. +3. Run `chezmoi apply` so the rendered `secrets.fish` includes it. +4. With `--commit`, stage the registry file and create the git commit. + +Every new fish shell then has `$OPENAI_API_KEY` set. ```fish list-secrets # show current bindings diff --git a/home/dot_config/fish/functions/add-secret.fish b/home/dot_config/fish/functions/add-secret.fish index 80ceae1..853d3e9 100644 --- a/home/dot_config/fish/functions/add-secret.fish +++ b/home/dot_config/fish/functions/add-secret.fish @@ -2,6 +2,9 @@ function add-secret --description "Register a 1Password secret as an auto-loaded if test (count $argv) -lt 2 echo "Usage: add-secret VAR_NAME \"op://Vault/Item/field\" [--commit]" echo "Example: add-secret OPENAI_API_KEY \"op://Private/OpenAI/credential\"" + echo "" + echo "If the 1Password item doesn't exist yet you'll be prompted for the" + echo "value and the item will be created automatically." return 1 end @@ -15,14 +18,39 @@ function add-secret --description "Register a 1Password secret as an auto-loaded return 1 end - if not string match -q 'op://*' -- $ref - echo "✗ reference must start with op:// , got: $ref" + # Parse op://Vault/Item/field — the Item segment may contain spaces. + set -l parts (string split / -- (string replace 'op://' '' -- $ref)) + if test (count $parts) -lt 3 + echo "✗ reference must be op://Vault/Item/field , got: $ref" return 1 end + set -l op_vault $parts[1] + set -l op_field $parts[-1] + set -l op_item (string join / $parts[2..-2]) + # Create the item if it doesn't exist yet. if not op read "$ref" >/dev/null 2>&1 - echo "✗ op read failed — sign in with `eval (op signin)` or check the ref" - return 1 + echo "No item found at $ref — creating it now." + if not op account list >/dev/null 2>&1 + echo "✗ 1Password CLI is not signed in. Run: eval (op signin)" + return 1 + end + read -s -P "Enter value for $var: " value + echo "" + if test -z "$value" + echo "✗ empty value, aborting" + return 1 + end + if not op item create --vault="$op_vault" --category="API Credential" \ + --title="$op_item" "$op_field=$value" >/dev/null 2>&1 + echo "✗ op item create failed (vault=$op_vault title=$op_item)" + return 1 + end + echo "✓ created 1Password item: $op_item" + if not op read "$ref" >/dev/null 2>&1 + echo "✗ item created but $ref still unreadable; check field name" + return 1 + end end set -l data (chezmoi source-path)/.chezmoidata/secrets.toml @@ -41,19 +69,19 @@ function add-secret --description "Register a 1Password secret as an auto-loaded echo "→ chezmoi apply" chezmoi apply; or begin - echo "✗ chezmoi apply failed; reverting data file" + echo "✗ chezmoi apply failed; reverting registry" sed -i '' "/^$var = /d" $data return 1 end echo "✓ applied. Open a new shell (or `exec fish`) to load \$$var." + set -l repo (dirname (chezmoi source-path)) if test $do_commit -eq 1 - set -l repo (dirname (chezmoi source-path)) git -C $repo add .chezmoidata/secrets.toml git -C $repo commit -m "feat(secrets): register $var" >/dev/null - echo "✓ committed. Push with: git -C "(dirname (chezmoi source-path))" push" + echo "✓ committed. Push with: git -C $repo push" else - echo "Commit: git -C "(dirname (chezmoi source-path))" add .chezmoidata/secrets.toml && git commit -m 'feat(secrets): register $var'" + echo "Commit: git -C $repo add .chezmoidata/secrets.toml && git commit -m 'feat(secrets): register $var'" end end From 44e44667f7e7741c57ba97b84f4296b3da7de8d2 Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Tue, 14 Apr 2026 13:05:19 +0700 Subject: [PATCH 04/10] docs: add customization guide and dfe edit-and-apply helper Introduce docs/customization.md: the universal edit-source -> chezmoi apply -> reload flow, a quick-reference table for every common change (Brewfile, fish, Starship, Ghostty, Zed, Claude Code, SSH, macOS defaults), the secrets workflow with a Mermaid flow diagram, and troubleshooting for common drift. Ship `dfe PATH` fish function wrapping `chezmoi edit --apply` so editing a managed file and reapplying is one command. Shrink the README's "Adding secrets" block to a pointer, and nudge CLAUDE.md to send users at the new guide. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 2 + README.md | 38 ++---- docs/customization.md | 147 ++++++++++++++++++++++++ home/dot_config/fish/functions/dfe.fish | 10 ++ 4 files changed, 170 insertions(+), 27 deletions(-) create mode 100644 docs/customization.md create mode 100644 home/dot_config/fish/functions/dfe.fish diff --git a/CLAUDE.md b/CLAUDE.md index 4208ed6..772b67b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co A chezmoi-managed dotfiles repo for macOS (Apple Silicon). The `home/` directory is the chezmoi source state — it maps to `$HOME` on the target machine. The `install.sh` script bootstraps a fresh Mac from zero. +User-facing customization flows (how to change Brewfile, secrets, editors, etc.) live in [`docs/customization.md`](docs/customization.md). When a user asks "how do I change X", point them there rather than reinventing. + ## Key commands ```bash diff --git a/README.md b/README.md index 3cf0b0e..4913b1a 100644 --- a/README.md +++ b/README.md @@ -193,42 +193,26 @@ chezmoi apply --refresh-externals | `home/.chezmoiscripts/run_once_after_macos-defaults.sh.tmpl` | macOS system preferences | | `home/.chezmoiexternal.toml` | Fish plugins to auto-download | -### Adding secrets +### Customizing and adding secrets -Secrets are injected at `chezmoi apply` time and never stored in git. +The universal flow: **edit source → `chezmoi apply` → new shell/reload**. +The `dfe` fish helper collapses edit + apply into one command: -**With 1Password — one-command workflow** (recommended): ```fish -add-secret OPENAI_API_KEY "op://Private/OpenAI/credential" --commit +dfe ~/.Brewfile # edit + auto-apply on save +dfe ~/.config/fish/config.fish ``` -That's it. `add-secret` will: -1. Check the 1Password item at `op://Private/OpenAI/credential`. If it - doesn't exist, prompt you (hidden input) for the value and create the - item automatically. -2. Append the binding to `.chezmoidata/secrets.toml`. -3. Run `chezmoi apply` so the rendered `secrets.fish` includes it. -4. With `--commit`, stage the registry file and create the git commit. - -Every new fish shell then has `$OPENAI_API_KEY` set. - -```fish -list-secrets # show current bindings -rm-secret OPENAI_API_KEY # unregister (optional --commit) -``` +For 1Password secrets, one command creates the item (if missing), +registers it, applies, and commits: -**With macOS Keychain:** ```fish -keychain-set MY_TOKEN "secret-value" # store -keychain-env MY_TOKEN # load into current shell +add-secret OPENAI_API_KEY "op://Private/OpenAI/credential" --commit ``` -**On-demand loading (no apply needed):** -```fish -op-env GITHUB_TOKEN "op://Vault/GitHub Token/password" # 1Password -keychain-env MY_TOKEN # Keychain -web3-env # ETH_RPC_URL + Etherscan -``` +Full reference — quick-change table for every common file, the secrets +workflow diagram, and troubleshooting — lives in +[docs/customization.md](docs/customization.md).
Encrypted files (age) diff --git a/docs/customization.md b/docs/customization.md new file mode 100644 index 0000000..574c77e --- /dev/null +++ b/docs/customization.md @@ -0,0 +1,147 @@ +# Customization guide + +How to change anything in this dotfiles repo without going back and +forth between the source directory and your machine. + +## The one rule + +``` +edit source → chezmoi apply → effect on your machine +``` + +Every customization follows this shape. The source lives in the repo at +`home/` and gets linked to `~/.local/share/chezmoi`. When you run +`chezmoi apply`, chezmoi renders any `.tmpl` files, then deploys the +result to `$HOME`. + +```mermaid +flowchart LR + A[you edit source file
in ~/.local/share/chezmoi/...] -->|chezmoi apply| B[chezmoi renders
.tmpl files] + B --> C[deploys to $HOME
~/.config/fish/config.fish etc.] + C --> D[new shell /
reload app] +``` + +### The shortcut + +Typing `chezmoi edit X --apply` does both steps in one command. This +repo ships a fish wrapper: + +```fish +dfe ~/.config/fish/config.fish +``` + +`dfe` opens the **source** file in your `$EDITOR`; when you save and +quit, chezmoi applies immediately. No separate `chezmoi apply` call. + +Other useful shortcuts you can alias yourself or type directly: + +| What | Command | +|---|---| +| Apply pending changes | `chezmoi apply` | +| See what would change | `chezmoi diff` | +| Edit without applying | `chezmoi edit ` | +| Jump to the source dir | `chezmoi cd` | +| List everything managed | `chezmoi managed` | + +## Quick reference — common customizations + +| Change | File | Command | +|---|---|---| +| Homebrew packages | `home/dot_Brewfile.tmpl` | `dfe ~/.Brewfile` (auto-runs `brew bundle`) | +| Fish abbrs / env | `home/dot_config/fish/config.fish.tmpl` | `dfe ~/.config/fish/config.fish` | +| Fish function | `home/dot_config/fish/functions/NAME.fish` | `dfe ~/.config/fish/functions/NAME.fish` | +| Starship prompt | `home/dot_config/starship.toml` | `dfe ~/.config/starship.toml` | +| Ghostty config | `home/dot_config/ghostty/config` | `dfe ~/.config/ghostty/config` (live reload) | +| Ghostty theme | any time | `ghostty-theme N` — live switch | +| VS Code settings | `home/dot_config/code/settings.json` | `dfe ~/.config/code/settings.json` | +| VS Code extensions | `home/dot_config/code/extensions.txt` | edit + `chezmoi apply` | +| Zed settings + MCP | `home/dot_config/zed/settings.json.tmpl` | `dfe ~/.config/zed/settings.json` | +| Git config | `home/dot_gitconfig.tmpl` | `dfe ~/.gitconfig` | +| SSH hosts | `home/dot_ssh/config.d/*` | drop file + `chezmoi apply` | +| macOS defaults | `home/.chezmoiscripts/run_once_after_macos-defaults.sh.tmpl` | edit + `chezmoi apply` (re-runs on content change) | +| Claude Code config | `home/dot_claude/` | `dfe ~/.claude/settings.json` | +| 1Password secrets | `home/.chezmoidata/secrets.toml` | `add-secret VAR op://...` — see below | +| Fish plugins | `home/.chezmoiexternal.toml` | edit + `chezmoi apply --refresh-externals` | +| Setup wizard answers | `~/.config/chezmoi/chezmoi.toml` | `chezmoi init` to re-prompt | + +## Secrets workflow + +Secrets are handled differently because the value lives in 1Password, +not in the repo. Three tiers, pick per secret: + +| Tier | When | Command | +|---|---|---| +| **Auto-loaded** | env var you want in every shell | `add-secret VAR "op://..."` | +| **Runtime** | occasional CLI use, don't want in every env | `op-env VAR "op://..."` | +| **One-off** | inline trial | `set -x VAR (op read "op://...")` | + +### The `add-secret` flow + +```mermaid +flowchart TD + A[add-secret VAR op://Vault/Item/field --commit] --> B{op read
succeeds?} + B -- no --> C[prompt for value
hidden input] --> D[op item create
in 1Password] + D --> E + B -- yes --> E[append VAR=ref to
.chezmoidata/secrets.toml] + E --> F[chezmoi apply
renders secrets.fish] + F --> G[git commit --commit flag] + G --> H[user opens new shell
$VAR is set] +``` + +Example: +```fish +add-secret OPENAI_API_KEY "op://Private/OpenAI/credential" --commit +exec fish # or open a new terminal — now $OPENAI_API_KEY is set +``` + +Companions: +```fish +list-secrets # show current bindings +rm-secret OPENAI_API_KEY # unregister (optional --commit) +``` + +### How it works under the hood + +`home/.chezmoidata/secrets.toml` is a registry chezmoi loads automatically +as the `.secrets` template variable. `home/dot_config/fish/conf.d/secrets.fish.tmpl` +iterates `.secrets` at apply time and emits one `set -gx` per entry, +resolving each `op://` ref via `onepasswordRead`. The rendered +`~/.config/fish/conf.d/secrets.fish` contains real tokens — it never +leaves your machine. + +### Rotating a token + +Just update the value in 1Password. The next `chezmoi apply` re-renders +the fish file with the new value. To force immediately: + +```fish +chezmoi apply ~/.config/fish/conf.d/secrets.fish +exec fish +``` + +## Troubleshooting + +- **"I changed the source but nothing happened"** — you probably edited + `~/.config/...` directly. Edit the source at + `~/.local/share/chezmoi/...` (or use `dfe`). `chezmoi diff` will show + if your change drifted. +- **"chezmoi apply wants to prompt me"** — you modified a deployed file + outside chezmoi. Either accept the overwrite, or `chezmoi merge PATH` + to bring the change back into the source. +- **"op read failed"** — run `eval (op signin)` once per shell session. +- **"Template error at apply"** — a `.tmpl` file is missing a variable. + Run `chezmoi init` to refresh the answer cache. +- **"Brewfile didn't re-run"** — `run_onchange_before_brew-bundle.sh.tmpl` + only fires when the file content changes. Force it with + `rm ~/.config/chezmoi/state/chezmoistate.boltdb` (nuclear) or just + `brew bundle --file=~/.Brewfile`. + +## Key design choices + +- **No hand-edits to `secrets.fish.tmpl`** for new env vars — use the + registry so the template stays small and readers see one source of + truth. +- **`dfe` over `chezmoi edit` + `chezmoi apply`** — less to remember, + fewer forgotten applies. +- **1Password references, not values, are in git** — rotation doesn't + touch the repo. diff --git a/home/dot_config/fish/functions/dfe.fish b/home/dot_config/fish/functions/dfe.fish new file mode 100644 index 0000000..2018e5d --- /dev/null +++ b/home/dot_config/fish/functions/dfe.fish @@ -0,0 +1,10 @@ +function dfe --description "Edit a managed dotfile and auto-apply on save" + if test (count $argv) -lt 1 + echo "Usage: dfe " + echo "Examples:" + echo " dfe ~/.config/fish/config.fish" + echo " dfe ~/.Brewfile" + return 1 + end + chezmoi edit --apply $argv +end From b60bfd993e83a6a027547afe72f78e68c02e918e Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Tue, 14 Apr 2026 13:12:23 +0700 Subject: [PATCH 05/10] feat(workflow): auto-commit on dfe, add dfs for reverse drift Codify the core requirement: every setting change must be both applied to the machine AND backed up in the dotfiles repo in one step. - dfe now commits after apply by default; --no-commit opts out. - New dfs: detects drifted deployed files via `chezmoi status`, prompts, runs `chezmoi re-add`, and commits the re-absorbed source. - add-secret and rm-secret flip to auto-commit by default; --no-commit opts out (renamed from --commit opt-in). - docs/customization.md gains a "Core requirement" section that pins the principle for future reference, and documents dfs + the new default commit behavior. Pushing remains manual; each helper prints the exact git push command. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 +- docs/customization.md | 48 ++++++++++++++++--- .../dot_config/fish/functions/add-secret.fish | 6 +-- home/dot_config/fish/functions/dfe.fish | 40 +++++++++++++--- home/dot_config/fish/functions/dfs.fish | 48 +++++++++++++++++++ home/dot_config/fish/functions/rm-secret.fish | 6 +-- 6 files changed, 130 insertions(+), 20 deletions(-) create mode 100644 home/dot_config/fish/functions/dfs.fish diff --git a/README.md b/README.md index 4913b1a..72359a0 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ For 1Password secrets, one command creates the item (if missing), registers it, applies, and commits: ```fish -add-secret OPENAI_API_KEY "op://Private/OpenAI/credential" --commit +add-secret OPENAI_API_KEY "op://Private/OpenAI/credential" ``` Full reference — quick-change table for every common file, the secrets diff --git a/docs/customization.md b/docs/customization.md index 574c77e..b259f87 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -3,6 +3,19 @@ How to change anything in this dotfiles repo without going back and forth between the source directory and your machine. +## Core requirement + +> Every change to a setting must be **applied to the machine** AND +> **backed up in the dotfiles repo** in one step. The user should never +> have to remember to re-run `chezmoi apply` or `git commit` separately. + +All helpers in this guide (`dfe`, `dfs`, `add-secret`, `rm-secret`) are +designed to satisfy this rule by default. Opt out with `--no-commit` if +you need to stage a change but not commit it. + +Pushing is still manual. Run `git -C (dirname (chezmoi source-path)) push` +when you're ready to share the commits with other machines. + ## The one rule ``` @@ -23,15 +36,33 @@ flowchart LR ### The shortcut -Typing `chezmoi edit X --apply` does both steps in one command. This -repo ships a fish wrapper: +The `dfe` fish helper edits the **source**, applies on save, and +commits the change in one shot: + +```fish +dfe ~/.config/fish/config.fish # edit + apply + commit +dfe ~/.config/fish/config.fish --no-commit # edit + apply only +``` + +Pushing is still on you. When there are commits to share: ```fish -dfe ~/.config/fish/config.fish +git -C (dirname (chezmoi source-path)) push ``` -`dfe` opens the **source** file in your `$EDITOR`; when you save and -quit, chezmoi applies immediately. No separate `chezmoi apply` call. +### Reverse drift — `dfs` + +If you edited a deployed file directly (bypassing `dfe`) and want the +change captured back into the source: + +```fish +dfs # diff, prompt, chezmoi re-add, commit +dfs --no-commit # re-absorb without committing +``` + +`dfs` runs `chezmoi diff` so you see the drift before agreeing, then +uses `chezmoi re-add` to pull the live file contents back into the +source tree and commits the result. Other useful shortcuts you can alias yourself or type directly: @@ -90,14 +121,17 @@ flowchart TD Example: ```fish -add-secret OPENAI_API_KEY "op://Private/OpenAI/credential" --commit +add-secret OPENAI_API_KEY "op://Private/OpenAI/credential" exec fish # or open a new terminal — now $OPENAI_API_KEY is set ``` +The commit is automatic. Pass `--no-commit` if you want to stage the +registry change without committing. + Companions: ```fish list-secrets # show current bindings -rm-secret OPENAI_API_KEY # unregister (optional --commit) +rm-secret OPENAI_API_KEY # unregister (auto-commits) ``` ### How it works under the hood diff --git a/home/dot_config/fish/functions/add-secret.fish b/home/dot_config/fish/functions/add-secret.fish index 853d3e9..2d42f74 100644 --- a/home/dot_config/fish/functions/add-secret.fish +++ b/home/dot_config/fish/functions/add-secret.fish @@ -1,6 +1,6 @@ function add-secret --description "Register a 1Password secret as an auto-loaded env var" if test (count $argv) -lt 2 - echo "Usage: add-secret VAR_NAME \"op://Vault/Item/field\" [--commit]" + echo "Usage: add-secret VAR_NAME \"op://Vault/Item/field\" [--no-commit]" echo "Example: add-secret OPENAI_API_KEY \"op://Private/OpenAI/credential\"" echo "" echo "If the 1Password item doesn't exist yet you'll be prompted for the" @@ -10,8 +10,8 @@ function add-secret --description "Register a 1Password secret as an auto-loaded set -l var $argv[1] set -l ref $argv[2] - set -l do_commit 0 - contains -- --commit $argv; and set do_commit 1 + set -l do_commit 1 + contains -- --no-commit $argv; and set do_commit 0 if not string match -qr '^[A-Z_][A-Z0-9_]*$' -- $var echo "✗ VAR_NAME must be UPPER_SNAKE_CASE, got: $var" diff --git a/home/dot_config/fish/functions/dfe.fish b/home/dot_config/fish/functions/dfe.fish index 2018e5d..71af6c0 100644 --- a/home/dot_config/fish/functions/dfe.fish +++ b/home/dot_config/fish/functions/dfe.fish @@ -1,10 +1,38 @@ -function dfe --description "Edit a managed dotfile and auto-apply on save" +function dfe --description "Edit a managed dotfile, apply on save, commit the change" if test (count $argv) -lt 1 - echo "Usage: dfe " - echo "Examples:" - echo " dfe ~/.config/fish/config.fish" - echo " dfe ~/.Brewfile" + echo "Usage: dfe [--no-commit]" + echo " Edits the source, applies on save, auto-commits the diff." + echo " Pass --no-commit to leave the change unstaged." return 1 end - chezmoi edit --apply $argv + + set -l no_commit 0 + set -l paths + for a in $argv + switch $a + case --no-commit + set no_commit 1 + case '*' + set -a paths $a + end + end + + chezmoi edit --apply $paths; or return 1 + + if test $no_commit -eq 1 + return 0 + end + + set -l repo (dirname (chezmoi source-path)) + if not git -C $repo diff --quiet -- home/ + # Stage only the files inside home/ that changed. + set -l changed (git -C $repo diff --name-only -- home/) + if test (count $changed) -gt 0 + git -C $repo add $changed + set -l summary (string join ", " (for p in $changed; basename $p; end)) + git -C $repo commit -m "chore(config): update $summary via dfe" >/dev/null + echo "✓ committed: $summary" + echo " push with: git -C $repo push" + end + end end diff --git a/home/dot_config/fish/functions/dfs.fish b/home/dot_config/fish/functions/dfs.fish new file mode 100644 index 0000000..bcef4b5 --- /dev/null +++ b/home/dot_config/fish/functions/dfs.fish @@ -0,0 +1,48 @@ +function dfs --description "Sync drifted deployed files back into the dotfiles source" + set -l no_commit 0 + contains -- --no-commit $argv; and set no_commit 1 + + # chezmoi status columns: "MX ~/path" where M=deployed differs from source. + # A=missing from target, R=would be run script. We only care about M. + set -l drifted (chezmoi status 2>/dev/null | string match -r '^ M\s+(.+)$' | string replace -r '^ M\s+' '') + # string match/replace pipeline returns matches + replacements; take odd indices. + set -l paths + set -l i 2 + while test $i -le (count $drifted) + set -a paths $drifted[$i] + set i (math $i + 2) + end + + if test (count $paths) -eq 0 + echo "✓ no drift — deployed files match source" + return 0 + end + + echo "Drifted files (deployed ≠ source):" + for p in $paths + echo " $p" + end + echo "" + echo "Run 'chezmoi diff $paths[1]' to preview; 'dfs' will re-absorb all of them." + read -P "Re-absorb into source? [y/N] " ans + if not string match -qri '^y' -- $ans + echo "aborted" + return 1 + end + + chezmoi re-add $paths; or return 1 + echo "✓ source updated" + + if test $no_commit -eq 1 + return 0 + end + + set -l repo (dirname (chezmoi source-path)) + if not git -C $repo diff --quiet -- home/ + set -l changed (git -C $repo diff --name-only -- home/) + git -C $repo add $changed + set -l summary (string join ", " (for p in $changed; basename $p; end)) + git -C $repo commit -m "chore(config): sync drift from machine ($summary)" >/dev/null + echo "✓ committed. Push with: git -C $repo push" + end +end diff --git a/home/dot_config/fish/functions/rm-secret.fish b/home/dot_config/fish/functions/rm-secret.fish index f2f1c6a..4238965 100644 --- a/home/dot_config/fish/functions/rm-secret.fish +++ b/home/dot_config/fish/functions/rm-secret.fish @@ -1,12 +1,12 @@ function rm-secret --description "Unregister an auto-loaded 1Password secret" if test (count $argv) -lt 1 - echo "Usage: rm-secret VAR_NAME [--commit]" + echo "Usage: rm-secret VAR_NAME [--no-commit]" return 1 end set -l var $argv[1] - set -l do_commit 0 - contains -- --commit $argv; and set do_commit 1 + set -l do_commit 1 + contains -- --no-commit $argv; and set do_commit 0 set -l data (chezmoi source-path)/.chezmoidata/secrets.toml if not grep -q "^$var = " $data From 90b4e682da4bc059b26ba77a988996047d0cd579 Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Tue, 14 Apr 2026 22:45:44 +0700 Subject: [PATCH 06/10] feat: unified dotfiles CLI, user guide, Claude-assisted sync Consolidate commands: - dfe/dfs/add-secret/rm-secret/list-secrets merged into dotfiles subcommands: edit, drift, secret add/rm/list - 5 standalone fish functions deleted - Fish abbreviations added: de, dd, ds, du - Completions updated for new subcommands Documentation: - docs/guide.md: comprehensive user guide (8 sections + cheat sheet) - docs/customization.md deleted (content migrated to guide) - docs/decisions/006-auto-commit-workflow.md: ADR for auto-commit design - 6 SVG workflow diagrams: chezmoi model, dfe, dfs, brew, config, secrets - README restructured around unified CLI + Claude sync Claude-assisted sync (S-32): - .claude/commands/dotfiles-sync.md: slash command scans 10 dimensions of drift, reports plain-language, waits for approval, syncs, logs - docs/sync-log.md: append-only sync history First sync: - Re-absorbed Zed settings drift - Added 5 VS Code extensions - Cleaned up orphaned standalone fish functions from machine Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/commands/dotfiles-sync.md | 152 +++++ CLAUDE.md | 8 +- README.md | 85 +-- docs/customization.md | 181 ----- docs/decisions/006-auto-commit-workflow.md | 88 +++ docs/dotfiles_chezmoi_model.svg | 69 ++ docs/dotfiles_dfe_workflow.svg | 55 ++ docs/dotfiles_dfs_workflow.svg | 70 ++ docs/dotfiles_workflow_brew.svg | 60 ++ docs/dotfiles_workflow_config.svg | 72 ++ docs/dotfiles_workflow_secrets.svg | 65 ++ docs/guide.md | 635 ++++++++++++++++++ docs/specs/S-31-user-guide.md | 154 +++++ docs/specs/S-32-claude-assisted-sync.md | 301 +++++++++ docs/sync-log.md | 22 + docs/tasks.md | 6 +- home/dot_config/code/extensions.txt | 5 + .../dot_config/fish/completions/dotfiles.fish | 16 +- home/dot_config/fish/config.fish.tmpl | 6 + .../dot_config/fish/functions/add-secret.fish | 87 --- home/dot_config/fish/functions/dfe.fish | 38 -- home/dot_config/fish/functions/dfs.fish | 48 -- home/dot_config/fish/functions/dotfiles.fish | 257 ++++++- .../fish/functions/list-secrets.fish | 8 - home/dot_config/fish/functions/rm-secret.fish | 30 - 25 files changed, 2057 insertions(+), 461 deletions(-) create mode 100644 .claude/commands/dotfiles-sync.md delete mode 100644 docs/customization.md create mode 100644 docs/decisions/006-auto-commit-workflow.md create mode 100644 docs/dotfiles_chezmoi_model.svg create mode 100644 docs/dotfiles_dfe_workflow.svg create mode 100644 docs/dotfiles_dfs_workflow.svg create mode 100644 docs/dotfiles_workflow_brew.svg create mode 100644 docs/dotfiles_workflow_config.svg create mode 100644 docs/dotfiles_workflow_secrets.svg create mode 100644 docs/guide.md create mode 100644 docs/specs/S-31-user-guide.md create mode 100644 docs/specs/S-32-claude-assisted-sync.md create mode 100644 docs/sync-log.md delete mode 100644 home/dot_config/fish/functions/add-secret.fish delete mode 100644 home/dot_config/fish/functions/dfe.fish delete mode 100644 home/dot_config/fish/functions/dfs.fish delete mode 100644 home/dot_config/fish/functions/list-secrets.fish delete mode 100644 home/dot_config/fish/functions/rm-secret.fish diff --git a/.claude/commands/dotfiles-sync.md b/.claude/commands/dotfiles-sync.md new file mode 100644 index 0000000..b34223e --- /dev/null +++ b/.claude/commands/dotfiles-sync.md @@ -0,0 +1,152 @@ +You are maintaining a chezmoi-managed dotfiles repo. The user operates their Mac freely (installs packages, edits configs, adds API keys). Your job is to detect what changed on the machine, report it clearly, and sync approved changes back into the repo. + +## Step 1: Read context + +Read `docs/sync-log.md` to understand when the last sync happened and what changed. If the file doesn't exist, this is the first sync. + +## Step 2: Scan for drift + +Run these detection commands in parallel where possible: + +### Config drift +```bash +chezmoi status 2>/dev/null +``` +Look for lines starting with ` M` (modified) or `MM` (modified both sides). + +### Brew packages +```bash +# Installed but not in Brewfile +comm -23 <(brew leaves | sort) <(grep '^brew "' ~/.Brewfile 2>/dev/null | sed 's/brew "//;s/".*//' | sort) + +# In Brewfile but not installed +comm -13 <(brew leaves | sort) <(grep '^brew "' ~/.Brewfile 2>/dev/null | sed 's/brew "//;s/".*//' | sort) +``` + +### Cask apps +```bash +# Installed but not in Brewfile +comm -23 <(brew list --cask 2>/dev/null | sort) <(grep '^cask "' ~/.Brewfile 2>/dev/null | sed 's/cask "//;s/".*//' | sort) + +# In Brewfile but not installed +comm -13 <(brew list --cask 2>/dev/null | sort) <(grep '^cask "' ~/.Brewfile 2>/dev/null | sed 's/cask "//;s/".*//' | sort) +``` + +### VS Code extensions +```bash +# Installed but not tracked +comm -23 <(code --list-extensions 2>/dev/null | sort) <(sort ~/.config/code/extensions.txt 2>/dev/null) + +# Tracked but not installed +comm -13 <(code --list-extensions 2>/dev/null | sort) <(sort ~/.config/code/extensions.txt 2>/dev/null) +``` + +### New fish functions (not managed by chezmoi) +```bash +# Functions on disk but not in source +comm -23 <(ls ~/.config/fish/functions/ 2>/dev/null | sort) <(chezmoi managed | grep 'fish/functions/' | xargs -I{} basename {} | sort) +``` + +### New SSH config fragments +```bash +# SSH config.d files not managed +comm -23 <(ls ~/.ssh/config.d/ 2>/dev/null | sort) <(chezmoi managed | grep 'ssh/config.d/' | xargs -I{} basename {} | sort) +``` + +### Hardcoded secrets in fish config +```bash +# Look for set -gx with what looks like API keys (long alphanumeric strings) +grep -n 'set -gx.*[A-Za-z0-9_]\{20,\}' ~/.config/fish/config.fish ~/.config/fish/conf.d/*.fish 2>/dev/null | grep -v 'onepasswordRead\|op://' || true +``` + +## Step 3: Report + +Present findings in plain language, grouped by category. For each category, show: +- What changed (specific names, not counts) +- Brief context if you can infer it ("ollama is probably for local LLM testing") + +Use this format: + +``` +Dotfiles sync report (YYYY-MM-DD) + +Config drift (N files): + - path — what changed (brief description of the diff) + +New packages (N brew, N casks): + Brew: pkg1, pkg2, ... + Cask: app1, app2, ... + +Stale entries (N brew, N casks): + Brew: pkg1, pkg2, ... (in Brewfile but not installed) + Cask: app1, app2, ... (in Brewfile but not installed) + +VS Code extensions: + New: ext1, ext2, ... + Removed: ext1, ... + +New fish functions (N): + func1, func2, ... + +New SSH configs (N): + host1, host2, ... + +Secrets: + [any findings or "no issues"] + +What would you like me to do? +``` + +If a category has no findings, omit it from the report. + +## Step 4: Wait for decisions + +Do NOT make any changes yet. Ask the user what to do. They'll respond in plain language: +- "Add the new packages" +- "Drop raycast and slack" +- "Keep btop in Brewfile even though not installed" +- "Sync the Zed config" +- "Do it all" + +## Step 5: Execute + +Based on the user's decisions: + +| Action | Method | +|--------|--------| +| Absorb config drift | `chezmoi re-add ` | +| Add brew packages | Edit `home/dot_Brewfile.tmpl`, add `brew "pkg"` in correct section | +| Remove stale brew | Edit `home/dot_Brewfile.tmpl`, delete lines | +| Add casks | Edit `home/dot_Brewfile.tmpl`, add `cask "app"` in correct section | +| Remove stale casks | Edit `home/dot_Brewfile.tmpl`, delete lines | +| Sync VS Code extensions | Update `home/dot_config/code/extensions.txt` | +| Track fish functions | `chezmoi add ~/.config/fish/functions/NAME.fish` | +| Track SSH configs | `chezmoi add ~/.ssh/config.d/NAME` | +| Register secrets | Append to `home/.chezmoidata/secrets.toml` | + +When editing the Brewfile, preserve the existing section structure (base/dev/apps). Place new entries in the appropriate section. + +## Step 6: Log + +Append an entry to `docs/sync-log.md`: + +```markdown +## [YYYY-MM-DD] sync + +[Category]: + - [what changed] + +--- +``` + +## Step 7: Commit + +Stage all changes and commit with a descriptive message: + +``` +chore(sync): dotfiles sync YYYY-MM-DD + +[Summary of changes by category] +``` + +Then ask: "Push to remote?" Only push if the user confirms. diff --git a/CLAUDE.md b/CLAUDE.md index 772b67b..8ef0dce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co A chezmoi-managed dotfiles repo for macOS (Apple Silicon). The `home/` directory is the chezmoi source state — it maps to `$HOME` on the target machine. The `install.sh` script bootstraps a fresh Mac from zero. -User-facing customization flows (how to change Brewfile, secrets, editors, etc.) live in [`docs/customization.md`](docs/customization.md). When a user asks "how do I change X", point them there rather than reinventing. +User-facing customization flows (how to change Brewfile, secrets, editors, etc.) live in [`docs/guide.md`](docs/guide.md). When a user asks "how do I change X", point them there rather than reinventing. ## Key commands @@ -38,9 +38,9 @@ chezmoi uses filename prefixes to encode target attributes: Used directly in: `dot_gitconfig.tmpl`, `dot_config/zed/settings.json.tmpl`. For auto-loaded shell env vars, prefer the data-driven workflow: register -entries in `.chezmoidata/secrets.toml` via `add-secret VAR op://...` and let -`secrets.fish.tmpl` iterate. Do not hand-edit the template to add new env -vars — use `add-secret` / `rm-secret` / `list-secrets`. +entries in `.chezmoidata/secrets.toml` via `dotfiles secret add VAR op://...` +and let `secrets.fish.tmpl` iterate. Do not hand-edit the template to add +new env vars — use `dotfiles secret add` / `dotfiles secret rm` / `dotfiles secret list`. **macOS Keychain** — via `keyring` template function or runtime fish functions: ``` diff --git a/README.md b/README.md index 72359a0..54ddbcd 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ cd ~/dotfiles | **Editors** | VS Code + Zed (settings, extensions, MCP servers) | | **Git** | .gitconfig (delta diffs, aliases) + .gitignore + commit template | | **SSH** | 1Password SSH Agent (optional), modular config.d/ | -| **Secrets** | 1Password (`op://`) + macOS Keychain -- never in git | +| **Secrets** | 1Password (`op://`) + data-driven registry (`secrets.toml`) -- never in git | | **Packages** | Layered Brewfile (base/dev/apps) + Mac App Store (`mas`) | | **Languages** | mise (Node, Python, Go, Ruby) via `.tool-versions` | | **Containers** | OrbStack / Docker config | @@ -137,16 +137,51 @@ cd ~/dotfiles - **Layered Brewfile** -- base tools always install; dev toolchains and GUI apps are conditional. Set `headless=true` for servers. - **Zero plaintext secrets** -- 1Password `op://` references in templates, macOS Keychain for the rest. The rendered secrets only exist on your machine, never in git. -- **13-command CLI** -- `dotfiles sync`, `dotfiles doctor`, `dotfiles bench`, `dotfiles backup`... no need to remember raw chezmoi commands. +- **Claude-assisted sync** -- run `/dotfiles-sync` in Claude Code to detect all drift (brew, casks, configs, extensions, secrets), review in plain language, and sync with one approval. Manual commands (`dotfiles edit`, `dotfiles drift`) work offline. +- **15-command CLI** -- `dotfiles edit`, `dotfiles drift`, `dotfiles secret`, `dotfiles sync`, `dotfiles doctor`, `dotfiles bench`... no need to remember raw chezmoi commands ([ADR-006](docs/decisions/006-auto-commit-workflow.md)). - **CI-tested weekly** -- shellcheck + chezmoi dry-run on macOS. Catches regressions before your next fresh install. - **Graceful degradation** -- works with or without 1Password. Skip web3, skip Mac App Store, pick your editor. Everything is opt-in. ## Daily usage -The `dotfiles` wrapper provides ergonomic commands: +**Core principle:** every setting change should be both applied to your machine and committed to the repo in one step. The helpers below enforce this by default. + +

+ chezmoi model: source to target +

+ +### Editing configs + +```fish +dotfiles edit ~/.config/fish/config.fish # edit source → apply → auto-commit +dotfiles edit ~/.Brewfile # edit Brewfile → apply (runs brew bundle) → commit +dotfiles edit ~/.config/ghostty/config # edit → apply (live reload) → commit +``` + +`dotfiles edit` opens the chezmoi source file in your editor, applies on save, and commits the change. Pass `--no-commit` to skip the commit. + +

+ dfe workflow: edit, apply, commit +

+ +### Syncing drift + +If you edited a deployed file directly (or an app rewrote its config), `dotfiles drift` detects the drift and pulls it back into the source: + +```fish +dotfiles drift # detect drift → prompt → re-add → commit +dotfiles drift --no-commit # re-absorb without committing +``` + +

+ dotfiles drift workflow: detect drift and sync +

+ +### The `dotfiles` wrapper + +For everything else, the `dotfiles` CLI provides ergonomic subcommands: ```fish -dotfiles edit ~/.config/fish/config.fish # edit a config dotfiles diff # preview changes dotfiles sync # apply everything dotfiles update # pull latest + apply @@ -157,12 +192,6 @@ dotfiles backup # back up config + age key to 1Passwo dotfiles encrypt-setup # guided age encryption setup ``` -Adding a Homebrew package: -```fish -dotfiles edit ~/.Brewfile # add the line -dotfiles sync # auto-runs brew bundle -``` -
Raw chezmoi commands @@ -177,42 +206,22 @@ chezmoi apply --refresh-externals ## Customization -### Files you'll want to edit - -| File | What to change | -|------|---------------| -| `home/dot_Brewfile.tmpl` | Add/remove Homebrew packages and casks (layered: base/dev/apps) | -| `home/dot_config/fish/config.fish.tmpl` | Shell aliases, paths, tool integrations | -| `home/dot_config/ghostty/config` | Terminal theme, font, keybindings | -| `home/dot_config/tmux/tmux.conf` | tmux prefix, keybindings, status bar | -| `home/dot_config/code/settings.json` | VS Code theme, font, settings | -| `home/dot_config/code/extensions.txt` | VS Code extensions (one per line) | -| `home/dot_config/zed/settings.json.tmpl` | Zed theme, MCP servers | -| `home/dot_tool-versions` | Global language versions | -| `home/.chezmoiscripts/run_once_after_mas-apps.sh.tmpl` | Mac App Store apps | -| `home/.chezmoiscripts/run_once_after_macos-defaults.sh.tmpl` | macOS system preferences | -| `home/.chezmoiexternal.toml` | Fish plugins to auto-download | - -### Customizing and adding secrets - -The universal flow: **edit source → `chezmoi apply` → new shell/reload**. -The `dfe` fish helper collapses edit + apply into one command: +Use `dotfiles edit` to edit any config (edit → apply → commit in one step): ```fish -dfe ~/.Brewfile # edit + auto-apply on save -dfe ~/.config/fish/config.fish +dotfiles edit ~/.Brewfile # add Homebrew packages +dotfiles edit ~/.config/fish/config.fish # shell config +dotfiles edit ~/.config/ghostty/config # terminal settings ``` -For 1Password secrets, one command creates the item (if missing), -registers it, applies, and commits: +Add secrets via 1Password: ```fish -add-secret OPENAI_API_KEY "op://Private/OpenAI/credential" +dotfiles secret add OPENAI_API_KEY "op://Private/OpenAI/credential" ``` -Full reference — quick-change table for every common file, the secrets -workflow diagram, and troubleshooting — lives in -[docs/customization.md](docs/customization.md). +The full user guide covers walkthroughs, the secrets workflow, multi-machine +setup, troubleshooting, and architecture: **[docs/guide.md](docs/guide.md)**.
Encrypted files (age) diff --git a/docs/customization.md b/docs/customization.md deleted file mode 100644 index b259f87..0000000 --- a/docs/customization.md +++ /dev/null @@ -1,181 +0,0 @@ -# Customization guide - -How to change anything in this dotfiles repo without going back and -forth between the source directory and your machine. - -## Core requirement - -> Every change to a setting must be **applied to the machine** AND -> **backed up in the dotfiles repo** in one step. The user should never -> have to remember to re-run `chezmoi apply` or `git commit` separately. - -All helpers in this guide (`dfe`, `dfs`, `add-secret`, `rm-secret`) are -designed to satisfy this rule by default. Opt out with `--no-commit` if -you need to stage a change but not commit it. - -Pushing is still manual. Run `git -C (dirname (chezmoi source-path)) push` -when you're ready to share the commits with other machines. - -## The one rule - -``` -edit source → chezmoi apply → effect on your machine -``` - -Every customization follows this shape. The source lives in the repo at -`home/` and gets linked to `~/.local/share/chezmoi`. When you run -`chezmoi apply`, chezmoi renders any `.tmpl` files, then deploys the -result to `$HOME`. - -```mermaid -flowchart LR - A[you edit source file
in ~/.local/share/chezmoi/...] -->|chezmoi apply| B[chezmoi renders
.tmpl files] - B --> C[deploys to $HOME
~/.config/fish/config.fish etc.] - C --> D[new shell /
reload app] -``` - -### The shortcut - -The `dfe` fish helper edits the **source**, applies on save, and -commits the change in one shot: - -```fish -dfe ~/.config/fish/config.fish # edit + apply + commit -dfe ~/.config/fish/config.fish --no-commit # edit + apply only -``` - -Pushing is still on you. When there are commits to share: - -```fish -git -C (dirname (chezmoi source-path)) push -``` - -### Reverse drift — `dfs` - -If you edited a deployed file directly (bypassing `dfe`) and want the -change captured back into the source: - -```fish -dfs # diff, prompt, chezmoi re-add, commit -dfs --no-commit # re-absorb without committing -``` - -`dfs` runs `chezmoi diff` so you see the drift before agreeing, then -uses `chezmoi re-add` to pull the live file contents back into the -source tree and commits the result. - -Other useful shortcuts you can alias yourself or type directly: - -| What | Command | -|---|---| -| Apply pending changes | `chezmoi apply` | -| See what would change | `chezmoi diff` | -| Edit without applying | `chezmoi edit ` | -| Jump to the source dir | `chezmoi cd` | -| List everything managed | `chezmoi managed` | - -## Quick reference — common customizations - -| Change | File | Command | -|---|---|---| -| Homebrew packages | `home/dot_Brewfile.tmpl` | `dfe ~/.Brewfile` (auto-runs `brew bundle`) | -| Fish abbrs / env | `home/dot_config/fish/config.fish.tmpl` | `dfe ~/.config/fish/config.fish` | -| Fish function | `home/dot_config/fish/functions/NAME.fish` | `dfe ~/.config/fish/functions/NAME.fish` | -| Starship prompt | `home/dot_config/starship.toml` | `dfe ~/.config/starship.toml` | -| Ghostty config | `home/dot_config/ghostty/config` | `dfe ~/.config/ghostty/config` (live reload) | -| Ghostty theme | any time | `ghostty-theme N` — live switch | -| VS Code settings | `home/dot_config/code/settings.json` | `dfe ~/.config/code/settings.json` | -| VS Code extensions | `home/dot_config/code/extensions.txt` | edit + `chezmoi apply` | -| Zed settings + MCP | `home/dot_config/zed/settings.json.tmpl` | `dfe ~/.config/zed/settings.json` | -| Git config | `home/dot_gitconfig.tmpl` | `dfe ~/.gitconfig` | -| SSH hosts | `home/dot_ssh/config.d/*` | drop file + `chezmoi apply` | -| macOS defaults | `home/.chezmoiscripts/run_once_after_macos-defaults.sh.tmpl` | edit + `chezmoi apply` (re-runs on content change) | -| Claude Code config | `home/dot_claude/` | `dfe ~/.claude/settings.json` | -| 1Password secrets | `home/.chezmoidata/secrets.toml` | `add-secret VAR op://...` — see below | -| Fish plugins | `home/.chezmoiexternal.toml` | edit + `chezmoi apply --refresh-externals` | -| Setup wizard answers | `~/.config/chezmoi/chezmoi.toml` | `chezmoi init` to re-prompt | - -## Secrets workflow - -Secrets are handled differently because the value lives in 1Password, -not in the repo. Three tiers, pick per secret: - -| Tier | When | Command | -|---|---|---| -| **Auto-loaded** | env var you want in every shell | `add-secret VAR "op://..."` | -| **Runtime** | occasional CLI use, don't want in every env | `op-env VAR "op://..."` | -| **One-off** | inline trial | `set -x VAR (op read "op://...")` | - -### The `add-secret` flow - -```mermaid -flowchart TD - A[add-secret VAR op://Vault/Item/field --commit] --> B{op read
succeeds?} - B -- no --> C[prompt for value
hidden input] --> D[op item create
in 1Password] - D --> E - B -- yes --> E[append VAR=ref to
.chezmoidata/secrets.toml] - E --> F[chezmoi apply
renders secrets.fish] - F --> G[git commit --commit flag] - G --> H[user opens new shell
$VAR is set] -``` - -Example: -```fish -add-secret OPENAI_API_KEY "op://Private/OpenAI/credential" -exec fish # or open a new terminal — now $OPENAI_API_KEY is set -``` - -The commit is automatic. Pass `--no-commit` if you want to stage the -registry change without committing. - -Companions: -```fish -list-secrets # show current bindings -rm-secret OPENAI_API_KEY # unregister (auto-commits) -``` - -### How it works under the hood - -`home/.chezmoidata/secrets.toml` is a registry chezmoi loads automatically -as the `.secrets` template variable. `home/dot_config/fish/conf.d/secrets.fish.tmpl` -iterates `.secrets` at apply time and emits one `set -gx` per entry, -resolving each `op://` ref via `onepasswordRead`. The rendered -`~/.config/fish/conf.d/secrets.fish` contains real tokens — it never -leaves your machine. - -### Rotating a token - -Just update the value in 1Password. The next `chezmoi apply` re-renders -the fish file with the new value. To force immediately: - -```fish -chezmoi apply ~/.config/fish/conf.d/secrets.fish -exec fish -``` - -## Troubleshooting - -- **"I changed the source but nothing happened"** — you probably edited - `~/.config/...` directly. Edit the source at - `~/.local/share/chezmoi/...` (or use `dfe`). `chezmoi diff` will show - if your change drifted. -- **"chezmoi apply wants to prompt me"** — you modified a deployed file - outside chezmoi. Either accept the overwrite, or `chezmoi merge PATH` - to bring the change back into the source. -- **"op read failed"** — run `eval (op signin)` once per shell session. -- **"Template error at apply"** — a `.tmpl` file is missing a variable. - Run `chezmoi init` to refresh the answer cache. -- **"Brewfile didn't re-run"** — `run_onchange_before_brew-bundle.sh.tmpl` - only fires when the file content changes. Force it with - `rm ~/.config/chezmoi/state/chezmoistate.boltdb` (nuclear) or just - `brew bundle --file=~/.Brewfile`. - -## Key design choices - -- **No hand-edits to `secrets.fish.tmpl`** for new env vars — use the - registry so the template stays small and readers see one source of - truth. -- **`dfe` over `chezmoi edit` + `chezmoi apply`** — less to remember, - fewer forgotten applies. -- **1Password references, not values, are in git** — rotation doesn't - touch the repo. diff --git a/docs/decisions/006-auto-commit-workflow.md b/docs/decisions/006-auto-commit-workflow.md new file mode 100644 index 0000000..0f02708 --- /dev/null +++ b/docs/decisions/006-auto-commit-workflow.md @@ -0,0 +1,88 @@ +# ADR-006: Auto-commit workflow for dotfile changes + +## Status: accepted + +## Context + +chezmoi separates the **source state** (the repo in `~/.local/share/chezmoi/`) from the **target state** (deployed files in `$HOME`). This two-layer model is powerful but creates a recurring failure mode: you change a config file, it works on your machine, but you forget to commit. Days later you set up a new machine or lose a disk, and the change is gone. + +The gap between "applied to my machine" and "backed up in git" is where config changes go to die. In practice, three things go wrong: + +1. **Forgot to commit.** You run `chezmoi edit --apply`, the change works, you move on. The source file is modified but never staged or committed. +2. **Edited the wrong layer.** You open `~/.config/fish/config.fish` directly instead of the chezmoi source. The deployed file now differs from the source (drift). Next `chezmoi apply` silently overwrites your change. +3. **Partial commit.** You edit three files, commit one, forget the other two. The repo is in an inconsistent state you won't notice until something breaks. + +All three are human memory failures, not technical failures. The tooling should close the loop automatically. + +## Decision + +Make **commit-on-change the default** for all dotfile editing workflows. Every helper that modifies the chezmoi source tree auto-commits the diff after a successful apply. Push remains manual. + +### The helpers + +**`dotfiles edit` (dotfile edit)** handles the forward path: source → machine. + +``` +dotfiles edit ~/.config/fish/config.fish +``` + +This runs `chezmoi edit --apply` to open the source file in your editor, applies on save, then auto-commits any changed files under `home/`. The commit message is mechanical: `chore(config): update config.fish via dotfiles edit`. + +**`dotfiles drift` (dotfile sync)** handles the reverse path: machine → source. + +``` +dotfiles drift +``` + +This runs `chezmoi status` to find deployed files that differ from the source (drift), shows you the list, asks for confirmation, then runs `chezmoi re-add` to pull the live versions back into the source tree and commits the result. + +**`dotfiles secret add` / `dotfiles secret rm`** modify `.chezmoidata/secrets.toml` (the secret registry) and auto-commit the change. + +All four helpers accept `--no-commit` to opt out. + +### Why push is manual + +Auto-commit is safe because it's local and reversible (`git reset`). Auto-push is not: + +- You might be mid-experiment and want to squash commits before pushing. +- A bad config could propagate to other machines that pull automatically. +- Push failures (auth, network) would need retry logic and error handling that doesn't belong in a fish function. + +Instead, each helper prints the exact `git push` command after committing so you can run it when ready. This keeps the helpers simple and the blast radius local. + +### Why commit is opt-out, not opt-in + +The original design had `--commit` as an opt-in flag. This was backwards: the safe default should be "your changes are backed up." Forgetting `--commit` means losing work silently. Forgetting `--no-commit` means at worst an extra commit you can amend or squash. + +The cost of a forgotten `--commit` (lost work) is much higher than the cost of a forgotten `--no-commit` (extra commit). So commit is the default. + +### The two-direction model + +Config drift can happen in either direction: + +| Direction | Cause | Tool | +|-----------|-------|------| +| Source → machine | You edit the source, need to apply | `dotfiles edit` | +| Machine → source | You edited the deployed file directly, or an app rewrote its config | `dotfiles drift` | + +Together, `dotfiles edit` and `dotfiles drift` cover both directions. The workflow is: + +1. **Normal edits**: use `dotfiles edit`. One command: edit, apply, commit. +2. **Accidental direct edits**: run `dotfiles drift` to detect and re-absorb drift. +3. **Secrets**: use `dotfiles secret add` / `dotfiles secret rm`. Same auto-commit behavior. + +## Alternatives considered + +- **fswatch / file watcher daemon**: Auto-apply on every source file save. Too aggressive; you lose the ability to make multi-file changes before applying. Also requires a background process that can die silently. +- **Git hooks (post-commit apply)**: Couples git operations to chezmoi apply. Breaks when you commit non-chezmoi files. Direction is wrong: we want apply-then-commit, not commit-then-apply. +- **Makefile / justfile targets**: `make apply`, `make sync`. Works but adds a dependency and another tool to remember. Fish functions are discoverable via tab completion and don't require being in the repo directory. +- **chezmoi's built-in git auto-commit**: chezmoi has `git.autoCommit` in its config, but it commits on every `chezmoi apply` including no-op runs, and the commit messages aren't customizable. Our helpers only commit when there are actual changes and produce descriptive messages. + +## Consequences + +- Every helper that touches source files auto-commits by default. Contributors should expect this. +- Commit messages from helpers follow the pattern `chore(config): ...`. They are mechanical and meant for backup, not human-readable changelogs. +- Push is always manual. No helper will ever run `git push` automatically. +- The `--no-commit` flag is the escape hatch for all helpers when you need to batch changes or are experimenting. +- `dotfiles drift` requires user confirmation before re-absorbing drift. It will never silently overwrite source files. +- The workflow assumes you're working in a git repo. If the dotfiles source isn't a git repo (unlikely but possible), the commit step is silently skipped. diff --git a/docs/dotfiles_chezmoi_model.svg b/docs/dotfiles_chezmoi_model.svg new file mode 100644 index 0000000..3585e05 --- /dev/null +++ b/docs/dotfiles_chezmoi_model.svg @@ -0,0 +1,69 @@ + + + + + + +How chezmoi works: source to target + + + +Source state (repo) + + +dot_gitconfig.tmpl + + +dot_config/fish/... + + +secrets.toml (registry) + + +Contains op:// refs +Never real values + + + +1Password vault + +sk-proj-abc123... + + + +chezmoi apply +Render + inject + + + + + + + +secrets + + + +Target state ($HOME) + + +~/.gitconfig + + +~/.config/fish/... + + +secrets.fish + + +Contains real values +Only on your machine + + + + + + +Git only sees the left column. Real secrets only exist in the right column. +The repo is safe to publish. Rotation happens in 1Password, then re-apply. + \ No newline at end of file diff --git a/docs/dotfiles_dfe_workflow.svg b/docs/dotfiles_dfe_workflow.svg new file mode 100644 index 0000000..f15548a --- /dev/null +++ b/docs/dotfiles_dfe_workflow.svg @@ -0,0 +1,55 @@ + + + + + + +dfe: edit, apply, and commit in one step + + + +dfe ~/.config/fish/config.fish + + + + + +chezmoi edit --apply +Opens the source file in your $EDITOR + + + + + +You save and close the editor + + + + + +chezmoi apply (automatic) +Renders template, deploys to $HOME + + + + + +git add + commit (automatic) +"chore(config): update config.fish via dfe" + + + + + +Done. Prints push command. +Push is always manual (--no-commit to skip step 5) + + + +Machine updated + + + +Repo backed up + + \ No newline at end of file diff --git a/docs/dotfiles_dfs_workflow.svg b/docs/dotfiles_dfs_workflow.svg new file mode 100644 index 0000000..4b3ca8a --- /dev/null +++ b/docs/dotfiles_dfs_workflow.svg @@ -0,0 +1,70 @@ + + + + + + + +dfs: detect drift and sync back to source + + + +Deployed file was edited directly +e.g. vim ~/.gitconfig instead of dfe ~/.gitconfig + + + + + +dfs + + + + + +chezmoi status +Finds files where deployed differs from source + + + + + +Re-absorb? y/N + + +no + + +aborted + + +yes + + + + +chezmoi re-add +Pulls deployed file back into source tree + + + + + +git add + commit (automatic) +"chore(config): sync drift from machine" + + + +Done. Prints push command. (--no-commit to skip step 5) + + + + + +Source updated + + + +Repo backed up + + \ No newline at end of file diff --git a/docs/dotfiles_workflow_brew.svg b/docs/dotfiles_workflow_brew.svg new file mode 100644 index 0000000..b01a6e2 --- /dev/null +++ b/docs/dotfiles_workflow_brew.svg @@ -0,0 +1,60 @@ + + + + + + + +Adding a Homebrew package + + + +The wrong way (don't do this) + + +brew install ripgrep + + + + +Package installed locally +but NOT in your Brewfile + + + + +Next machine: package missing +brew bundle won't install it +because Brewfile doesn't know about it + + + +The right way (one command) + + +dfe ~/.Brewfile + + + + +Editor opens Brewfile +Add: brew "ripgrep" + + + + +chezmoi apply triggers +brew bundle runs automatically + + + + +git auto-commit +Brewfile change saved in repo + + + + +Package installed + backed up +Next machine gets it too + \ No newline at end of file diff --git a/docs/dotfiles_workflow_config.svg b/docs/dotfiles_workflow_config.svg new file mode 100644 index 0000000..6cfae57 --- /dev/null +++ b/docs/dotfiles_workflow_config.svg @@ -0,0 +1,72 @@ + + + + + + + +Changing a config file (two paths) + + + +Scenario A: you edit via dfe (ideal) + + +Scenario B: you edit the file directly + + + +dfe ~/.config/fish/config.fish + + + + +Edit source in $EDITOR, save + + + + +chezmoi apply (automatic) + + + + +git commit (automatic) + + + + +Done. Machine + repo in sync. + + + +vim ~/.config/fish/config.fish +(edited deployed file, not source) + + + + +Machine updated, repo NOT +Next chezmoi apply will overwrite! + + + + + +Fix: run dfs + + + + +Detects drift, shows changes + + + + +Confirm, re-add, auto-commit + + + + +Recovered. Now in sync. + \ No newline at end of file diff --git a/docs/dotfiles_workflow_secrets.svg b/docs/dotfiles_workflow_secrets.svg new file mode 100644 index 0000000..72d3d65 --- /dev/null +++ b/docs/dotfiles_workflow_secrets.svg @@ -0,0 +1,65 @@ + + + + + + +Adding or updating a secret + + + +New secret (never in 1Password) + +add-secret OPENAI_API_KEY \ + "op://Private/OpenAI/credential" + + +Rotate existing secret + +Update value in 1Password app +or: op item edit "OpenAI" credential=sk-... + + + + + + + + + + +What happens automatically + + + +1 +Creates 1Password item if it doesn't exist +op item create --vault=Private --title="OpenAI" ... + + + + + +2 +Registers binding in secrets.toml +OPENAI_API_KEY = "op://Private/OpenAI/credential" + + + + + +3 +chezmoi apply renders secrets.fish +Template reads op:// ref, 1Password returns real value + + + + + +4 +git auto-commit secrets.toml change +Only the op:// reference, never the real value + + +For rotation: just update in 1Password, then chezmoi apply. No repo change needed. + \ No newline at end of file diff --git a/docs/guide.md b/docs/guide.md new file mode 100644 index 0000000..e6e2f90 --- /dev/null +++ b/docs/guide.md @@ -0,0 +1,635 @@ +# User guide + +Everything you need to use and customize this dotfiles setup. The +[README](../README.md) covers quick start and what's included; this guide +covers how it all works and how to make it yours. + +--- + +## 1. How this works + +### The two layers + +chezmoi keeps two copies of every config file: + +| Layer | Location | Contains | +|-------|----------|----------| +| **Source** (repo) | `~/.local/share/chezmoi/home/` | Templates, `op://` refs, chezmoi prefixes | +| **Target** (machine) | `$HOME` | Rendered files with real values | + +You edit the source. chezmoi renders templates and copies the result to +your home directory. This is a one-way flow: source to target. + +

+ chezmoi model: source to target +

+ +**Why two layers?** The source can live in a public git repo because it +never contains real secrets — only `op://` references. The target has +your actual API keys, but it's never committed. + +Here's the full bootstrap flow when you first install: + +

+ Bootstrap flow +

+ +### Where things live + +``` +~/dotfiles/ ← the repo (git clone location) +├── home/ ← chezmoi source state +│ ├── dot_gitconfig.tmpl ← becomes ~/.gitconfig +│ ├── dot_config/ +│ │ ├── fish/ +│ │ │ ├── config.fish.tmpl ← shell config +│ │ │ ├── functions/ ← one file per function +│ │ │ ├── completions/ ← tab completions +│ │ │ └── conf.d/ ← auto-sourced snippets +│ │ ├── ghostty/config ← terminal config +│ │ ├── starship.toml ← prompt config +│ │ ├── tmux/tmux.conf ← multiplexer config +│ │ ├── code/ ← VS Code settings +│ │ └── zed/ ← Zed settings +│ ├── dot_Brewfile.tmpl ← Homebrew packages +│ ├── .chezmoidata/secrets.toml ← secret registry (op:// refs) +│ └── .chezmoiscripts/ ← automation scripts +├── docs/ ← this guide, specs, ADRs +└── install.sh ← bootstrap script +``` + +Naming: `dot_` becomes `.`, `.tmpl` means "render this template", +`private_` sets mode 0600. + +--- + +## 2. Your first 30 minutes + +You just ran `install.sh`. Here's what's on your machine now. + +### Shell: Fish + +Your default shell is now [Fish](https://fishshell.com/). It differs +from bash/zsh in a few ways: + +- **Autosuggestions** — type a few characters, Fish suggests from history + in gray. Press `→` to accept. +- **Tab completion** — press `Tab` for rich completions with descriptions. +- **No `.bashrc`** — config lives at `~/.config/fish/config.fish`. +- **Abbreviations, not aliases** — type `gs` then space, it expands to + `git status`. Your abbreviations: + +| Category | Abbreviations | +|----------|--------------| +| Git | `g` `gs` `gd` `gl` `gp` `gc` `gco` `gpr` | +| Navigation | `ls` `l` `ll` `la` `lt` `..` `...` | +| Modern replacements | `cat`→bat `top`→btop `du`→dust `df`→duf `ps`→procs | +| Tmux | `tx` `tml` `tma` `tmk` | +| Kubernetes/Docker | `k` `k9` `d` `dc` | + +Fish plugins (installed without a plugin manager, via direct download): + +| Plugin | What it does | +|--------|-------------| +| **autopair** | Auto-closes brackets, quotes, parentheses | +| **done** | Desktop notification when a long command finishes | +| **sponge** | Removes failed commands from history | +| **async-prompt** | Keeps the prompt responsive while git info loads | + +### Terminal: Ghostty + +[Ghostty](https://ghostty.org/) is your terminal. Key settings: + +- **Font**: SauceCodePro Nerd Font, size 12 +- **Theme**: base16-eighties-dark (switch with `ghostty-theme N`) +- **Quick toggle**: `Cmd+`` brings up the terminal from anywhere +- **Splits**: `Cmd+Shift+Enter` (right), `Cmd+Shift+-` (down) +- **Navigate splits**: `Cmd+Alt+Arrows` + +### Prompt: Starship + +[Starship](https://starship.rs/) shows: directory, git branch/status, +language versions (Go, Python, Node, Rust), cloud context (AWS, k8s), +and command duration (if >3s). Green arrow = last command OK, red = error. + +### Editors + +Your editor was set during install (`chezmoi init`). VS Code and Zed +both have managed settings and extensions. MCP servers in Zed are +pre-configured with 1Password-injected API keys. + +### Verify everything works + +```fish +dotfiles doctor +``` + +This checks: chezmoi installed, fish is default shell, Homebrew, 1Password, +key config files exist, git identity set, CLI tools present, no drift. + +--- + +## 3. Daily workflows + +### Primary workflow: Claude-assisted sync + +The easiest way to keep your dotfiles in sync is to ask Claude. Run +`/dotfiles-sync` in Claude Code (or just say "catch up with my dotfiles"). +Claude scans your machine for drift across brew packages, casks, config +files, VS Code extensions, fish functions, and secrets. It reports what +changed in plain language and waits for your instructions before syncing. + +This is the recommended workflow for batch changes. The manual commands +below are for quick edits or when you're not in a Claude session. + +### Editing any config + +Use `dotfiles edit` (dotfile edit) for all config changes. It edits the source, +applies, and auto-commits in one step: + +```fish +dotfiles edit ~/.config/fish/config.fish +``` + +

+ dfe workflow +

+ +Pass `--no-commit` to skip the auto-commit. + +### Adding a Homebrew package + +Don't run `brew install` directly — that installs locally but doesn't +update the Brewfile, so your next machine won't have it. + +```fish +dotfiles edit ~/.Brewfile # add the line, save, brew bundle runs automatically +``` + +

+ Homebrew workflow +

+ +### Handling drift + +If you (or an app) edited a deployed file directly, `dotfiles drift` detects and +fixes it: + +```fish +dotfiles drift # shows drifted files, prompts, re-absorbs into source, commits +``` + +

+ dfs workflow +

+ +### Changing a config: two paths + +

+ Config change: dfe vs direct edit +

+ +### The `dotfiles` CLI + +For everything beyond editing, use the `dotfiles` wrapper: + +| Command | Alias | Abbr | What it does | +|---------|-------|------|-------------| +| `dotfiles edit ` | `e` | `de` | Edit + apply + auto-commit | +| `dotfiles drift` | | `dd` | Detect and re-absorb drifted files | +| `dotfiles secret ` | | | Manage 1Password secrets (add/rm/list) | +| `dotfiles diff` | `d` | | Show pending changes | +| `dotfiles sync` | `s` | `ds` | Apply all changes | +| `dotfiles update` | `u` | `du` | Pull latest + apply | +| `dotfiles status` | `st` | | Managed file count + pending diffs | +| `dotfiles cd` | | | Go to chezmoi source directory | +| `dotfiles refresh` | `r` | | Re-download fish plugins | +| `dotfiles add ` | `a` | | Add a new file to chezmoi | +| `dotfiles doctor` | | | Health check | +| `dotfiles bench` | | | Benchmark shell startup time | +| `dotfiles backup` | | | Back up config + age key to 1Password | +| `dotfiles encrypt-setup` | | | Set up age encryption | + +All subcommands have tab completions. Abbreviations expand on space (e.g. type `de` then space, it becomes `dotfiles edit`). + +--- + +## 4. Customization cookbook + +### Quick-change table + +| Change | File to edit | Command | +|--------|-------------|---------| +| Homebrew packages | `dot_Brewfile.tmpl` | `dotfiles edit ~/.Brewfile` | +| Fish abbreviations/env | `config.fish.tmpl` | `dotfiles edit ~/.config/fish/config.fish` | +| Fish function | `functions/NAME.fish` | `dotfiles edit ~/.config/fish/functions/NAME.fish` | +| Starship prompt | `starship.toml` | `dotfiles edit ~/.config/starship.toml` | +| Ghostty terminal | `ghostty/config` | `dotfiles edit ~/.config/ghostty/config` | +| Ghostty theme | any time | `ghostty-theme N` | +| tmux | `tmux/tmux.conf` | `dotfiles edit ~/.config/tmux/tmux.conf` | +| VS Code settings | `code/settings.json` | `dotfiles edit ~/.config/code/settings.json` | +| VS Code extensions | `code/extensions.txt` | edit + `chezmoi apply` | +| Zed settings + MCP | `zed/settings.json.tmpl` | `dotfiles edit ~/.config/zed/settings.json` | +| Git config | `dot_gitconfig.tmpl` | `dotfiles edit ~/.gitconfig` | +| SSH hosts | `dot_ssh/config.d/*` | drop file + `chezmoi apply` | +| macOS defaults | `run_once_after_macos-defaults.sh.tmpl` | edit + `chezmoi apply` | +| Fish plugins | `.chezmoiexternal.toml` | edit + `chezmoi apply --refresh-externals` | +| Setup answers | `~/.config/chezmoi/chezmoi.toml` | `chezmoi init` | +| 1Password secrets | `.chezmoidata/secrets.toml` | `dotfiles secret add VAR op://...` | + +### Walkthrough: add a new fish function + +**Goal:** create a `weather` function that shows the forecast. +**File:** `home/dot_config/fish/functions/weather.fish` (new file) + +```fish +dotfiles edit ~/.config/fish/functions/weather.fish +``` + +In the editor, write: +```fish +function weather --description "Show weather forecast" + curl -s "wttr.in/?format=3" +end +``` + +Save and close. `dotfiles edit` applies (function is immediately available) and +auto-commits. Test it: + +```fish +weather +``` + +**Expected result:** prints something like `Da Nang: ☀️ +31°C`. +The function is available in every new shell. The change is committed. + +### Walkthrough: change your Starship prompt + +**Goal:** shorten the directory display from 3 levels to 2. +**File:** `home/dot_config/starship.toml` + +```fish +dotfiles edit ~/.config/starship.toml +``` + +Edit the `[directory]` section: +```toml +[directory] +truncation_length = 2 +``` + +Save and close. + +**Expected result:** your prompt immediately shows shorter paths +(e.g. `~/w/dotfiles` instead of `~/workspace/tieubao/dotfiles`). +The change is auto-committed. + +### Walkthrough: add a VS Code extension + +**Goal:** install the GitHub Copilot extension and persist it. +**File:** `home/dot_config/code/extensions.txt` + +```fish +dotfiles edit ~/.config/code/extensions.txt +``` + +Add one extension ID per line (e.g. `github.copilot`). Save. Then: + +```fish +chezmoi apply # triggers the VS Code extension sync script +``` + +**Expected result:** VS Code installs the extension. The extensions +list is committed, so your next machine gets it too. + +### Walkthrough: switch Ghostty theme + +**Goal:** preview and switch terminal themes. +No file editing needed — use the built-in helper: + +```fish +ghostty-theme # lists available themes with numbers +ghostty-theme 5 # switch to theme #5 +``` + +**Expected result:** the terminal theme changes immediately (live +reload, no restart). The config file is updated in place. + +### Walkthrough: add an SSH host + +**Goal:** add a `staging` SSH host for quick access. +**File:** `home/dot_ssh/config.d/work-servers` + +```fish +dotfiles edit ~/.ssh/config.d/work-servers +``` + +Write: +``` +Host staging + HostName 10.0.1.50 + User deploy + IdentityFile ~/.ssh/id_ed25519 +``` + +Save and close. + +**Expected result:** `ssh staging` connects to 10.0.1.50. The main +`~/.ssh/config` includes everything in `config.d/` via `Include config.d/*`. +The change is auto-committed. + +--- + +## 5. Secrets management + +### The three tiers + +| Tier | When to use | Command | +|------|------------|---------| +| **Auto-loaded** | Env var in every shell session | `dotfiles secret add VAR "op://..."` | +| **Runtime** | Occasional CLI use, don't pollute every env | `op-env VAR "op://..."` | +| **One-off** | Quick inline use | `set -x VAR (op read "op://...")` | + +### Adding a new secret + +```fish +dotfiles secret add OPENAI_API_KEY "op://Private/OpenAI/credential" +``` + +

+ Secrets workflow +

+ +This command: +1. Creates the 1Password item if it doesn't exist (prompts for the value) +2. Registers the `op://` binding in `secrets.toml` +3. Runs `chezmoi apply` to render `secrets.fish` with the real value +4. Auto-commits the change (only the `op://` ref, never the value) + +Open a new shell (or `exec fish`) to pick up the variable. + +### Rotating a token + +Update the value in 1Password (app or CLI). Then: + +```fish +chezmoi apply ~/.config/fish/conf.d/secrets.fish +exec fish +``` + +No repo change needed — the `op://` reference hasn't changed. + +### Removing a secret + +```fish +dotfiles secret rm OPENAI_API_KEY # unregisters from secrets.toml, auto-commits +``` + +### Listing current secrets + +```fish +dotfiles secret list # shows all registered VAR → op:// bindings +``` + +### How it works under the hood + +

+ Secrets flow: template to machine +

+ +`secrets.toml` is a chezmoi data file loaded as `.secrets` in templates. +`secrets.fish.tmpl` iterates the registry and emits one `set -gx` per +entry, resolving each `op://` ref via `onepasswordRead`. The rendered +file at `~/.config/fish/conf.d/secrets.fish` contains real tokens but +never leaves your machine. + +--- + +## 6. Multi-machine setup + +### Deploying to a second Mac + +On the new machine: + +```bash +git clone https://github.com/dwarvesf/dotfiles ~/dotfiles +cd ~/dotfiles && ./install.sh +``` + +The setup wizard prompts for the same config (name, email, editor, +1Password). If you use 1Password, sign in first (`eval $(op signin)`), +and all secrets are pulled automatically. + +### Headless/server mode + +During `chezmoi init`, answer `headless = true`. This skips: +- GUI apps (Ghostty, VS Code, Zed) +- Mac App Store apps +- Dev casks (Docker, Figma, etc.) +- macOS defaults + +You get: Fish shell, CLI tools, git config, SSH config, tmux. + +### Keeping machines in sync + +On any machine: + +```fish +dotfiles update # git pull + chezmoi apply +``` + +Or manually: + +```fish +cd ~/dotfiles && git pull +chezmoi apply +``` + +### Machine-specific overrides + +chezmoi templates handle per-machine differences. The `.chezmoi` variable +provides hostname, OS, and arch. Example in a `.tmpl` file: + +``` +{{ if eq .chezmoi.hostname "work-mbp" }} +# work-specific config here +{{ end }} +``` + +The `headless` flag is the most common override — it gates entire +sections of the Brewfile and script execution. + +--- + +## 7. Troubleshooting + +Start with `dotfiles doctor` — it catches most issues. + +### "I edited the wrong file" + +You edited `~/.config/fish/config.fish` directly instead of the source. +Your change works now but will be lost on the next `chezmoi apply`. + +**Fix:** run `dotfiles drift` to detect the drift and re-absorb it into the source. + +### "chezmoi apply wants to overwrite my change" + +Same root cause: the deployed file differs from the source. Options: + +1. `dotfiles drift` — pull the deployed version back into source +2. `chezmoi merge ` — three-way merge +3. `chezmoi apply --force` — overwrite deployed with source (destructive) + +### "1Password errors" + +- **"not signed in"**: run `eval (op signin)` or `eval $(op signin)` in bash +- **"item not found"**: check vault name and item title match the `op://` path +- **"connect timeout"**: 1Password CLI needs internet for the first auth + +### "Template rendering failed" + +A `.tmpl` file is missing a variable. Usually means `chezmoi init` needs +to be re-run: + +```fish +chezmoi init +chezmoi apply +``` + +### "Brewfile didn't re-run" + +The brew script only fires when the Brewfile content changes (chezmoi +tracks a hash). Force it: + +```fish +brew bundle --file=~/.Brewfile +``` + +### "Fish function not found" + +The function file must be named exactly `FUNCTIONNAME.fish` in +`~/.config/fish/functions/`. Check: + +```fish +functions --names | grep yourfunction +ls ~/.config/fish/functions/yourfunction.fish +``` + +### "Shell startup is slow" + +Benchmark it: + +```fish +dotfiles bench +``` + +Common causes: 1Password CLI calls on every shell (move to auto-loaded +secrets), too many PATH additions, slow network for async prompt. + +### Nuclear options + +If something is deeply broken: + +```fish +# Re-run setup wizard (re-prompts for all config) +chezmoi init + +# Force apply everything (overwrites all deployed files) +chezmoi apply --force + +# Full reinstall (teardown + rebuild) +cd ~/dotfiles && ./install.sh --force +``` + +--- + +## 8. Architecture reference + +

+ Architecture overview +

+ +### Script execution order + +During `chezmoi apply`, scripts run in this order: + +1. `run_before_aa-init.sh` — resets the apply log +2. `run_before_ab-1password-check.sh` — validates 1Password CLI +3. `run_onchange_before_brew-bundle.sh` — `brew bundle` (triggers on Brewfile change) +4. **File deployment** — templates rendered, files copied +5. `run_once_after_*` — one-time setup (Fish shell, macOS defaults, MAS apps, toolchains) +6. `run_onchange_after_*` — VS Code extensions, Zed config (triggers on content change) +7. `run_after_zz-summary.sh` — styled apply summary with OK/warning/error counts + +`run_once_` scripts won't re-run unless their content changes. +`run_onchange_` scripts re-run when the template output changes. + +### How templates work + +Any file ending in `.tmpl` is rendered through Go's `text/template` +engine at apply time. chezmoi provides variables from: + +- `chezmoi init` answers (`.name`, `.email`, `.editor`, `.headless`, `.use_1password`) +- `.chezmoidata/*.toml` files (`.secrets` from `secrets.toml`) +- Built-in `.chezmoi.*` (hostname, OS, arch, username) + +Every `.tmpl` file validates required variables at the top with +`hasKey`/`fail` guards. Missing variables produce actionable error +messages instead of cryptic Go template errors. + +### External downloads + +Fish plugins and completions are defined in `.chezmoiexternal.toml` as +GitHub URLs. chezmoi downloads and caches them with a 30-day refresh. +Force re-download with `dotfiles refresh` or `chezmoi apply --refresh-externals`. + +### Design decisions + +Architectural choices are documented as ADRs in `docs/decisions/`: + +- [001: chezmoi over GNU Stow](decisions/001-chezmoi-over-stow.md) +- [002: Fish over Zsh](decisions/002-fish-over-zsh.md) +- [003: Ghostty over Kitty](decisions/003-ghostty-over-kitty.md) +- [004: 1Password for secrets](decisions/004-1password-for-secrets.md) +- [005: No plugin manager for Fish](decisions/005-no-plugin-manager-for-fish.md) +- [006: Auto-commit workflow](decisions/006-auto-commit-workflow.md) + +--- + +## Appendix: cheat sheet + +### Commands at a glance + +| I want to... | Command | Abbreviation | +|--------------|---------|-------------| +| **Batch sync everything** | **`/dotfiles-sync` (in Claude Code)** | | +| Edit any config | `dotfiles edit ` | `de` | +| Detect and fix drift | `dotfiles drift` | `dd` | +| Apply all changes | `dotfiles sync` | `ds` | +| Pull latest + apply | `dotfiles update` | `du` | +| See what would change | `dotfiles diff` | | +| Check health | `dotfiles doctor` | | +| Add a Homebrew package | `dotfiles edit ~/.Brewfile` | | +| Add a secret | `dotfiles secret add VAR "op://..."` | | +| List secrets | `dotfiles secret list` | | +| Remove a secret | `dotfiles secret rm VAR` | | +| Rotate a secret | Update in 1Password, then `chezmoi apply` | | +| Add a fish function | `dotfiles edit ~/.config/fish/functions/NAME.fish` | | +| Switch Ghostty theme | `ghostty-theme N` | | +| Benchmark startup | `dotfiles bench` | | +| Backup config | `dotfiles backup` | | +| Re-run setup wizard | `chezmoi init` | | + +### Key file locations + +| What | Source (repo) | Target (machine) | +|------|--------------|-------------------| +| Fish config | `home/dot_config/fish/config.fish.tmpl` | `~/.config/fish/config.fish` | +| Fish functions | `home/dot_config/fish/functions/` | `~/.config/fish/functions/` | +| Ghostty | `home/dot_config/ghostty/config` | `~/.config/ghostty/config` | +| Starship | `home/dot_config/starship.toml` | `~/.config/starship.toml` | +| tmux | `home/dot_config/tmux/tmux.conf` | `~/.config/tmux/tmux.conf` | +| Git | `home/dot_gitconfig.tmpl` | `~/.gitconfig` | +| Brewfile | `home/dot_Brewfile.tmpl` | `~/.Brewfile` | +| Secrets registry | `home/.chezmoidata/secrets.toml` | (data file, not deployed) | +| Rendered secrets | `home/dot_config/fish/conf.d/secrets.fish.tmpl` | `~/.config/fish/conf.d/secrets.fish` | diff --git a/docs/specs/S-31-user-guide.md b/docs/specs/S-31-user-guide.md new file mode 100644 index 0000000..679fadb --- /dev/null +++ b/docs/specs/S-31-user-guide.md @@ -0,0 +1,154 @@ +--- +id: S-31 +title: User guide +type: docs +status: done +--- + +# Comprehensive user guide + +### Problem + +The repo has good docs scattered across multiple files: +- **README.md** — quick start, what's included, daily usage summary +- **docs/customization.md** — the editing workflow, secrets, troubleshooting +- **docs/decisions/*.md** — 6 ADRs explaining architectural choices +- **docs/tool-comparison.md** — why each tool over alternatives +- **7 SVG workflow diagrams** — visual references + +But three user journeys are poorly served: + +1. **Day-1 newcomer**: installed, now staring at a fish prompt. Doesn't know what tools are available, what the keybindings are, what the fish plugins do, or how chezmoi's two-layer model works. README says "what's included" as a table but doesn't orient them. + +2. **Day-2 customizer**: wants to change things but keeps hitting chezmoi's source-vs-target confusion. `customization.md` helps but lacks a guided walkthrough with a real example end-to-end. + +3. **Returning user**: hasn't touched dotfiles in 6 months. Needs a quick refresher on the workflow without re-reading everything. Needs a cheat sheet. + +### Approach + +Expand `docs/customization.md` into `docs/guide.md` — a single comprehensive user guide. Don't create a separate manual that duplicates existing content. The README stays as the "front door" (quick start + overview) and links to the guide for depth. + +### Proposed structure + +``` +docs/guide.md +├── 1. How this works (mental model) +│ ├── The two layers: source vs target +│ ├── What chezmoi does (diagram: dotfiles_chezmoi_model.svg) +│ ├── Where things live (directory map) +│ └── Templates and secrets (diagram: dotfiles_secrets_flow.svg) +│ +├── 2. Your first 30 minutes +│ ├── What just got installed (tool tour with one-liner for each) +│ ├── Fish shell orientation (keybindings, abbreviations, autosuggestions) +│ ├── Terminal: Ghostty basics (theme switching, splits, font) +│ ├── Editor setup check (VS Code / Zed / nvim) +│ └── Verify everything works: dotfiles doctor +│ +├── 3. Daily workflows +│ ├── Editing any config (diagram: dotfiles_dfe_workflow.svg) +│ ├── Adding a Homebrew package (diagram: dotfiles_workflow_brew.svg) +│ ├── Handling drift (diagram: dotfiles_dfs_workflow.svg) +│ ├── The dotfiles CLI (subcommand reference table) +│ └── Cheat sheet (one-page quick reference) +│ +├── 4. Customization cookbook +│ ├── Quick-change table (migrated from customization.md) +│ ├── Walkthrough: add a new fish function +│ ├── Walkthrough: change your Starship prompt +│ ├── Walkthrough: add a new Homebrew package +│ ├── Walkthrough: add a VS Code extension +│ ├── Walkthrough: switch Ghostty theme +│ └── Walkthrough: add an SSH host +│ +├── 5. Secrets management +│ ├── The three tiers (auto-loaded, runtime, one-off) +│ ├── Adding a new secret (diagram: dotfiles_workflow_secrets.svg) +│ ├── Rotating a token +│ ├── Removing a secret +│ └── How it works under the hood +│ +├── 6. Multi-machine setup +│ ├── Deploying to a second Mac +│ ├── Headless/server mode +│ ├── Keeping machines in sync (git pull + apply) +│ └── Machine-specific overrides (chezmoi templates) +│ +├── 7. Troubleshooting +│ ├── Common issues (migrated + expanded from customization.md) +│ ├── "I edited the wrong file" +│ ├── "chezmoi apply wants to overwrite my change" +│ ├── "1Password errors" +│ ├── "Template rendering failed" +│ └── Nuclear options (reinit, state reset) +│ +├── 8. Architecture reference +│ ├── Directory layout (diagram: dotfiles_architecture.svg) +│ ├── Script execution order +│ ├── How templates work +│ ├── How external downloads work +│ └── Design decisions (links to ADRs) +│ +└── Appendix: cheat sheet + ├── Commands at a glance (1-page table) + ├── File locations + └── "I want to X" → command mapping +``` + +### What gets migrated vs written new + +| Section | Source | Work needed | +|---------|--------|-------------| +| Mental model | New | Write ~300 words wrapping the chezmoi model diagram | +| First 30 minutes | New | Write tool tour, fish orientation, editor check | +| Daily workflows | customization.md (partial) | Restructure + add diagrams | +| Customization cookbook | customization.md quick-change table | Migrate table + write 6 walkthroughs | +| Secrets management | customization.md secrets section | Migrate + add diagrams | +| Multi-machine | New | Write ~400 words | +| Troubleshooting | customization.md troubleshooting | Migrate + expand | +| Architecture | CLAUDE.md + README | Extract into reader-friendly prose | +| Cheat sheet | New | Compile from all sections | + +### What happens to existing docs + +- `docs/customization.md` → **deleted**, content migrated to `docs/guide.md` +- `README.md` → **updated**: "Customization" section links to guide instead of duplicating +- `docs/decisions/*.md` → **unchanged**, linked from guide section 8 +- `docs/tool-comparison.md` → **unchanged**, linked from guide section 2 + +### Diagrams placement + +| Diagram | Section | +|---------|---------| +| `dotfiles_chezmoi_model.svg` | 1. Mental model | +| `dotfiles_bootstrap_flow.svg` | 1. Mental model (or section 8) | +| `dotfiles_secrets_flow.svg` | 1. Mental model + 5. Secrets | +| `dotfiles_dfe_workflow.svg` | 3. Daily workflows | +| `dotfiles_dfs_workflow.svg` | 3. Daily workflows | +| `dotfiles_workflow_brew.svg` | 3. Daily workflows + 4. Cookbook | +| `dotfiles_workflow_config.svg` | 3. Daily workflows | +| `dotfiles_workflow_secrets.svg` | 5. Secrets management | +| `dotfiles_architecture.svg` | 8. Architecture | + +### Acceptance criteria + +- [ ] `docs/guide.md` exists with all 8 sections + appendix +- [ ] `docs/customization.md` removed, no broken links +- [ ] README "Customization" section links to `docs/guide.md` +- [ ] All 9 SVG diagrams embedded in appropriate sections +- [ ] Each walkthrough in section 4 has: goal, file to edit, exact command, expected result +- [ ] Cheat sheet fits in one screen (terminal height) +- [ ] No content duplication between README and guide (README = overview, guide = depth) +- [ ] `docs/guide.md` reads well as a standalone doc (no assumed context from README) +- [ ] Troubleshooting covers at least 8 common issues +- [ ] Multi-machine section covers fresh Mac, headless server, and sync workflow + +### Estimated size + +~2000-2500 words (excluding diagrams). Target: 15-minute read for the full guide, 2-minute skim via cheat sheet. + +### Non-goals + +- Video/screencast (that's S-29: VHS demo) +- Contributing guide (not needed yet, single-user repo) +- Plugin development guide (no custom plugin system) diff --git a/docs/specs/S-32-claude-assisted-sync.md b/docs/specs/S-32-claude-assisted-sync.md new file mode 100644 index 0000000..4949c95 --- /dev/null +++ b/docs/specs/S-32-claude-assisted-sync.md @@ -0,0 +1,301 @@ +--- +id: S-32 +title: Claude-assisted dotfiles sync +type: workflow +status: planned +--- + +# Claude-assisted dotfiles sync + +## The core idea + +Most dotfiles setups assume the user will manually keep the repo in sync +with their machine. Edit the source, run apply, commit, push. In +practice, nobody does this consistently. You `brew install` something in +the heat of debugging, tweak a config file directly, add an API key for +a new tool. The changes accumulate on your machine but never make it +back to the repo. After a few weeks, the repo is stale. + +The idea here is different. Instead of expecting the user to maintain +the repo, **the LLM maintains it.** You operate your machine naturally. +Periodically (weekly, or whenever you think of it), you ask Claude to +catch up. Claude scans your machine, detects everything that drifted, +reports it in plain language, and waits for your decisions. Then Claude +syncs the changes back into the chezmoi source, commits, and pushes. + +This is the same pattern as the LLM Wiki: the human curates and +decides, the LLM does the bookkeeping. The tedious part of dotfiles +management is not choosing your tools — it's keeping the repo in sync +with your choices. Claude handles that. + +## Architecture + +Three layers, mirroring the LLM Wiki: + +**Machine state** (the "raw sources") — what's actually installed and +configured on your Mac right now. Brew packages, cask apps, config +files, secrets, shell functions. This is the source of truth. You +change it freely by installing tools, editing configs, whatever. + +**The repo** (the "wiki") — the chezmoi source at `~/dotfiles/home/`. +This is the persistent artifact that Claude maintains. It should reflect +your machine state, but it often lags behind. Claude's job is to close +the gap. + +**The schema** (the "CLAUDE.md") — instructions that tell Claude how to +scan, what to detect, how to report, and how to sync. Lives in +`.claude/commands/dotfiles-sync.md` as a slash command, plus detection +logic in the CLAUDE.md project instructions. + +## Operations + +### Scan (detect) + +Claude runs detection across multiple dimensions: + +| Dimension | Detection method | What it finds | +|-----------|-----------------|---------------| +| **Config drift** | `chezmoi status` | Managed files where deployed differs from source (Ghostty, Starship, tmux, Git, Zed, SSH, fish config, etc.) | +| **New brew packages** | `brew leaves` vs `dot_Brewfile.tmpl` | Packages you installed but didn't track | +| **Removed brew packages** | `dot_Brewfile.tmpl` vs `brew leaves` | Packages in Brewfile but no longer installed | +| **New casks** | `brew list --cask` vs `dot_Brewfile.tmpl` | GUI apps you installed but didn't track | +| **Removed casks** | `dot_Brewfile.tmpl` vs `brew list --cask` | Casks in Brewfile but no longer installed | +| **VS Code extensions** | `code --list-extensions` vs `extensions.txt` | New or removed extensions | +| **New fish functions** | `ls ~/.config/fish/functions/` vs `chezmoi managed` | Functions created outside chezmoi | +| **New SSH config** | `ls ~/.ssh/config.d/` vs `chezmoi managed` | SSH host configs added directly | +| **Secrets** | `secrets.toml` entries vs `op read` | Stale or broken secret refs | +| **Env vars** | Scan fish config for `set -gx` with hardcoded keys | API keys that should be in 1Password | + +Note: `chezmoi status` covers most config drift (any file chezmoi +already manages). The "new files" checks cover the gap: files that +exist on the machine but chezmoi doesn't know about yet. + +macOS defaults (`defaults write`) are not detectable — those are +fire-and-forget commands with no clean diff mechanism. + +Each scan produces a structured finding. Claude collects all findings +into a plain-language report. + +### Report (review) + +Claude presents findings grouped by category, not as raw diffs. Example: + +``` +Dotfiles sync report (2026-04-14) + +Config drift (1 file): + - Zed settings.json — modified outside chezmoi + (MCP server config changed, 2 new servers added) + +New packages (25 brew, 10 casks): + Brew: chezmoi, ollama, rclone, pandoc, opencode, ... + Cask: claude, cursor, calibre, codexbar, ... + +Stale entries (9 brew, 9 casks): + Brew: age, btop, caddy, ... (in Brewfile but not installed) + Cask: raycast, slack, meetingbar, ... (in Brewfile but not installed) + +New VS Code extensions (3): + github.copilot-chat, ms-python.debugpy, ... + +No secret drift detected. + +What would you like me to do? +``` + +The report is conversational. Claude may add context: "ollama was +probably installed for local LLM testing" or "raycast might have been +replaced by spotlight." The user decides what to sync. + +### Sync (act) + +Based on the user's decisions, Claude executes: + +| Action | Method | +|--------|--------| +| Absorb config drift | `chezmoi re-add ` | +| Add brew packages to Brewfile | Edit `dot_Brewfile.tmpl`, add `brew "pkg"` lines | +| Remove stale Brewfile entries | Edit `dot_Brewfile.tmpl`, delete lines | +| Add casks to Brewfile | Edit `dot_Brewfile.tmpl`, add `cask "app"` lines | +| Remove stale cask entries | Edit `dot_Brewfile.tmpl`, delete lines | +| Sync VS Code extensions | Update `extensions.txt` | +| Register new secrets | Append to `secrets.toml` | +| Track new fish functions | `chezmoi add ` | + +Each action modifies the chezmoi source. Claude batches related changes +and commits with a descriptive message: + +``` +chore(sync): weekly dotfiles sync + +Config: + - re-add Zed settings (2 new MCP servers) + +Brewfile: + - add 25 packages: chezmoi, ollama, rclone, ... + - add 10 casks: claude, cursor, calibre, ... + - remove 9 stale packages: age, btop, ... + - remove 9 stale casks: raycast, slack, ... + +VS Code: + - add 3 extensions +``` + +### Log (record) + +After every sync, Claude appends an entry to `docs/sync-log.md`. This +is an append-only chronological record of what changed and when. + +```markdown +## [2026-04-14] sync + +Config: + - re-add Zed settings (2 new MCP servers) + +Brewfile: + - add 25 packages: chezmoi, ollama, rclone, ... + - remove 9 stale: raycast, slack, ... + +Secrets: + - no changes + +--- +``` + +The log serves two purposes: + +**Context for future syncs.** Claude reads the log at the start of each +session. "Last sync was 2 weeks ago, you added ollama and cursor." This +helps Claude spot patterns ("you keep installing ML tools, want me to +add a comment group in the Brewfile?") and ask smarter questions. + +**Audit trail.** "When did I add rclone?" Grep the log instead of +digging through git history. Each entry starts with a consistent +`## [date] sync` prefix so it's parseable with unix tools. + +The log is committed alongside the sync changes. It's part of the repo, +not ephemeral. + +### Publish (push) + +Claude asks before pushing. The user can review the commit, amend, or +just say "push it." + +## The sync session flow + +A typical sync session looks like this: + +``` +User: /dotfiles-sync +Claude: [runs scan, produces report] +Claude: "Here's what I found. What should I do?" +User: "Add the new packages. Drop raycast and slack, I use Spotlight now. + Keep btop in the Brewfile even though it's not installed, I want it + on my next machine. Sync the Zed config." +Claude: [executes sync, commits] +Claude: "Done. 1 commit ready. Push?" +User: "Push." +Claude: [pushes] +``` + +The user speaks in plain language. Claude translates to chezmoi/git +operations. No commands to remember. + +## What happens to existing commands + +The manual commands (`dfe`, `dfs`, `add-secret`, etc.) stay in the repo +as **escape hatches** for when you're not in a Claude session: + +| Scenario | Tool | +|----------|------| +| Weekly sync, batch changes | `/dotfiles-sync` (Claude) | +| Quick edit during a Claude session | Ask Claude directly | +| On a plane, no Claude | `dfe`, `dfs`, `dotfiles` CLI | +| SSH into a server | `dotfiles` CLI | +| CI/CD | `chezmoi apply` directly | + +The guide and README reframe: Claude-assisted sync is the primary +workflow. Manual commands are the fallback for offline/headless use. + +## What to build + +### Phase 1: the slash command (MVP) + +| Artifact | Purpose | +|----------|---------| +| `.claude/commands/dotfiles-sync.md` | Slash command prompt template. Tells Claude what to scan, how to report, how to sync. | +| `docs/sync-log.md` | Append-only sync history. Created on first sync. | + +The slash command is the core artifact. It instructs Claude to: + +1. Read `docs/sync-log.md` for context on last sync +2. Run detection commands (chezmoi status, brew leaves, code --list-extensions, ls key dirs, etc.) +3. Diff against the repo state +4. Format a plain-language report +5. Wait for user decisions +6. Execute sync actions +7. Append to `docs/sync-log.md` +8. Commit and optionally push + +No shell scripts needed. Claude runs the detection commands directly +via Bash tool. The intelligence is in the prompt, not in code. + +### Phase 2: scan helper (optional optimization) + +If the scan step takes too long interactively, extract it into a fish +function that outputs structured JSON: + +```fish +dotfiles scan # outputs machine state as JSON +``` + +Claude reads the JSON instead of running 8 separate commands. This is +an optimization, not a requirement. Start without it. + +### Phase 3: scheduled sync (optional) + +Use Claude Code's cron/schedule feature to run the scan weekly and +notify the user if drift is detected. The user can then start a sync +session or ignore it. + +## Design principles + +**Claude does the bookkeeping, you make the decisions.** Claude never +auto-syncs without asking. It scans, reports, and waits. You decide +what to sync, what to drop, what to keep. + +**Plain language over commands.** You say "drop raycast, I switched to +Spotlight." Claude figures out which line to delete from the Brewfile. + +**One sync session covers everything.** Brew packages, casks, configs, +extensions, secrets — all in one pass. No separate workflows for +different types of drift. + +**The repo is the persistent artifact.** Like the LLM Wiki, the repo +compounds over time. Every sync session makes it more accurate. Claude +writes good commit messages so the history is readable. + +**No daemon, no watcher, no automation.** You trigger the sync when you +want it. This is intentional: dotfiles sync should be a conscious +decision, not a background process that might commit garbage. + +## Acceptance criteria + +- [ ] `.claude/commands/dotfiles-sync.md` exists +- [ ] Running `/dotfiles-sync` produces a scan report +- [ ] Report covers: config drift, brew packages, casks, VS Code extensions, new fish functions, new SSH configs, secrets, env vars +- [ ] Claude reads `docs/sync-log.md` for context at start of scan +- [ ] Claude waits for user decisions before making changes +- [ ] Sync modifies the correct chezmoi source files +- [ ] Commit message summarizes all changes by category +- [ ] Claude appends entry to `docs/sync-log.md` after sync +- [ ] Claude asks before pushing +- [ ] Works as a normal conversation too ("catch up with my dotfiles") +- [ ] Guide updated to describe Claude-assisted sync as primary workflow + +## Non-goals + +- Automated/scheduled sync (Phase 3, not MVP) +- `dotfiles scan` fish helper (Phase 2, not MVP) +- Syncing across multiple machines (separate concern) +- Replacing `chezmoi apply` (that's deployment, not sync) diff --git a/docs/sync-log.md b/docs/sync-log.md new file mode 100644 index 0000000..2f4b0a7 --- /dev/null +++ b/docs/sync-log.md @@ -0,0 +1,22 @@ +# Dotfiles sync log + +Append-only record of Claude-assisted sync sessions. Each entry logs +what changed and when. Read by Claude at the start of each sync for +context. + +--- + +## [2026-04-14] sync + +Config: + - re-add Zed settings.json (MCP server changes, local edits absorbed) + +VS Code extensions: + - add 5: docker.docker, dwarvesf.md-ar-ext, ms-vsliveshare.vsliveshare, ocamllabs.ocaml-platform, openai.openai-chatgpt-adhoc + +Fish functions: + - removed 5 orphaned standalone functions from machine (add-secret, dfe, dfs, list-secrets, rm-secret) — consolidated into dotfiles subcommands + +Brew/casks: deferred to next sync + +--- diff --git a/docs/tasks.md b/docs/tasks.md index 85ee454..988afce 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -1,6 +1,6 @@ # Task Backlog: dwarvesf/dotfiles -Updated: 2026-04-04 +Updated: 2026-04-14 @@ -29,6 +29,8 @@ Updated: 2026-04-04 - [x] S-21: Consolidate toolchain scripts into install-toolchains.sh - [x] S-22: Gum TUI onboarding - [x] S-23: Error message system (gum-styled output, template guards, apply summary) +- [x] Data-driven secret registry (secrets.toml, add-secret, rm-secret, list-secrets) +- [x] Auto-commit workflow (dfe auto-commits, dfs reverse drift sync, ADR-006) ## Next up @@ -39,6 +41,8 @@ Updated: 2026-04-04 - [x] S-28: README tool showcase icons — skillicons.dev + shields.io badges - [ ] S-29: VHS terminal demo — animated GIF of install wizard - [x] S-30: Verification loop — CLAUDE.md rules, subagent, hooks, slash command +- [x] S-31: User guide — comprehensive manual replacing customization.md +- [ ] S-32: Claude-assisted dotfiles sync — LLM scans drift, reports, syncs on approval ## Backlog (no immediate plans) diff --git a/home/dot_config/code/extensions.txt b/home/dot_config/code/extensions.txt index 555676a..31c4f2f 100644 --- a/home/dot_config/code/extensions.txt +++ b/home/dot_config/code/extensions.txt @@ -1,6 +1,8 @@ anthropic.claude-code azemoh.one-monokai davidanson.vscode-markdownlint +docker.docker +dwarvesf.md-ar-ext github.codespaces github.vscode-github-actions golang.go @@ -19,5 +21,8 @@ ms-vscode-remote.remote-ssh ms-vscode-remote.remote-ssh-edit ms-vscode.makefile-tools ms-vscode.remote-explorer +ms-vsliveshare.vsliveshare +ocamllabs.ocaml-platform +openai.openai-chatgpt-adhoc pgourlain.erlang shopify.ruby-lsp diff --git a/home/dot_config/fish/completions/dotfiles.fish b/home/dot_config/fish/completions/dotfiles.fish index c34d49d..4dfa8b9 100644 --- a/home/dot_config/fish/completions/dotfiles.fish +++ b/home/dot_config/fish/completions/dotfiles.fish @@ -1,5 +1,7 @@ complete -c dotfiles -f -complete -c dotfiles -n "__fish_use_subcommand" -a "edit" -d "Edit managed file" +complete -c dotfiles -n "__fish_use_subcommand" -a "edit" -d "Edit + apply + auto-commit" +complete -c dotfiles -n "__fish_use_subcommand" -a "drift" -d "Detect and re-absorb drifted files" +complete -c dotfiles -n "__fish_use_subcommand" -a "secret" -d "Manage 1Password secrets" complete -c dotfiles -n "__fish_use_subcommand" -a "diff" -d "Show pending changes" complete -c dotfiles -n "__fish_use_subcommand" -a "sync" -d "Apply all changes" complete -c dotfiles -n "__fish_use_subcommand" -a "status" -d "Show status" @@ -11,3 +13,15 @@ complete -c dotfiles -n "__fish_use_subcommand" -a "doctor" -d "Health check" complete -c dotfiles -n "__fish_use_subcommand" -a "bench" -d "Benchmark shell startup" complete -c dotfiles -n "__fish_use_subcommand" -a "backup" -d "Back up config + age key" complete -c dotfiles -n "__fish_use_subcommand" -a "encrypt-setup" -d "Set up age encryption" + +# dotfiles edit accepts file paths +complete -c dotfiles -n "__fish_seen_subcommand_from edit" -a "--no-commit" -d "Skip auto-commit" +complete -c dotfiles -n "__fish_seen_subcommand_from edit" -F + +# dotfiles drift +complete -c dotfiles -n "__fish_seen_subcommand_from drift" -a "--no-commit" -d "Skip auto-commit" + +# dotfiles secret subcommands +complete -c dotfiles -n "__fish_seen_subcommand_from secret; and not __fish_seen_subcommand_from add rm list ls" -a "add" -d "Register a secret" +complete -c dotfiles -n "__fish_seen_subcommand_from secret; and not __fish_seen_subcommand_from add rm list ls" -a "rm" -d "Unregister a secret" +complete -c dotfiles -n "__fish_seen_subcommand_from secret; and not __fish_seen_subcommand_from add rm list ls" -a "list" -d "Show all bindings" diff --git a/home/dot_config/fish/config.fish.tmpl b/home/dot_config/fish/config.fish.tmpl index cabdbb3..913da2b 100644 --- a/home/dot_config/fish/config.fish.tmpl +++ b/home/dot_config/fish/config.fish.tmpl @@ -103,6 +103,12 @@ if status is-interactive abbr -a dc "docker compose" abbr -a tf terraform + # ── Dotfiles ───────────────────────────────────────────────────── + abbr -a de "dotfiles edit" + abbr -a dd "dotfiles drift" + abbr -a ds "dotfiles sync" + abbr -a du "dotfiles update" + # ── tmux ────────────────────────────────────────────────────────── abbr -a tx tmux abbr -a tml "tmux list-sessions" diff --git a/home/dot_config/fish/functions/add-secret.fish b/home/dot_config/fish/functions/add-secret.fish deleted file mode 100644 index 2d42f74..0000000 --- a/home/dot_config/fish/functions/add-secret.fish +++ /dev/null @@ -1,87 +0,0 @@ -function add-secret --description "Register a 1Password secret as an auto-loaded env var" - if test (count $argv) -lt 2 - echo "Usage: add-secret VAR_NAME \"op://Vault/Item/field\" [--no-commit]" - echo "Example: add-secret OPENAI_API_KEY \"op://Private/OpenAI/credential\"" - echo "" - echo "If the 1Password item doesn't exist yet you'll be prompted for the" - echo "value and the item will be created automatically." - return 1 - end - - set -l var $argv[1] - set -l ref $argv[2] - set -l do_commit 1 - contains -- --no-commit $argv; and set do_commit 0 - - if not string match -qr '^[A-Z_][A-Z0-9_]*$' -- $var - echo "✗ VAR_NAME must be UPPER_SNAKE_CASE, got: $var" - return 1 - end - - # Parse op://Vault/Item/field — the Item segment may contain spaces. - set -l parts (string split / -- (string replace 'op://' '' -- $ref)) - if test (count $parts) -lt 3 - echo "✗ reference must be op://Vault/Item/field , got: $ref" - return 1 - end - set -l op_vault $parts[1] - set -l op_field $parts[-1] - set -l op_item (string join / $parts[2..-2]) - - # Create the item if it doesn't exist yet. - if not op read "$ref" >/dev/null 2>&1 - echo "No item found at $ref — creating it now." - if not op account list >/dev/null 2>&1 - echo "✗ 1Password CLI is not signed in. Run: eval (op signin)" - return 1 - end - read -s -P "Enter value for $var: " value - echo "" - if test -z "$value" - echo "✗ empty value, aborting" - return 1 - end - if not op item create --vault="$op_vault" --category="API Credential" \ - --title="$op_item" "$op_field=$value" >/dev/null 2>&1 - echo "✗ op item create failed (vault=$op_vault title=$op_item)" - return 1 - end - echo "✓ created 1Password item: $op_item" - if not op read "$ref" >/dev/null 2>&1 - echo "✗ item created but $ref still unreadable; check field name" - return 1 - end - end - - set -l data (chezmoi source-path)/.chezmoidata/secrets.toml - if test ! -f $data - echo "✗ $data missing; dotfiles may need reinstall" - return 1 - end - - if grep -q "^$var = " $data - echo "⚠ $var already registered; use rm-secret first to replace" - return 1 - end - - printf '%s = "%s"\n' "$var" "$ref" >> $data - echo "✓ added $var → $ref" - - echo "→ chezmoi apply" - chezmoi apply; or begin - echo "✗ chezmoi apply failed; reverting registry" - sed -i '' "/^$var = /d" $data - return 1 - end - - echo "✓ applied. Open a new shell (or `exec fish`) to load \$$var." - - set -l repo (dirname (chezmoi source-path)) - if test $do_commit -eq 1 - git -C $repo add .chezmoidata/secrets.toml - git -C $repo commit -m "feat(secrets): register $var" >/dev/null - echo "✓ committed. Push with: git -C $repo push" - else - echo "Commit: git -C $repo add .chezmoidata/secrets.toml && git commit -m 'feat(secrets): register $var'" - end -end diff --git a/home/dot_config/fish/functions/dfe.fish b/home/dot_config/fish/functions/dfe.fish deleted file mode 100644 index 71af6c0..0000000 --- a/home/dot_config/fish/functions/dfe.fish +++ /dev/null @@ -1,38 +0,0 @@ -function dfe --description "Edit a managed dotfile, apply on save, commit the change" - if test (count $argv) -lt 1 - echo "Usage: dfe [--no-commit]" - echo " Edits the source, applies on save, auto-commits the diff." - echo " Pass --no-commit to leave the change unstaged." - return 1 - end - - set -l no_commit 0 - set -l paths - for a in $argv - switch $a - case --no-commit - set no_commit 1 - case '*' - set -a paths $a - end - end - - chezmoi edit --apply $paths; or return 1 - - if test $no_commit -eq 1 - return 0 - end - - set -l repo (dirname (chezmoi source-path)) - if not git -C $repo diff --quiet -- home/ - # Stage only the files inside home/ that changed. - set -l changed (git -C $repo diff --name-only -- home/) - if test (count $changed) -gt 0 - git -C $repo add $changed - set -l summary (string join ", " (for p in $changed; basename $p; end)) - git -C $repo commit -m "chore(config): update $summary via dfe" >/dev/null - echo "✓ committed: $summary" - echo " push with: git -C $repo push" - end - end -end diff --git a/home/dot_config/fish/functions/dfs.fish b/home/dot_config/fish/functions/dfs.fish deleted file mode 100644 index bcef4b5..0000000 --- a/home/dot_config/fish/functions/dfs.fish +++ /dev/null @@ -1,48 +0,0 @@ -function dfs --description "Sync drifted deployed files back into the dotfiles source" - set -l no_commit 0 - contains -- --no-commit $argv; and set no_commit 1 - - # chezmoi status columns: "MX ~/path" where M=deployed differs from source. - # A=missing from target, R=would be run script. We only care about M. - set -l drifted (chezmoi status 2>/dev/null | string match -r '^ M\s+(.+)$' | string replace -r '^ M\s+' '') - # string match/replace pipeline returns matches + replacements; take odd indices. - set -l paths - set -l i 2 - while test $i -le (count $drifted) - set -a paths $drifted[$i] - set i (math $i + 2) - end - - if test (count $paths) -eq 0 - echo "✓ no drift — deployed files match source" - return 0 - end - - echo "Drifted files (deployed ≠ source):" - for p in $paths - echo " $p" - end - echo "" - echo "Run 'chezmoi diff $paths[1]' to preview; 'dfs' will re-absorb all of them." - read -P "Re-absorb into source? [y/N] " ans - if not string match -qri '^y' -- $ans - echo "aborted" - return 1 - end - - chezmoi re-add $paths; or return 1 - echo "✓ source updated" - - if test $no_commit -eq 1 - return 0 - end - - set -l repo (dirname (chezmoi source-path)) - if not git -C $repo diff --quiet -- home/ - set -l changed (git -C $repo diff --name-only -- home/) - git -C $repo add $changed - set -l summary (string join ", " (for p in $changed; basename $p; end)) - git -C $repo commit -m "chore(config): sync drift from machine ($summary)" >/dev/null - echo "✓ committed. Push with: git -C $repo push" - end -end diff --git a/home/dot_config/fish/functions/dotfiles.fish b/home/dot_config/fish/functions/dotfiles.fish index 84ece19..5ddd9ba 100644 --- a/home/dot_config/fish/functions/dotfiles.fish +++ b/home/dot_config/fish/functions/dotfiles.fish @@ -1,7 +1,224 @@ function dotfiles -d "Manage dotfiles via chezmoi" switch $argv[1] case edit e - chezmoi edit $argv[2..] + if test (count $argv) -lt 2 + echo "Usage: dotfiles edit [--no-commit]" + echo " Edits the source, applies on save, auto-commits the diff." + return 1 + end + + set -l no_commit 0 + set -l paths + for a in $argv[2..] + switch $a + case --no-commit + set no_commit 1 + case '*' + set -a paths $a + end + end + + chezmoi edit --apply $paths; or return 1 + + if test $no_commit -eq 1 + return 0 + end + + set -l repo (dirname (chezmoi source-path)) + if not git -C $repo diff --quiet -- home/ + set -l changed (git -C $repo diff --name-only -- home/) + if test (count $changed) -gt 0 + git -C $repo add $changed + set -l summary (string join ", " (for p in $changed; basename $p; end)) + git -C $repo commit -m "chore(config): update $summary via dotfiles edit" >/dev/null + echo "✓ committed: $summary" + echo " push with: git -C $repo push" + end + end + + case drift + set -l no_commit 0 + contains -- --no-commit $argv; and set no_commit 1 + + set -l drifted (chezmoi status 2>/dev/null | string match -r '^ M\s+(.+)$' | string replace -r '^ M\s+' '') + set -l paths + set -l i 2 + while test $i -le (count $drifted) + set -a paths $drifted[$i] + set i (math $i + 2) + end + + if test (count $paths) -eq 0 + echo "✓ no drift — deployed files match source" + return 0 + end + + echo "Drifted files (deployed ≠ source):" + for p in $paths + echo " $p" + end + echo "" + echo "Run 'chezmoi diff $paths[1]' to preview; 'dotfiles drift' will re-absorb all of them." + read -P "Re-absorb into source? [y/N] " ans + if not string match -qri '^y' -- $ans + echo "aborted" + return 1 + end + + chezmoi re-add $paths; or return 1 + echo "✓ source updated" + + if test $no_commit -eq 1 + return 0 + end + + set -l repo (dirname (chezmoi source-path)) + if not git -C $repo diff --quiet -- home/ + set -l changed (git -C $repo diff --name-only -- home/) + git -C $repo add $changed + set -l summary (string join ", " (for p in $changed; basename $p; end)) + git -C $repo commit -m "chore(config): sync drift from machine ($summary)" >/dev/null + echo "✓ committed. Push with: git -C $repo push" + end + + case secret + switch $argv[2] + case add + if test (count $argv) -lt 4 + echo "Usage: dotfiles secret add VAR_NAME \"op://Vault/Item/field\" [--no-commit]" + echo "Example: dotfiles secret add OPENAI_API_KEY \"op://Private/OpenAI/credential\"" + echo "" + echo "If the 1Password item doesn't exist yet you'll be prompted for the" + echo "value and the item will be created automatically." + return 1 + end + + set -l var $argv[3] + set -l ref $argv[4] + set -l do_commit 1 + contains -- --no-commit $argv; and set do_commit 0 + + if not string match -qr '^[A-Z_][A-Z0-9_]*$' -- $var + echo "✗ VAR_NAME must be UPPER_SNAKE_CASE, got: $var" + return 1 + end + + set -l parts (string split / -- (string replace 'op://' '' -- $ref)) + if test (count $parts) -lt 3 + echo "✗ reference must be op://Vault/Item/field , got: $ref" + return 1 + end + set -l op_vault $parts[1] + set -l op_field $parts[-1] + set -l op_item (string join / $parts[2..-2]) + + if not op read "$ref" >/dev/null 2>&1 + echo "No item found at $ref — creating it now." + if not op account list >/dev/null 2>&1 + echo "✗ 1Password CLI is not signed in. Run: eval (op signin)" + return 1 + end + read -s -P "Enter value for $var: " value + echo "" + if test -z "$value" + echo "✗ empty value, aborting" + return 1 + end + if not op item create --vault="$op_vault" --category="API Credential" \ + --title="$op_item" "$op_field=$value" >/dev/null 2>&1 + echo "✗ op item create failed (vault=$op_vault title=$op_item)" + return 1 + end + echo "✓ created 1Password item: $op_item" + if not op read "$ref" >/dev/null 2>&1 + echo "✗ item created but $ref still unreadable; check field name" + return 1 + end + end + + set -l data (chezmoi source-path)/.chezmoidata/secrets.toml + if test ! -f $data + echo "✗ $data missing; dotfiles may need reinstall" + return 1 + end + + if grep -q "^$var = " $data + echo "⚠ $var already registered; use 'dotfiles secret rm' first to replace" + return 1 + end + + printf '%s = "%s"\n' "$var" "$ref" >> $data + echo "✓ added $var → $ref" + + echo "→ chezmoi apply" + chezmoi apply; or begin + echo "✗ chezmoi apply failed; reverting registry" + sed -i '' "/^$var = /d" $data + return 1 + end + + echo "✓ applied. Open a new shell (or `exec fish`) to load \$$var." + + set -l repo (dirname (chezmoi source-path)) + if test $do_commit -eq 1 + git -C $repo add .chezmoidata/secrets.toml + git -C $repo commit -m "feat(secrets): register $var" >/dev/null + echo "✓ committed. Push with: git -C $repo push" + else + echo "Commit: git -C $repo add .chezmoidata/secrets.toml && git commit -m 'feat(secrets): register $var'" + end + + case rm + if test (count $argv) -lt 3 + echo "Usage: dotfiles secret rm VAR_NAME [--no-commit]" + return 1 + end + + set -l var $argv[3] + set -l do_commit 1 + contains -- --no-commit $argv; and set do_commit 0 + + set -l data (chezmoi source-path)/.chezmoidata/secrets.toml + if not grep -q "^$var = " $data + echo "⚠ $var not registered" + return 1 + end + + sed -i '' "/^$var = /d" $data + echo "✓ removed $var" + + echo "→ chezmoi apply" + chezmoi apply; or return 1 + echo "✓ applied. \$$var will be absent from new shells." + + if test $do_commit -eq 1 + set -l repo (dirname (chezmoi source-path)) + git -C $repo add .chezmoidata/secrets.toml + git -C $repo commit -m "chore(secrets): unregister $var" >/dev/null + echo "✓ committed." + end + + case list ls + set -l data (chezmoi source-path)/.chezmoidata/secrets.toml + if test ! -f $data + echo "(no secrets registered)" + return 0 + end + grep -E '^[A-Z_][A-Z0-9_]* = ' $data | sed 's/ = / → /; s/"//g' + + case '' + echo "Usage: dotfiles secret " + echo "" + echo " add VAR \"op://...\" Register a secret" + echo " rm VAR Unregister a secret" + echo " list Show all bindings" + + case '*' + echo "Unknown secret command: $argv[2]" + echo "Usage: dotfiles secret " + return 1 + end + case diff d chezmoi diff --no-pager case sync s @@ -26,7 +243,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" echo "=====================" echo "" - # chezmoi if command -q chezmoi echo "[ok] chezmoi installed" else @@ -34,7 +250,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" set issues (math $issues + 1) end - # Source link if test -L ~/.local/share/chezmoi echo "[ok] chezmoi source linked" else @@ -42,7 +257,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" set issues (math $issues + 1) end - # Fish is default shell if string match -q "*/fish" $SHELL echo "[ok] fish is default shell" else @@ -50,7 +264,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" set issues (math $issues + 1) end - # Homebrew if command -q brew echo "[ok] homebrew installed" else @@ -58,7 +271,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" set issues (math $issues + 1) end - # 1Password CLI if command -q op if op account list &>/dev/null echo "[ok] 1Password CLI: signed in" @@ -69,14 +281,12 @@ function dotfiles -d "Manage dotfiles via chezmoi" echo "[--] 1Password CLI: not installed (optional)" end - # 1Password SSH Agent if test -e "$HOME/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock" echo "[ok] 1Password SSH agent: socket exists" else echo "[--] 1Password SSH agent: not found (optional)" end - # Key config files for f in ~/.gitconfig ~/.config/fish/config.fish ~/.ssh/config if test -f $f echo "[ok] $f exists" @@ -86,7 +296,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" end end - # Git identity set -l git_name (git config --global user.name 2>/dev/null) if test -n "$git_name" echo "[ok] git identity: $git_name <"(git config --global user.email)">" @@ -95,7 +304,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" set issues (math $issues + 1) end - # Key CLI tools for tool in fzf bat eza zoxide delta mise starship if command -q $tool echo "[ok] $tool" @@ -105,14 +313,12 @@ function dotfiles -d "Manage dotfiles via chezmoi" end end - # Age key if test -f ~/.config/chezmoi/key.txt echo "[ok] age encryption key exists" else echo "[--] age encryption key: not set up (optional)" end - # Drift set -l drift_count (chezmoi diff --no-pager 2>/dev/null | grep '^diff' | wc -l | string trim) if test "$drift_count" -gt 0 echo "[!!] $drift_count file(s) have drifted from source" @@ -172,7 +378,6 @@ function dotfiles -d "Manage dotfiles via chezmoi" echo " chezmoi add --encrypt ~/.kube/config" case update u - # Source path is home/ inside the git repo; find the repo root set -l src (chezmoi source-path) if not test -d $src echo "chezmoi source not found" @@ -276,18 +481,20 @@ function dotfiles -d "Manage dotfiles via chezmoi" echo "Usage: dotfiles " echo "" echo "Commands:" - echo " edit Edit a managed file" - echo " diff Show pending changes" - echo " sync Apply all changes" - echo " update Pull latest + apply" - echo " status Show managed file count + pending diffs" - echo " cd cd to chezmoi source directory" - echo " refresh Re-download external files (fish plugins)" - echo " add Add a new file to chezmoi" - echo " doctor Run health check on dotfiles setup" - echo " bench Benchmark shell startup time" - echo " backup Back up chezmoi config + age key" - echo " encrypt-setup Set up age encryption" + echo " edit Edit + apply + auto-commit" + echo " drift Detect and re-absorb drifted files" + echo " secret Manage 1Password secrets (add/rm/list)" + echo " diff Show pending changes" + echo " sync Apply all changes" + echo " update Pull latest + apply" + echo " status Show managed file count + pending diffs" + echo " cd cd to chezmoi source directory" + echo " refresh Re-download external files (fish plugins)" + echo " add Add a new file to chezmoi" + echo " doctor Run health check on dotfiles setup" + echo " bench Benchmark shell startup time" + echo " backup Back up chezmoi config + age key" + echo " encrypt-setup Set up age encryption" case '*' chezmoi $argv end diff --git a/home/dot_config/fish/functions/list-secrets.fish b/home/dot_config/fish/functions/list-secrets.fish deleted file mode 100644 index 3264fc0..0000000 --- a/home/dot_config/fish/functions/list-secrets.fish +++ /dev/null @@ -1,8 +0,0 @@ -function list-secrets --description "List auto-loaded 1Password secret bindings" - set -l data (chezmoi source-path)/.chezmoidata/secrets.toml - if test ! -f $data - echo "(no secrets registered)" - return 0 - end - grep -E '^[A-Z_][A-Z0-9_]* = ' $data | sed 's/ = / → /; s/"//g' -end diff --git a/home/dot_config/fish/functions/rm-secret.fish b/home/dot_config/fish/functions/rm-secret.fish deleted file mode 100644 index 4238965..0000000 --- a/home/dot_config/fish/functions/rm-secret.fish +++ /dev/null @@ -1,30 +0,0 @@ -function rm-secret --description "Unregister an auto-loaded 1Password secret" - if test (count $argv) -lt 1 - echo "Usage: rm-secret VAR_NAME [--no-commit]" - return 1 - end - - set -l var $argv[1] - set -l do_commit 1 - contains -- --no-commit $argv; and set do_commit 0 - - set -l data (chezmoi source-path)/.chezmoidata/secrets.toml - if not grep -q "^$var = " $data - echo "⚠ $var not registered" - return 1 - end - - sed -i '' "/^$var = /d" $data - echo "✓ removed $var" - - echo "→ chezmoi apply" - chezmoi apply; or return 1 - echo "✓ applied. \$$var will be absent from new shells." - - if test $do_commit -eq 1 - set -l repo (dirname (chezmoi source-path)) - git -C $repo add .chezmoidata/secrets.toml - git -C $repo commit -m "chore(secrets): unregister $var" >/dev/null - echo "✓ committed." - end -end From 5ce29663c3be88752858d6721262b0a12eecddb3 Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Tue, 14 Apr 2026 23:49:13 +0700 Subject: [PATCH 07/10] docs: LLM-first README, shareable llm-dotfiles pattern, security hardening Reposition the repo narrative: lead with the LLM-maintained workflow, chezmoi as the backbone that makes it possible. README: - New pitch: "A dotfiles repo maintained by an LLM" - How it works section with scan dimension table - Security section explaining op:// vault structure visibility - Install details, manual commands, architecture moved to guide.md docs/llm-dotfiles.md (new): - Shareable, stack-agnostic doc for the LLM-maintained dotfiles pattern - "Setting it up" with 4 concrete steps + reference implementation link - Works with any dotfiles manager + any LLM agent docs/guide.md: - Section 1 is now "The LLM workflow" (was chezmoi details) - Install details absorbed from README - Fixed du abbreviation conflict (dfu for dotfiles update) Security fixes: - Vault name now configurable via chezmoi data (no hardcoded Developer) - Post-install summary shows /dotfiles-sync hint Cleanup: - Remove stale docs/session_state.md Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 323 +++++-------------- docs/guide.md | 101 ++++-- docs/llm-dotfiles.md | 321 ++++++++++++++++++ docs/session_state.md | 62 ---- home/.chezmoiscripts/run_after_zz-summary.sh | 3 + home/dot_config/fish/config.fish.tmpl | 2 +- home/dot_config/fish/functions/dotfiles.fish | 8 +- 7 files changed, 484 insertions(+), 336 deletions(-) create mode 100644 docs/llm-dotfiles.md delete mode 100644 docs/session_state.md diff --git a/README.md b/README.md index 54ddbcd..5f0d99f 100644 --- a/README.md +++ b/README.md @@ -8,302 +8,131 @@ ![1Password](https://img.shields.io/badge/1Password-Secrets-0572EC?logo=1password&logoColor=white) ![CI](https://img.shields.io/github/actions/workflow/status/dwarvesf/dotfiles/test.yml?label=CI&logo=github) -A modern developer tooling stack for macOS, deployed in one command. Every tool is chosen for speed, ergonomics, and native macOS integration; no legacy defaults, no bloat. +A dotfiles repo maintained by an LLM. You operate your Mac freely; Claude detects what drifted and syncs it back to the repo on your approval. **You never manually keep this repo in sync.** -**The stack:** [Fish](https://fishshell.com/) replaces Zsh (faster startup, better defaults). [Starship](https://starship.rs/) replaces Oh My Zsh themes (cross-shell, instant). [Ghostty](https://ghostty.org/) replaces iTerm2 (GPU-rendered, native). [delta](https://github.com/dandavison/delta) replaces diff (syntax-highlighted). [eza](https://eza.rocks/), [bat](https://github.com/sharkdp/bat), [fd](https://github.com/sharkdp/fd), [ripgrep](https://github.com/BurntSushi/ripgrep), [zoxide](https://github.com/ajeetdsouza/zoxide), [fzf](https://github.com/junegunn/fzf) replace ls, cat, find, grep, cd, Ctrl+R. [1Password](https://1password.com/) handles secrets via `op://` templates; nothing is ever stored in git. All managed by [chezmoi](https://www.chezmoi.io/) with a [gum](https://github.com/charmbracelet/gum)-powered setup wizard. +## The idea - - +Most dotfiles repos expect you to edit the source, apply, commit, push. In practice, nobody does this consistently. You `brew install` while debugging, tweak a config directly, add an API key, and move on. After a few weeks, the repo is stale. -**Requirements:** macOS 12+, Apple Silicon (Intel works too). First run takes ~30 minutes (Homebrew downloads). +This repo works differently. You change things on your machine. Periodically, you ask Claude to catch up: -## Quick start - -```bash -git clone https://github.com/dwarvesf/dotfiles ~/dotfiles -cd ~/dotfiles && ./install.sh ``` +You: /dotfiles-sync +Claude: [scans machine — packages, configs, extensions, secrets] -A styled setup wizard ([gum](https://github.com/charmbracelet/gum)) will prompt for your name, email, editor, headless mode, and whether you use 1Password. Everything adapts accordingly. On a headless/server machine, GUI apps, dev toolchains, and casks are skipped automatically. - -**Flags:** -- `./install.sh --check` -- dry-run, validates without applying -- `./install.sh --force` -- teardown and reinit from scratch -- `./install.sh --config-only` -- deploy config files only, skip brew/mas/defaults +Claude: Dotfiles sync report + Config drift: Zed settings (2 new MCP servers) + New packages: ollama, rclone, pandoc + Stale: raycast, slack (not installed) + VS Code: 5 new extensions + What should I do? -
-Adopt on an existing Mac +You: sync everything, drop raycast and slack -Already have brew, fish, and your tools installed? Use `--config-only` to deploy just the config files without re-running brew bundle, mas installs, or macOS defaults: +Claude: [edits Brewfile, re-adds configs, updates extensions, + logs to sync-log.md, commits] + Done. Push? -```bash -git clone https://github.com/dwarvesf/dotfiles ~/dotfiles -cd ~/dotfiles && ./install.sh --config-only +You: push ``` -This will: -1. Link chezmoi source to `~/dotfiles/home` -2. Prompt for your name, email, editor, headless mode, 1Password -3. Deploy all config files to `$HOME` -4. **Skip** brew bundle, Mac App Store apps, macOS defaults, toolchain installs +Two sentences from you. The LLM handled 6 file edits, a commit message, and a push. -Then switch your shell and reload: -```bash -# Set fish as default (if not already) -grep -q /opt/homebrew/bin/fish /etc/shells || echo /opt/homebrew/bin/fish | sudo tee -a /etc/shells -chsh -s /opt/homebrew/bin/fish - -# Open a new terminal to pick up the configs -``` - -
- -
-Alternative: bootstrap without git - -On a truly fresh Mac, git requires Xcode CLT (10+ minutes to install). These methods skip that: - -**Via chezmoi directly (no git, no Homebrew):** -```bash -sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply dwarvesf -``` - -**Via Homebrew + chezmoi (no git):** -```bash -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -eval "$(/opt/homebrew/bin/brew shellenv)" -brew install chezmoi -chezmoi init --apply dwarvesf -``` +The pattern is general and works with any dotfiles manager and any LLM agent. The full write-up, including setup instructions, is in **[docs/llm-dotfiles.md](docs/llm-dotfiles.md)**. -> **Note:** These methods clone into `~/.local/share/chezmoi/` (chezmoi's default) instead of `~/dotfiles`. The git clone method is better for active development since you control the repo location. - -
- -
-Fork and customize - -```bash -# 1. Fork this repo on GitHub -# 2. Clone your fork -git clone https://github.com/YOUR_USERNAME/dotfiles ~/dotfiles -cd ~/dotfiles - -# 3. Edit what you want (see "Customization" below) -# 4. Run -./install.sh -``` - -
- -## What happens on install - -

- Bootstrap flow -

- -1. Installs Homebrew (if missing) -2. Installs chezmoi -3. Runs setup wizard (styled prompts for name, email, editor, headless mode, 1Password) -4. Deploys all config files to `$HOME` -5. Runs automation scripts: - - `brew bundle` -- installs ~80 packages + casks - - Mac App Store apps via `mas` - - macOS defaults (Dock, Finder, keyboard, trackpad, screenshots) - - Sets Fish as default shell - - Installs Foundry (cast), Rust, npm/uv tools - - VS Code extensions -6. Verifies key files were deployed - -## What's included - -| Layer | Tools | -|-------|-------| -| **Shell** | Fish + Starship prompt + plugins (autopair, done, sponge, async-prompt) | -| **Terminal** | Ghostty (catppuccin-mocha, JetBrains Mono) | -| **Multiplexer** | tmux (C-a prefix, vim nav, fzf session picker, project launcher) | -| **Editors** | VS Code + Zed (settings, extensions, MCP servers) | -| **Git** | .gitconfig (delta diffs, aliases) + .gitignore + commit template | -| **SSH** | 1Password SSH Agent (optional), modular config.d/ | -| **Secrets** | 1Password (`op://`) + data-driven registry (`secrets.toml`) -- never in git | -| **Packages** | Layered Brewfile (base/dev/apps) + Mac App Store (`mas`) | -| **Languages** | mise (Node, Python, Go, Ruby) via `.tool-versions` | -| **Containers** | OrbStack / Docker config | -| **macOS** | 30+ `defaults write` (Dock left, fast key repeat, Finder, screenshots) | -| **Web3/DeFi** | Foundry (`cast`), fish aliases + helper functions | -| **Claude Code** | Settings, hooks, statusline, verify-dotfiles subagent, `/implement-feature` command | - -## Why this setup - -- **Layered Brewfile** -- base tools always install; dev toolchains and GUI apps are conditional. Set `headless=true` for servers. -- **Zero plaintext secrets** -- 1Password `op://` references in templates, macOS Keychain for the rest. The rendered secrets only exist on your machine, never in git. -- **Claude-assisted sync** -- run `/dotfiles-sync` in Claude Code to detect all drift (brew, casks, configs, extensions, secrets), review in plain language, and sync with one approval. Manual commands (`dotfiles edit`, `dotfiles drift`) work offline. -- **15-command CLI** -- `dotfiles edit`, `dotfiles drift`, `dotfiles secret`, `dotfiles sync`, `dotfiles doctor`, `dotfiles bench`... no need to remember raw chezmoi commands ([ADR-006](docs/decisions/006-auto-commit-workflow.md)). -- **CI-tested weekly** -- shellcheck + chezmoi dry-run on macOS. Catches regressions before your next fresh install. -- **Graceful degradation** -- works with or without 1Password. Skip web3, skip Mac App Store, pick your editor. Everything is opt-in. - -## Daily usage - -**Core principle:** every setting change should be both applied to your machine and committed to the repo in one step. The helpers below enforce this by default. +## How it works

chezmoi model: source to target

-### Editing configs +[chezmoi](https://www.chezmoi.io/) is the backbone. It separates the source (repo) from the target ($HOME), renders templates with injected secrets, and provides drift detection via `chezmoi status`. This two-layer model is what makes LLM-maintained sync possible: the LLM can safely scan, diff, and re-add without touching secrets in git. -```fish -dotfiles edit ~/.config/fish/config.fish # edit source → apply → auto-commit -dotfiles edit ~/.Brewfile # edit Brewfile → apply (runs brew bundle) → commit -dotfiles edit ~/.config/ghostty/config # edit → apply (live reload) → commit -``` - -`dotfiles edit` opens the chezmoi source file in your editor, applies on save, and commits the change. Pass `--no-commit` to skip the commit. - -

- dfe workflow: edit, apply, commit -

+The `/dotfiles-sync` slash command (at [.claude/commands/dotfiles-sync.md](.claude/commands/dotfiles-sync.md)) teaches Claude what to scan: -### Syncing drift +| Dimension | What it detects | +|-----------|----------------| +| Config drift | Files changed on machine but not in repo | +| Brew packages | Installed but not in Brewfile (and vice versa) | +| Cask apps | GUI apps installed but not tracked | +| VS Code extensions | New or removed extensions | +| Fish functions | Functions created outside chezmoi | +| SSH configs | New host configs in config.d/ | +| Secrets | Hardcoded keys that should be in 1Password | -If you edited a deployed file directly (or an app rewrote its config), `dotfiles drift` detects the drift and pulls it back into the source: +Every sync is logged in [docs/sync-log.md](docs/sync-log.md) so future syncs have context. -```fish -dotfiles drift # detect drift → prompt → re-add → commit -dotfiles drift --no-commit # re-absorb without committing -``` - -

- dotfiles drift workflow: detect drift and sync -

- -### The `dotfiles` wrapper - -For everything else, the `dotfiles` CLI provides ergonomic subcommands: - -```fish -dotfiles diff # preview changes -dotfiles sync # apply everything -dotfiles update # pull latest + apply -dotfiles status # managed file count + pending diffs -dotfiles doctor # health check (tools, config, drift) -dotfiles bench # benchmark shell startup time -dotfiles backup # back up config + age key to 1Password -dotfiles encrypt-setup # guided age encryption setup -``` - -
-Raw chezmoi commands +## Quick start ```bash -chezmoi edit ~/.config/fish/config.fish -chezmoi diff -chezmoi apply -chezmoi apply --refresh-externals -``` - -
- -## Customization - -Use `dotfiles edit` to edit any config (edit → apply → commit in one step): - -```fish -dotfiles edit ~/.Brewfile # add Homebrew packages -dotfiles edit ~/.config/fish/config.fish # shell config -dotfiles edit ~/.config/ghostty/config # terminal settings +git clone https://github.com/dwarvesf/dotfiles ~/dotfiles +cd ~/dotfiles && ./install.sh ``` -Add secrets via 1Password: - -```fish -dotfiles secret add OPENAI_API_KEY "op://Private/OpenAI/credential" -``` +A [gum](https://github.com/charmbracelet/gum)-powered wizard prompts for your name, email, editor, headless mode, and 1Password. First run takes ~30 minutes (Homebrew downloads). After that, just use `/dotfiles-sync` to keep things current. -The full user guide covers walkthroughs, the secrets workflow, multi-machine -setup, troubleshooting, and architecture: **[docs/guide.md](docs/guide.md)**. +**Requirements:** macOS 12+, Apple Silicon (Intel works too).
-Encrypted files (age) - -For files too complex for template injection (kubeconfig, VPN configs, certificates): - -```fish -# Guided setup (generates key, prints next steps) -dotfiles encrypt-setup +Other install methods -# Then add encrypted files -chezmoi add --encrypt ~/.kube/config -# Creates home/encrypted_dot_kube/config.age in the repo +**Existing Mac** (configs only, skip brew/mas/defaults): +```bash +cd ~/dotfiles && ./install.sh --config-only ``` -Manual setup if you prefer: +**Without git** (fresh Mac, no Xcode CLT): ```bash -brew install age -age-keygen -o ~/.config/chezmoi/key.txt -# Copy the public key (age1...) from output -chezmoi edit-config # uncomment age section, paste public key -# Backup key.txt to 1Password as a Secure Note +sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply dwarvesf ``` +**Flags:** `--check` (dry-run), `--force` (reinit from scratch) +
-
-Removing what you don't need +## The stack -- **No web3?** Delete web3 aliases from `config.fish.tmpl`, remove `cast_*` functions, remove Foundry from install script -- **No 1Password?** Answer "no" during `chezmoi init` -- all 1Password sections are skipped -- **No Mac App Store?** Delete `run_once_after_mas-apps.sh.tmpl` -- **Different editor?** `chezmoi init` prompts for your choice (VS Code, Zed, Neovim, Vim) +| Layer | Tools | +|-------|-------| +| **Shell** | Fish + Starship prompt + plugins (autopair, done, sponge, async-prompt) | +| **Terminal** | Ghostty (GPU-rendered, catppuccin-mocha, JetBrains Mono) | +| **Multiplexer** | tmux (C-a prefix, vim nav, fzf session picker) | +| **Editors** | VS Code + Zed (settings, extensions, MCP servers with 1P secrets) | +| **Git** | delta diffs, aliases, commit template | +| **SSH** | 1Password SSH Agent, modular config.d/ | +| **Secrets** | 1Password `op://` templates + data-driven registry | +| **Packages** | Layered Brewfile (base/dev/apps) + Mac App Store | +| **Languages** | mise (Node, Python, Go, Ruby) | +| **macOS** | 30+ `defaults write` (Dock, Finder, keyboard, screenshots) | -
+Every tool is chosen for speed, ergonomics, and native macOS integration. No legacy defaults, no bloat. -## Troubleshooting +## Offline fallback -Run `dotfiles doctor` to diagnose issues: +When you're not in a Claude session (SSH, airplane, quick edit), the `dotfiles` CLI works standalone: -``` -$ dotfiles doctor -Dotfiles health check -===================== - -[ok] chezmoi installed -[ok] chezmoi source linked -[ok] fish is default shell -[ok] homebrew installed -[ok] 1Password CLI: signed in -[ok] 1Password SSH agent: socket exists -[ok] ~/.gitconfig exists -[ok] ~/.config/fish/config.fish exists -[ok] ~/.ssh/config exists -[ok] git identity: Your Name -[ok] fzf -[ok] bat -... -[ok] no drift detected - -All checks passed. +```fish +dotfiles edit ~/.config/fish/config.fish # edit + apply + auto-commit +dotfiles drift # detect and re-absorb drift +dotfiles doctor # health check ``` -
-How secrets flow - -

- Secrets flow -

+Full command reference, walkthroughs, secrets management, multi-machine setup, and troubleshooting are in the **[user guide](docs/guide.md)**. -On a new Mac: clone -> `./install.sh` -> `op signin` -> `chezmoi apply` -> done. +## Security -
+This repo is safe to make public. Actual secrets (API keys, tokens, passwords) are never committed; only `op://` references to 1Password items appear in the source. Real values are resolved at `chezmoi apply` time and only exist on your machine. -
-Architecture +The `op://` references do reveal 1Password vault and item names (e.g. `op://Private/OpenAI/credential`). This is intentional: it makes the repo forkable. If you fork, replace the item names with your own. The vault structure tells someone what services you use, not how to access them. -

- Architecture -

+## Docs -
+| Document | What it covers | +|----------|---------------| +| **[docs/llm-dotfiles.md](docs/llm-dotfiles.md)** | The LLM-maintained dotfiles pattern. Shareable, stack-agnostic. Includes setup instructions. | +| **[docs/guide.md](docs/guide.md)** | Full user guide. chezmoi details, manual commands, customization, secrets, multi-machine, troubleshooting. | +| **[docs/decisions/](docs/decisions/)** | Architecture decision records (why chezmoi, Fish, Ghostty, 1Password, auto-commit). | +| **[docs/sync-log.md](docs/sync-log.md)** | Sync history. Append-only log of every Claude-assisted sync. | ## Credits diff --git a/docs/guide.md b/docs/guide.md index e6e2f90..9d49356 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1,12 +1,44 @@ # User guide Everything you need to use and customize this dotfiles setup. The -[README](../README.md) covers quick start and what's included; this guide -covers how it all works and how to make it yours. +[README](../README.md) covers the pitch and quick start; this guide +covers how it all works and how to make it yours. For the general +LLM-maintained dotfiles pattern, see [llm-dotfiles.md](llm-dotfiles.md). --- -## 1. How this works +## 1. The LLM workflow + +The primary way to maintain this repo is to ask Claude. + +Run `/dotfiles-sync` in Claude Code (or just say "catch up with my +dotfiles"). Claude scans your machine across 10 dimensions: config +drift, brew packages, casks, VS Code extensions, fish functions, SSH +configs, secrets, and more. It reports what changed in plain language +and waits for your instructions. + +``` +You: /dotfiles-sync +Claude: Config drift: Zed settings (2 new MCP servers) + New packages: ollama, rclone, pandoc + Stale: raycast, slack (not installed) + What should I do? +You: sync everything, drop raycast and slack +Claude: Done. 1 commit. Push? +You: push +``` + +Every sync is logged in `docs/sync-log.md`. Claude reads it at the +start of each session for context ("last sync was 2 weeks ago, you +added ollama"). + +The manual commands in the sections below are fallbacks for when you're +offline, SSH'd into a server, or want a quick one-off edit. You don't +need to learn them to use this repo day-to-day. + +--- + +## 2. How chezmoi works ### The two layers @@ -63,7 +95,41 @@ Naming: `dot_` becomes `.`, `.tmpl` means "render this template", --- -## 2. Your first 30 minutes +### What happens on install + +

+ Bootstrap flow +

+ +1. Installs Homebrew (if missing) +2. Installs chezmoi + runs setup wizard (name, email, editor, headless, 1Password) +3. Deploys all config files to `$HOME` +4. Runs `brew bundle` (~80 packages + casks) +5. Mac App Store apps via `mas` +6. macOS defaults (Dock, Finder, keyboard, trackpad, screenshots) +7. Sets Fish as default shell +8. Installs toolchains (Foundry, Rust, npm/uv), VS Code extensions +9. Verifies key files were deployed + +**Install flags:** +- `./install.sh --check` -- dry-run, validates without applying +- `./install.sh --force` -- teardown and reinit from scratch +- `./install.sh --config-only` -- deploy config files only, skip brew/mas/defaults + +**Adopt on an existing Mac:** +```bash +cd ~/dotfiles && ./install.sh --config-only +``` +Then set Fish as default: `chsh -s /opt/homebrew/bin/fish` + +**Bootstrap without git** (fresh Mac, no Xcode CLT): +```bash +sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply dwarvesf +``` + +--- + +## 3. Your first 30 minutes You just ran `install.sh`. Here's what's on your machine now. @@ -129,18 +195,7 @@ key config files exist, git identity set, CLI tools present, no drift. --- -## 3. Daily workflows - -### Primary workflow: Claude-assisted sync - -The easiest way to keep your dotfiles in sync is to ask Claude. Run -`/dotfiles-sync` in Claude Code (or just say "catch up with my dotfiles"). -Claude scans your machine for drift across brew packages, casks, config -files, VS Code extensions, fish functions, and secrets. It reports what -changed in plain language and waits for your instructions before syncing. - -This is the recommended workflow for batch changes. The manual commands -below are for quick edits or when you're not in a Claude session. +## 4. Manual commands (offline fallback) ### Editing any config @@ -200,7 +255,7 @@ For everything beyond editing, use the `dotfiles` wrapper: | `dotfiles secret ` | | | Manage 1Password secrets (add/rm/list) | | `dotfiles diff` | `d` | | Show pending changes | | `dotfiles sync` | `s` | `ds` | Apply all changes | -| `dotfiles update` | `u` | `du` | Pull latest + apply | +| `dotfiles update` | `u` | `dfu` | Pull latest + apply | | `dotfiles status` | `st` | | Managed file count + pending diffs | | `dotfiles cd` | | | Go to chezmoi source directory | | `dotfiles refresh` | `r` | | Re-download fish plugins | @@ -214,7 +269,7 @@ All subcommands have tab completions. Abbreviations expand on space (e.g. type ` --- -## 4. Customization cookbook +## 5. Customization cookbook ### Quick-change table @@ -340,7 +395,7 @@ The change is auto-committed. --- -## 5. Secrets management +## 6. Secrets management ### The three tiers @@ -405,7 +460,7 @@ never leaves your machine. --- -## 6. Multi-machine setup +## 7. Multi-machine setup ### Deploying to a second Mac @@ -461,7 +516,7 @@ sections of the Brewfile and script execution. --- -## 7. Troubleshooting +## 8. Troubleshooting Start with `dotfiles doctor` — it catches most issues. @@ -543,7 +598,7 @@ cd ~/dotfiles && ./install.sh --force --- -## 8. Architecture reference +## 9. Architecture reference

Architecture overview @@ -606,7 +661,7 @@ Architectural choices are documented as ADRs in `docs/decisions/`: | Edit any config | `dotfiles edit ` | `de` | | Detect and fix drift | `dotfiles drift` | `dd` | | Apply all changes | `dotfiles sync` | `ds` | -| Pull latest + apply | `dotfiles update` | `du` | +| Pull latest + apply | `dotfiles update` | `dfu` | | See what would change | `dotfiles diff` | | | Check health | `dotfiles doctor` | | | Add a Homebrew package | `dotfiles edit ~/.Brewfile` | | diff --git a/docs/llm-dotfiles.md b/docs/llm-dotfiles.md new file mode 100644 index 0000000..1a28952 --- /dev/null +++ b/docs/llm-dotfiles.md @@ -0,0 +1,321 @@ +# LLM-maintained dotfiles + +A pattern for keeping dotfiles repos in sync using LLMs. + +This is an idea file. It describes a general pattern, not a specific +implementation. Copy it into your own LLM agent (Claude Code, Codex, +OpenCode, etc.) and adapt it to your stack. Your agent will figure out +the specifics. + +## The core idea + +Most dotfiles repos assume you'll manually keep the repo in sync with +your machine. Edit the source, run apply, commit, push. The tooling is +good: chezmoi, GNU Stow, yadm, bare git repos. But the workflow has a +fundamental flaw: it depends on human memory. + +In practice, you `brew install` something while debugging and forget to +update the Brewfile. You tweak a config file directly instead of going +through the dotfiles manager. You add an API key and never register it +in the secrets manager. After a few weeks, the repo is stale. After a +few months, it's fiction. + +The idea here is different. Instead of expecting you to maintain the +repo, **the LLM maintains it.** You operate your machine freely. +Periodically, you ask the LLM to catch up. It scans your machine, +detects everything that drifted, reports it in plain language, and waits +for your decisions. Then it syncs the changes back into the source, +commits, and pushes. + +The human curates and decides; the LLM does the bookkeeping. The tedious +part of dotfiles management isn't choosing your tools; it's keeping the +repo in sync with your choices. The LLM handles that. + +## Architecture + +Three layers: + +**Machine state** (the source of truth) -- what's actually installed +and configured on your computer right now. Brew packages, cask apps, +config files, shell functions, API keys, editor extensions. You change +this freely, without thinking about the repo. + +**The repo** (the persistent artifact) -- your dotfiles source tree. +Managed by chezmoi, Stow, yadm, or a bare git repo. This is what gets +deployed on a new machine. The LLM's job is to keep it accurate. + +**The schema** (the instructions) -- a document that tells the LLM how +to scan your machine, what to detect, how to report, and how to sync. +In Claude Code this lives in `CLAUDE.md` or a slash command file. In +other agents it might be a system prompt or an AGENTS.md. This is the +key configuration file; it's what turns a generic LLM into a dotfiles +maintainer. + +## Operations + +### Scan + +The LLM runs detection commands to compare machine state against the +repo. What to detect depends on your stack, but common dimensions: + +| Dimension | Detection approach | +|-----------|-------------------| +| Config drift | Compare deployed files against source (chezmoi status, diff) | +| Package drift | Compare installed packages against package list (brew leaves vs Brewfile) | +| App drift | Compare installed apps against tracked list (brew casks, snap, flatpak) | +| Extension drift | Compare editor extensions against tracked list | +| New files | Find files in managed directories that the dotfiles tool doesn't know about | +| Secret drift | Check for hardcoded keys that should be in a secrets manager | +| Stale entries | Find entries in the repo that no longer exist on the machine | + +The LLM runs these commands in parallel and collects the findings. No +custom scripts needed; the LLM calls the detection commands directly. + +### Report + +The LLM presents findings in plain language, grouped by category: + +``` +Dotfiles sync report + +Config drift (2 files): + - Zed settings — 2 new MCP servers added + - tmux config — changed prefix key + +New packages (5): + ollama, rclone, pandoc, ... + +Stale entries (3): + raycast, slack (not installed, in Brewfile) + +What should I do? +``` + +The report is conversational. The LLM adds context where it can: +"ollama is probably for local LLM testing." The user sees what changed +and why, not raw diffs. + +### Decide + +The LLM waits. It never syncs without approval. The user responds in +plain language: + +- "Add the new packages" +- "Drop raycast, I use Spotlight now" +- "Keep btop in the Brewfile, I want it on my next machine" +- "Sync the Zed config" +- "Do everything" + +This is the critical design choice: the LLM does the bookkeeping, but +the human makes the decisions. Fully automated sync is tempting but +dangerous; a bad config could propagate to every machine. + +### Sync + +The LLM executes the approved changes: re-adds config files, edits +package lists, updates extension lists, registers secrets. It uses the +dotfiles tool's native commands (chezmoi re-add, chezmoi add, etc.). + +### Log + +After each sync, the LLM appends an entry to a log file: + +```markdown +## [2026-04-14] sync + +Config: re-add Zed settings (2 new MCP servers) +Packages: add 5 (ollama, rclone, ...) +Stale: remove raycast, slack from Brewfile +``` + +The log serves two purposes. First, context for future syncs: the LLM +reads it at the start of each session and knows what happened recently. +Second, an audit trail that's easier to scan than git log. + +### Commit and push + +The LLM commits with a descriptive message summarizing all changes by +category. Pushing is manual: the LLM asks, the user confirms. + +## Example session + +``` +User: catch up with my dotfiles + +Claude: [scans machine, 30 seconds] + +Claude: Dotfiles sync report (2026-04-14) + + Config drift (1 file): + - Zed settings.json — 2 new MCP servers + + New brew packages (5): + ollama, rclone, pandoc, pipx, opencode + + Stale casks (2): + raycast, slack (in Brewfile but not installed) + + VS Code extensions (3 new): + docker.docker, ms-vsliveshare.vsliveshare, ocaml-platform + + What should I do? + +User: sync everything, drop raycast and slack + +Claude: [re-adds Zed settings, adds packages to Brewfile, + removes raycast/slack, updates extensions.txt, + appends to sync log, commits] + +Claude: Done. 1 commit ready. Push? + +User: push + +Claude: [pushes] +``` + +The user spoke 2 sentences. The LLM handled 6 file edits, a commit +message, and a push. + +## What stays manual + +- **Initial setup.** Clone, install, configure. The LLM can guide you + through it, but the first run is yours. +- **Destructive decisions.** The LLM will never auto-remove a package + or overwrite a config without asking. +- **Push.** Always manual. A bad push propagates to every machine. +- **New tool adoption.** If you switch from Zsh to Fish, that's a + design decision, not bookkeeping. Tell the LLM what you decided. + +## Design principles + +**LLM does bookkeeping, you make decisions.** The LLM never auto-syncs +without asking. It scans, reports, and waits. + +**Plain language over commands.** You say "drop raycast, I switched to +Spotlight." The LLM figures out which line to delete. + +**One session covers everything.** Packages, configs, extensions, +secrets, all in one pass. No separate workflows per category. + +**The repo is the persistent artifact.** It compounds over time. Every sync makes it more accurate. + +**No daemon, no watcher.** You trigger the sync when you want it. This +is intentional: dotfiles sync should be a conscious decision, not +background automation. + +## Setting it up + +### Prerequisites + +You need two things: + +1. **A dotfiles repo** managed by any tool (chezmoi, Stow, yadm, bare + git). If you don't have one yet, start with + [chezmoi](https://www.chezmoi.io/quick-start/). +2. **An LLM agent with shell access.** Claude Code, OpenAI Codex, + OpenCode, or similar. The agent needs to run commands on your + machine and edit files. + +### Step 1: Create the sync command + +In Claude Code, create a slash command file: + +``` +.claude/commands/dotfiles-sync.md +``` + +This file is the schema. It teaches the LLM what to scan, how to +report, and how to sync. Write it as a prompt that instructs the LLM +to: + +1. Read the sync log for context +2. Run detection commands for each drift dimension +3. Diff results against the repo state +4. Format a plain-language report +5. Wait for user decisions +6. Execute approved changes +7. Append to the sync log +8. Commit and ask before pushing + +The detection commands depend on your stack: + +| Stack | Drift detection | +|-------|----------------| +| chezmoi | `chezmoi status` | +| GNU Stow | `diff -r ~/.dotfiles/ ~/` | +| Homebrew | `brew leaves` vs Brewfile | +| apt | `apt-mark showmanual` vs packages list | +| VS Code | `code --list-extensions` vs extensions.txt | +| Fish/Zsh functions | `ls` functions dir vs managed list | +| Secrets | grep for hardcoded keys in shell config | + +A working reference implementation for chezmoi + Homebrew + Fish is at +[dwarvesf/dotfiles/.claude/commands/dotfiles-sync.md](https://github.com/dwarvesf/dotfiles/blob/main/.claude/commands/dotfiles-sync.md). + +### Step 2: Create the sync log + +``` +docs/sync-log.md +``` + +Start with a header. The LLM appends entries after each sync: + +```markdown +# Dotfiles sync log + +--- + +## [2026-04-14] sync + +Config: re-add Zed settings +Packages: add ollama, rclone +Stale: remove raycast + +--- +``` + +### Step 3: Add project context (optional but recommended) + +If your LLM agent supports project instructions (CLAUDE.md, AGENTS.md, +etc.), add a brief section explaining your repo layout: where source +files live, which are templates, how secrets work. This helps the LLM +make better decisions during sync. + +### Step 4: Run your first sync + +``` +/dotfiles-sync +``` + +Or just tell the agent: "catch up with my dotfiles." + +### Adapting to other stacks + +The pattern works with any combination: + +| Layer | Options | +|-------|---------| +| Dotfiles manager | chezmoi, GNU Stow, yadm, bare git, Nix Home Manager | +| Shell | Fish, Zsh, Bash | +| Package manager | Homebrew, apt, pacman, nix | +| Secrets | 1Password, Bitwarden, SOPS, age, pass | +| LLM agent | Claude Code, OpenAI Codex, OpenCode, any agent with shell access | + +The detection commands change; the pattern doesn't. + +## Why this works + +The tedious part of dotfiles management is not choosing tools; it's the +ongoing maintenance. Updating the Brewfile after every `brew install`. +Committing after every config tweak. Noticing when a deployed file +drifted from the source. Nobody does this consistently because the cost +of each individual sync is low but the friction is constant. + +LLMs are good at exactly this kind of work: scanning for differences, +formatting reports, making mechanical edits to text files, writing +commit messages. The human's job is to operate their machine and make +decisions. The LLM's job is everything in between. + +The result: a dotfiles repo that's always accurate, a sync log that +documents your machine's evolution, and a workflow that takes 30 seconds +instead of being silently skipped. diff --git a/docs/session_state.md b/docs/session_state.md deleted file mode 100644 index 936abae..0000000 --- a/docs/session_state.md +++ /dev/null @@ -1,62 +0,0 @@ -# Session State - -Updated: 2026-04-02 -Project: dwarvesf/dotfiles -Phase: Stable, expanding - -## POSITION - -### Phase -Mature. All 12 original features (F-01 through F-12) and 5 cleanup specs (R-03 through R-09) are shipped except F-03 (bootstrap without git), F-04 (Brewfile split), and F-12 (v0.1.0 tag). 24+ commits on main. CI passing weekly. - -### What is decided -- chezmoi as dotfiles manager (over GNU Stow, yadm, Nix) -- Fish shell (over Zsh/Oh My Zsh/Prezto) for startup speed and built-in features -- Ghostty terminal (over Kitty, WezTerm, iTerm2) for native macOS feel + performance -- 1Password for secrets (op:// template injection, never plaintext in git) -- XDG Base Directory compliance where possible -- Fish plugins via .chezmoiexternal.toml (no plugin manager, ADR-005) -- mise for language version management (.tool-versions) -- age encryption for complex sensitive files (encrypt-setup command exists) -- Modular SSH config with config.d/ and 1Password agent -- tmux with C-a prefix, vim nav, fzf session picker - -### What is still open -- Whether to add Starship prompt (recommended, not yet added) -- Brewfile split into base/dev/apps layers (F-04) -- Window tiling manager (Aerospace or similar) -- Multi-machine profiles (chezmoi tags for work vs personal) - -### Codebase status -Production-grade dotfiles system with: -- Idempotent install.sh (--check, --force flags, exit codes) -- CI pipeline (shellcheck + dry-run on macOS, weekly schedule) -- `dotfiles` CLI with 9 subcommands (edit, diff, sync, status, cd, refresh, add, doctor, encrypt-setup) -- Drift detection (dotfiles-drift function) -- 9 custom Fish functions with tab completions -- 5 ADRs documenting key decisions -- 14 external Fish plugins/completions via .chezmoiexternal.toml -- ~80 Homebrew packages + 26 MAS apps - -## CONTEXT - -### User preferences -- Brutally honest feedback, no yes-man behavior -- Visual learner, likes diagrams and charts -- Light theme for visual elements -- No em dashes in text -- Leadership at Dwarves Foundation, based in Da Nang/Saigon -- Uses Claude Code heavily, has custom skills/MCP infrastructure -- Familiar with chezmoi, not a beginner - -### Constraints -- macOS primary (Apple Silicon) -- Must work with 1Password or without (graceful degradation) -- Public repo (no plaintext secrets ever) -- XDG-compliant where tools support it -- Fish as default shell - -## INTENT - -### Next steps (v0.2.0 roadmap) -See docs/tasks.md for the full backlog. diff --git a/home/.chezmoiscripts/run_after_zz-summary.sh b/home/.chezmoiscripts/run_after_zz-summary.sh index e6b5fe1..fa9b532 100644 --- a/home/.chezmoiscripts/run_after_zz-summary.sh +++ b/home/.chezmoiscripts/run_after_zz-summary.sh @@ -30,10 +30,13 @@ if [ "$warn_count" -eq 0 ] && [ "$fail_count" -eq 0 ]; then # All OK — short message if _has_gum; then BODY=$(_gum style --foreground 78 --bold "✓ dotfiles apply complete — all OK") + TIP=$(_gum style --faint " Tip: use /dotfiles-sync in Claude Code to detect drift and keep the repo current") + BODY=$(_gum join --vertical "$BODY" "" "$TIP") _gum style --border rounded --border-foreground $border_color --padding "1 2" --margin "1 0" "$BODY" else echo "" printf '\033[38;5;78m ✓ dotfiles apply complete — all OK\033[0m\n' + printf '\033[38;5;245m Tip: use /dotfiles-sync in Claude Code to detect drift and keep the repo current\033[0m\n' echo "" fi exit 0 diff --git a/home/dot_config/fish/config.fish.tmpl b/home/dot_config/fish/config.fish.tmpl index 913da2b..b912f50 100644 --- a/home/dot_config/fish/config.fish.tmpl +++ b/home/dot_config/fish/config.fish.tmpl @@ -107,7 +107,7 @@ if status is-interactive abbr -a de "dotfiles edit" abbr -a dd "dotfiles drift" abbr -a ds "dotfiles sync" - abbr -a du "dotfiles update" + abbr -a dfu "dotfiles update" # ── tmux ────────────────────────────────────────────────────────── abbr -a tx tmux diff --git a/home/dot_config/fish/functions/dotfiles.fish b/home/dot_config/fish/functions/dotfiles.fish index 5ddd9ba..284d010 100644 --- a/home/dot_config/fish/functions/dotfiles.fish +++ b/home/dot_config/fish/functions/dotfiles.fish @@ -371,8 +371,9 @@ function dotfiles -d "Manage dotfiles via chezmoi" echo " identity = \"$key_path\"" echo " recipient = \"$pubkey\"" echo "" + set -l vault (chezmoi data 2>/dev/null | grep op_vault | awk '{print $NF}' | tr -d '"'); or set vault Private echo " 3. Back up the key to 1Password:" - echo " op document create $key_path --title 'chezmoi age key' --vault=Developer" + echo " op document create $key_path --title 'chezmoi age key' --vault=$vault" echo "" echo " 4. Add encrypted files:" echo " chezmoi add --encrypt ~/.kube/config" @@ -430,6 +431,7 @@ function dotfiles -d "Manage dotfiles via chezmoi" set -l config_path (chezmoi config-path 2>/dev/null) set -l key_path "$HOME/.config/chezmoi/key.txt" + set -l vault (chezmoi data 2>/dev/null | grep op_vault | awk '{print $NF}' | tr -d '"'); or set vault Private if test -z "$config_path"; or not test -f "$config_path" echo "[!!] chezmoi config not found" @@ -439,7 +441,7 @@ function dotfiles -d "Manage dotfiles via chezmoi" echo "[1/3] chezmoi config: $config_path" if command -q op echo " Backing up to 1Password..." - op document create "$config_path" --title "chezmoi config (dotfiles backup)" --vault=Developer 2>/dev/null + op document create "$config_path" --title "chezmoi config (dotfiles backup)" --vault="$vault" 2>/dev/null and echo " [ok] Uploaded to 1Password" or echo " [!!] Upload failed. Are you signed in? (op signin)" else @@ -451,7 +453,7 @@ function dotfiles -d "Manage dotfiles via chezmoi" if test -f "$key_path" if command -q op echo " Backing up to 1Password..." - op document create "$key_path" --title "chezmoi age key (dotfiles backup)" --vault=Developer 2>/dev/null + op document create "$key_path" --title "chezmoi age key (dotfiles backup)" --vault="$vault" 2>/dev/null and echo " [ok] Uploaded to 1Password" or echo " [!!] Upload failed" else From 174cad38bac9d94c50849c04f8cd8c8287a79f0b Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Tue, 14 Apr 2026 23:53:33 +0700 Subject: [PATCH 08/10] feat: user-level /dotfiles-sync install + workflow diagram - Deploy dotfiles-sync.md to ~/.claude/commands/ via chezmoi so the slash command works from any directory, not just the repo - Add LLM sync workflow diagram (SVG) embedded in README and llm-dotfiles.md - Update setup instructions in llm-dotfiles.md with two-level install (project + user) - Guide notes that chezmoi apply deploys the command Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 4 +- docs/dotfiles_llm_sync_workflow.svg | 70 ++++++++++ docs/guide.md | 5 + docs/llm-dotfiles.md | 13 +- home/dot_claude/commands/dotfiles-sync.md | 152 ++++++++++++++++++++++ 5 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 docs/dotfiles_llm_sync_workflow.svg create mode 100644 home/dot_claude/commands/dotfiles-sync.md diff --git a/README.md b/README.md index 5f0d99f..38d00d7 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,12 @@ The pattern is general and works with any dotfiles manager and any LLM agent. Th ## How it works

- chezmoi model: source to target + LLM sync workflow: machine drifts, Claude syncs

[chezmoi](https://www.chezmoi.io/) is the backbone. It separates the source (repo) from the target ($HOME), renders templates with injected secrets, and provides drift detection via `chezmoi status`. This two-layer model is what makes LLM-maintained sync possible: the LLM can safely scan, diff, and re-add without touching secrets in git. -The `/dotfiles-sync` slash command (at [.claude/commands/dotfiles-sync.md](.claude/commands/dotfiles-sync.md)) teaches Claude what to scan: +The `/dotfiles-sync` command is installed to `~/.claude/commands/` during setup, so it's available in Claude Code from any directory. The command prompt (at [.claude/commands/dotfiles-sync.md](.claude/commands/dotfiles-sync.md)) teaches Claude what to scan: | Dimension | What it detects | |-----------|----------------| diff --git a/docs/dotfiles_llm_sync_workflow.svg b/docs/dotfiles_llm_sync_workflow.svg new file mode 100644 index 0000000..4a9797e --- /dev/null +++ b/docs/dotfiles_llm_sync_workflow.svg @@ -0,0 +1,70 @@ + + + + + + +LLM-maintained dotfiles: the sync workflow + + + +Your machine + + +brew install ollama + + +tweak Zed settings + + +add API key for new tool + + +install new VS Code ext + + +You don't think about +the repo at all + + +days / weeks pass... + + + +Repo (stale) + + +Brewfile (missing ollama) + + +Zed config (old version) + + +extensions.txt (outdated) + + + +/dotfiles-sync +Claude scans, reports, waits for approval + + +"catch up with my dotfiles" + + + + +scan + + + +diff + + + + + + +Repo (current) +Brewfile updated, configs synced, extensions tracked +Committed + logged in sync-log.md + \ No newline at end of file diff --git a/docs/guide.md b/docs/guide.md index 9d49356..5005ef0 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -32,6 +32,11 @@ Every sync is logged in `docs/sync-log.md`. Claude reads it at the start of each session for context ("last sync was 2 weeks ago, you added ollama"). +The `/dotfiles-sync` command is installed to `~/.claude/commands/` +during `chezmoi apply`, so it works from any directory in Claude Code, +not just the dotfiles repo. If it's missing, run `chezmoi apply` to +deploy it. + The manual commands in the sections below are fallbacks for when you're offline, SSH'd into a server, or want a quick one-off edit. You don't need to learn them to use this repo day-to-day. diff --git a/docs/llm-dotfiles.md b/docs/llm-dotfiles.md index 1a28952..7c57947 100644 --- a/docs/llm-dotfiles.md +++ b/docs/llm-dotfiles.md @@ -7,6 +7,10 @@ implementation. Copy it into your own LLM agent (Claude Code, Codex, OpenCode, etc.) and adapt it to your stack. Your agent will figure out the specifics. +

+ LLM sync workflow +

+ ## The core idea Most dotfiles repos assume you'll manually keep the repo in sync with @@ -218,12 +222,17 @@ You need two things: ### Step 1: Create the sync command -In Claude Code, create a slash command file: +Create a slash command file in two places: ``` -.claude/commands/dotfiles-sync.md +~/dotfiles/.claude/commands/dotfiles-sync.md # project-level (works in repo dir) +~/.claude/commands/dotfiles-sync.md # user-level (works from any dir) ``` +If you use chezmoi, add it to your source tree at +`home/dot_claude/commands/dotfiles-sync.md` and chezmoi deploys it to +`~/.claude/commands/` on apply. Otherwise, copy the file manually. + This file is the schema. It teaches the LLM what to scan, how to report, and how to sync. Write it as a prompt that instructs the LLM to: diff --git a/home/dot_claude/commands/dotfiles-sync.md b/home/dot_claude/commands/dotfiles-sync.md new file mode 100644 index 0000000..b34223e --- /dev/null +++ b/home/dot_claude/commands/dotfiles-sync.md @@ -0,0 +1,152 @@ +You are maintaining a chezmoi-managed dotfiles repo. The user operates their Mac freely (installs packages, edits configs, adds API keys). Your job is to detect what changed on the machine, report it clearly, and sync approved changes back into the repo. + +## Step 1: Read context + +Read `docs/sync-log.md` to understand when the last sync happened and what changed. If the file doesn't exist, this is the first sync. + +## Step 2: Scan for drift + +Run these detection commands in parallel where possible: + +### Config drift +```bash +chezmoi status 2>/dev/null +``` +Look for lines starting with ` M` (modified) or `MM` (modified both sides). + +### Brew packages +```bash +# Installed but not in Brewfile +comm -23 <(brew leaves | sort) <(grep '^brew "' ~/.Brewfile 2>/dev/null | sed 's/brew "//;s/".*//' | sort) + +# In Brewfile but not installed +comm -13 <(brew leaves | sort) <(grep '^brew "' ~/.Brewfile 2>/dev/null | sed 's/brew "//;s/".*//' | sort) +``` + +### Cask apps +```bash +# Installed but not in Brewfile +comm -23 <(brew list --cask 2>/dev/null | sort) <(grep '^cask "' ~/.Brewfile 2>/dev/null | sed 's/cask "//;s/".*//' | sort) + +# In Brewfile but not installed +comm -13 <(brew list --cask 2>/dev/null | sort) <(grep '^cask "' ~/.Brewfile 2>/dev/null | sed 's/cask "//;s/".*//' | sort) +``` + +### VS Code extensions +```bash +# Installed but not tracked +comm -23 <(code --list-extensions 2>/dev/null | sort) <(sort ~/.config/code/extensions.txt 2>/dev/null) + +# Tracked but not installed +comm -13 <(code --list-extensions 2>/dev/null | sort) <(sort ~/.config/code/extensions.txt 2>/dev/null) +``` + +### New fish functions (not managed by chezmoi) +```bash +# Functions on disk but not in source +comm -23 <(ls ~/.config/fish/functions/ 2>/dev/null | sort) <(chezmoi managed | grep 'fish/functions/' | xargs -I{} basename {} | sort) +``` + +### New SSH config fragments +```bash +# SSH config.d files not managed +comm -23 <(ls ~/.ssh/config.d/ 2>/dev/null | sort) <(chezmoi managed | grep 'ssh/config.d/' | xargs -I{} basename {} | sort) +``` + +### Hardcoded secrets in fish config +```bash +# Look for set -gx with what looks like API keys (long alphanumeric strings) +grep -n 'set -gx.*[A-Za-z0-9_]\{20,\}' ~/.config/fish/config.fish ~/.config/fish/conf.d/*.fish 2>/dev/null | grep -v 'onepasswordRead\|op://' || true +``` + +## Step 3: Report + +Present findings in plain language, grouped by category. For each category, show: +- What changed (specific names, not counts) +- Brief context if you can infer it ("ollama is probably for local LLM testing") + +Use this format: + +``` +Dotfiles sync report (YYYY-MM-DD) + +Config drift (N files): + - path — what changed (brief description of the diff) + +New packages (N brew, N casks): + Brew: pkg1, pkg2, ... + Cask: app1, app2, ... + +Stale entries (N brew, N casks): + Brew: pkg1, pkg2, ... (in Brewfile but not installed) + Cask: app1, app2, ... (in Brewfile but not installed) + +VS Code extensions: + New: ext1, ext2, ... + Removed: ext1, ... + +New fish functions (N): + func1, func2, ... + +New SSH configs (N): + host1, host2, ... + +Secrets: + [any findings or "no issues"] + +What would you like me to do? +``` + +If a category has no findings, omit it from the report. + +## Step 4: Wait for decisions + +Do NOT make any changes yet. Ask the user what to do. They'll respond in plain language: +- "Add the new packages" +- "Drop raycast and slack" +- "Keep btop in Brewfile even though not installed" +- "Sync the Zed config" +- "Do it all" + +## Step 5: Execute + +Based on the user's decisions: + +| Action | Method | +|--------|--------| +| Absorb config drift | `chezmoi re-add ` | +| Add brew packages | Edit `home/dot_Brewfile.tmpl`, add `brew "pkg"` in correct section | +| Remove stale brew | Edit `home/dot_Brewfile.tmpl`, delete lines | +| Add casks | Edit `home/dot_Brewfile.tmpl`, add `cask "app"` in correct section | +| Remove stale casks | Edit `home/dot_Brewfile.tmpl`, delete lines | +| Sync VS Code extensions | Update `home/dot_config/code/extensions.txt` | +| Track fish functions | `chezmoi add ~/.config/fish/functions/NAME.fish` | +| Track SSH configs | `chezmoi add ~/.ssh/config.d/NAME` | +| Register secrets | Append to `home/.chezmoidata/secrets.toml` | + +When editing the Brewfile, preserve the existing section structure (base/dev/apps). Place new entries in the appropriate section. + +## Step 6: Log + +Append an entry to `docs/sync-log.md`: + +```markdown +## [YYYY-MM-DD] sync + +[Category]: + - [what changed] + +--- +``` + +## Step 7: Commit + +Stage all changes and commit with a descriptive message: + +``` +chore(sync): dotfiles sync YYYY-MM-DD + +[Summary of changes by category] +``` + +Then ask: "Push to remote?" Only push if the user confirms. From 596a76c152150c66f239f8623f758c995868ba9e Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Tue, 14 Apr 2026 23:57:51 +0700 Subject: [PATCH 09/10] docs: add lifecycle section (install, update, uninstall) - Guide section 9: full lifecycle coverage with install steps, update methods (LLM + manual + reinstall), and complete uninstall procedure (restore shell, remove configs, remove Claude command, optionally uninstall Homebrew) - README: lifecycle quick-reference table - Architecture renumbered to section 10 Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 10 ++++++ docs/guide.md | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 38d00d7..65e048a 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,16 @@ dotfiles doctor # health check Full command reference, walkthroughs, secrets management, multi-machine setup, and troubleshooting are in the **[user guide](docs/guide.md)**. +## Lifecycle + +| Stage | Command | +|-------|---------| +| **Install** | `git clone ... ~/dotfiles && cd ~/dotfiles && ./install.sh` | +| **Update** (LLM) | `/dotfiles-sync` in Claude Code | +| **Update** (manual) | `dotfiles update` (pull + apply) | +| **Reinstall** | `./install.sh --force` | +| **Uninstall** | See [guide](docs/guide.md#9-lifecycle-install-update-uninstall) | + ## Security This repo is safe to make public. Actual secrets (API keys, tokens, passwords) are never committed; only `op://` references to 1Password items appear in the source. Real values are resolved at `chezmoi apply` time and only exist on your machine. diff --git a/docs/guide.md b/docs/guide.md index 5005ef0..c0c0517 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -603,7 +603,104 @@ cd ~/dotfiles && ./install.sh --force --- -## 9. Architecture reference +## 9. Lifecycle: install, update, uninstall + +### Install (first time) + +```bash +git clone https://github.com/dwarvesf/dotfiles ~/dotfiles +cd ~/dotfiles && ./install.sh +``` + +This installs Homebrew, chezmoi, runs the setup wizard, deploys all +configs (including `~/.claude/commands/dotfiles-sync.md`), installs +packages, sets Fish as default shell, and prints a summary. + +After install, `/dotfiles-sync` is available in Claude Code from any +directory. See [section 1](#1-the-llm-workflow). + +### Update (ongoing) + +**Primary (LLM-assisted):** run `/dotfiles-sync` in Claude Code. Claude +detects drift, you approve, it syncs. + +**Pull from remote** (new commits from another machine): + +```fish +dotfiles update # git pull + chezmoi apply +``` + +**Re-run setup wizard** (change name, email, editor, 1Password config): + +```fish +chezmoi init # re-prompts for all answers +chezmoi apply # deploy with new answers +``` + +**Reinstall from scratch** (teardown + rebuild): + +```bash +cd ~/dotfiles && ./install.sh --force +``` + +This removes chezmoi state and config, re-links, re-runs the wizard, +and re-applies everything. + +### Uninstall + +There is no automated uninstall. To remove this dotfiles setup: + +**1. Restore default shell:** + +```bash +chsh -s /bin/zsh +``` + +**2. Remove deployed configs:** + +```bash +# See what chezmoi manages +chezmoi managed + +# Remove all chezmoi-managed files from $HOME +chezmoi managed | while read f; do rm -f "$HOME/$f"; done + +# Remove chezmoi state and config +rm -rf ~/.local/share/chezmoi +rm -rf ~/.config/chezmoi +``` + +**3. Remove the Claude Code slash command:** + +```bash +rm -rf ~/.claude/commands/dotfiles-sync.md +``` + +**4. Remove the repo:** + +```bash +rm -rf ~/dotfiles +``` + +**5. Optionally uninstall Homebrew packages:** + +```bash +# See what was installed via the Brewfile +brew bundle list --file=~/.Brewfile + +# Remove everything in the Brewfile +brew bundle cleanup --force --file=~/.Brewfile + +# Or uninstall Homebrew entirely +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)" +``` + +After uninstall, your Mac reverts to Zsh with default configs. Any +1Password secrets remain in your vault (unchanged). + +--- + +## 10. Architecture reference

Architecture overview From 47196f391d880a89d96f20aed3927b1291197c83 Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Tue, 14 Apr 2026 23:58:46 +0700 Subject: [PATCH 10/10] docs: clarify shell restore is optional during uninstall Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/guide.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/guide.md b/docs/guide.md index c0c0517..7a3ab53 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -650,12 +650,14 @@ and re-applies everything. There is no automated uninstall. To remove this dotfiles setup: -**1. Restore default shell:** +**1. Restore default shell** (if you're also removing Fish): ```bash chsh -s /bin/zsh ``` +Skip this if you want to keep Fish as your shell. + **2. Remove deployed configs:** ```bash