Skip to content

Latest commit

 

History

History
251 lines (188 loc) · 15.2 KB

File metadata and controls

251 lines (188 loc) · 15.2 KB

AGENTS Guide: dotfiles

Project Overview

This is a personal dotfiles repository managed by chezmoi, targeting macOS (Apple Silicon) with some Linux support via chezmoi templates. There is no build system, no test suite, and no CI/CD.

Primary configuration languages: Lua (Neovim/LazyVim), Zsh, Bash, and Go templates (chezmoi .tmpl files). Supporting formats: TOML, YAML, HJSON, JSON.

Repository Structure

chezmoi/
├── dot_*                    # Home directory dotfiles (e.g., dot_zshenv -> ~/.zshenv)
├── private_dot_config/      # ~/.config/ (restricted permissions)
│   ├── nvim/                # Neovim configuration (LazyVim-based) — most active area
│   │   ├── lua/plugins/     # LazyVim plugin specs
│   │   ├── lua/config/      # Options, keymaps, lazy.nvim bootstrap
│   │   │   └── plugins/     # Extracted plugin configuration modules
│   │   └── ftplugin/        # Filetype-specific settings (25+ languages)
│   ├── zsh/                 # Zsh configuration (zinit, p10k)
│   ├── git/                 # Git config, global ignore, attributes
│   ├── wezterm/             # WezTerm terminal configuration
│   ├── kitty/               # Kitty terminal configuration
│   ├── opencode/            # OpenCode (AI agent) configuration
│   │   ├── agents/          # Global agent definitions (tdd, retrospective)
│   │   ├── commands/        # Global workflow commands (/tdd, /retro)
│   │   ├── plugins/         # Local plugins (loaded directly at startup)
│   │   └── skills/          # Agent skills (demand-loaded reference material)
├── dot_local/bin/           # Wrapper scripts (shadow Homebrew binaries via PATH priority)
└── dot_vim/                 # Legacy Vim configuration

Chezmoi Naming Conventions

  • dot_ prefix: installed as .filename (e.g., dot_zshenv -> ~/.zshenv)
  • private_ prefix: restricted permissions (e.g., private_dot_config/ -> ~/.config/ with no group/world access)
  • symlink_ prefix: creates a symbolic link; file contents are the link target
  • remove_ prefix: removes the corresponding entry from the target on chezmoi apply
  • executable_ prefix: installed with executable permissions
  • .tmpl suffix: Go template files processed by chezmoi with variable substitution
  • Files without special prefixes are installed as-is

For the full list of source state attributes, see the chezmoi reference.

OpenCode Permission Model

OpenCode's permission system uses a last-wins rule evaluation. Rules from multiple sources are flattened into a single list in this order:

  1. System defaults — built into OpenCode ("*": "allow" for most permissions)
  2. Agent-specific overrides — built-in per-agent rules (e.g., Plan denies edit)
  3. User global configopencode.json permission block
  4. User agent config — agent .md frontmatter permission block

evaluate uses findLast on the flattened list, so later rules override earlier ones. A "*": "ask" in opencode.json overrides the system default "*": "allow" for all agents.

Implications:

  • Write agents (Build, TDD) should not need bash permission blocks — the system default "*": "allow" is correct for them.
  • Read-only agents (Plan, Retro) need their own bash blocks with "*": "ask" or "*": "deny" to restrict bash, plus explicit allows for read-only tools.
  • Global opencode.json should only contain read restrictions (e.g., denying .env files). Do not add a global bash block — it will restrict write agents.

Before creating or modifying agent definitions, commands, tools, skills, or plugins under private_dot_config/opencode/, load the opencode-authoring skill.

Chezmoi Commands

# Apply all configurations to home directory
chezmoi apply

# Apply a single file (use the TARGET path, not the source path)
chezmoi apply ~/.config/opencode/AGENTS.md

# Preview changes before applying
chezmoi diff

# Add a new file to the repo
chezmoi add ~/.config/some/file

# Edit a managed file (opens in $EDITOR, applies on save)
chezmoi edit ~/.config/some/file

# List all managed files
chezmoi managed

# Re-initialize after editing .chezmoi.toml.tmpl
chezmoi init

Critical: Always edit files in the chezmoi source directory (this repository), never the deployed targets (e.g., ~/.config/, ~/.*). The source is the single source of truth; deployed files are overwritten by chezmoi apply. If you need to modify a deployed file like ~/.config/opencode/AGENTS.md, edit its source at private_dot_config/opencode/AGENTS.md in this repo, then run chezmoi apply to deploy.

Note: chezmoi does not remove files from the target when they are deleted from the source directory. If you delete or rename a file in the chezmoi source, you must manually remove the stale file from the target (e.g., ~/.config/), or use chezmoi forget and then delete the target file.

Important: After committing changes to chezmoi-tracked files (anything under dot_*, private_dot_config/, dot_local/, etc.), run chezmoi apply to deploy the changes to the home directory. Use chezmoi diff to preview what would change before applying.

Code Style: Lua (Neovim Configuration)

Formatting

Lua files are formatted with StyLua. Two configs exist:

  • private_dot_config/stylua.toml — global: 2-space indent, AutoPreferSingle quotes
  • private_dot_config/nvim/stylua.toml — nvim-specific: 2-space indent, 120 column width

Use -- stylua: ignore comments to suppress formatting on specific lines when needed.

Module Pattern

Use the standard Lua module idiom:

local M = {}

function M.someFunction()
  -- ...
end

return M

Plugin Specs (LazyVim)

  • Plugin specs are returned as Lua tables from files in lua/plugins/.
  • Larger plugin configs are extracted to lua/config/plugins/ and referenced via require():
opts = require("config.plugins.snacks").opts,

Filetype Settings

Filetype-specific settings in ftplugin/ follow this pattern:

vim.opt_local.expandtab = true
vim.opt_local.softtabstop = 2
vim.opt_local.shiftwidth = 2
vim.opt_local.formatoptions:append({ c = true, r = true, o = true, q = true })

Type Annotations

Use LuaLS ---@type annotations where they aid clarity:

---@type snacks.Config

Naming

  • snake_case for local variables and functions
  • Trailing commas in table definitions (always)

Code Style: Shell (Zsh / Bash)

  • Double-quote all variable expansions: "$HOME/.config", "$1"
  • Use if command -v somecmd &> /dev/null; then for command existence checks
  • Use source_if_exists helper function for safe file sourcing (defined in dot_bash_common)
  • 4-space indentation inside functions
  • Comments on their own line above the code they describe, not inline

Code Style: Go Templates (chezmoi .tmpl files)

  • Use {{ if eq .chezmoi.os "darwin" }} for OS-specific conditional blocks
  • Reference variables from .chezmoi.toml.tmpl via {{ .email }}, etc.
  • Keep template logic minimal; prefer separate files over complex branching

Formatting and Linting

  • Lua: StyLua (see configs above)
  • Markdown: Prettier (formatting) and markdownlint-cli2 (linting); one sentence per line (unbroken) for better diffs
  • Shell: No automated formatter; follow existing style

Pre-commit hooks are managed by prek (see prek.toml). Hooks run automatically on git commit and enforce trailing whitespace, final newlines, file format validation (YAML, TOML, JSON), markdownlint, prettier, and StyLua. Run prek run --all-files to check all files before committing.

When a pre-commit hook modifies files (e.g., prettier reformats), the commit is rejected and HEAD does not advance. To recover: git add the modified files and create a new git commit (do not use --amend, which would modify the previous unrelated commit).

Naming Conventions

Context Convention Example
Lua locals/functions snake_case diff_source, copy_selector
Lua exported helpers camelCase (LazyVim convention) M.rgbToHex, M.hslToHex
Shell variables UPPER_SNAKE_CASE for env vars $EDITOR, $XDG_CONFIG_HOME
Shell functions snake_case source_if_exists
chezmoi files Follow chezmoi prefix conventions dot_zshenv, private_dot_config

Git Commit Messages

  • Imperative mood, sentence case
  • End with a period
  • Short and direct, describing what the change does
  • Examples: Switch Python LSP to ty., Add opencode configuration., Remove archived barbecue plugin; just use winbar.

Design Decisions

  • Catppuccin Macchiato is the color theme across all tools (Neovim, WezTerm, Kitty, bat, broot, lazygit)
  • Vi/Vim keybindings are preferred everywhere (bash, zsh, tmux, readline, broot)
  • JetBrains Mono is the preferred font
  • mise is the runtime version manager (replacing asdf/pyenv)
  • LazyVim is the Neovim distribution base, with extensive customization
  • Per-directory git identity uses git's native includeIf mechanism via a local include chain:
    • The tracked git config includes ~/.config/git/config.local (silently ignored if absent).
    • config.local is untracked and contains includeIf "gitdir:..." directives pointing to per-org config files.
    • Per-org config files (also untracked) override user.email and other settings.
    • This keeps organization names out of the public repository.
  • Per-directory GitHub CLI account uses a gh wrapper script (dot_local/bin/executable_gh.tmpl) combined with mise environment variables:
    • The wrapper checks for a GH_USER environment variable. If set, it fetches the token for that user from the keyring via gh auth token -u "$GH_USER" and passes it as GH_TOKEN for that invocation.
    • If GH_USER is not set, the wrapper passes through to the real gh binary with default authentication.
    • Per-directory mise.toml files (untracked) set GH_USER to the appropriate GitHub account name.
    • The wrapper script contains no account names; identity mappings live in untracked mise config files.
    • The git credential helper in private_dot_config/git/config.tmpl must invoke the wrapper ($HOME/.local/bin/gh auth git-credential), not the real gh binary directly. If it bypasses the wrapper, git operations (push, fetch) will authenticate with the default active account instead of the per-directory account.
  • Wrapper scripts in dot_local/bin/ shadow Homebrew binaries via PATH priority (~/.local/bin appears before /opt/homebrew/bin). These wrappers add per-directory behavior (e.g., account selection) before delegating to the real binary. Other configuration that invokes these tools (e.g., git credential helpers, editor integrations) should reference the wrapper path, not the real binary, to preserve the per-directory behavior.
  • Shell PATH ordering relies on a deliberate sequence across multiple files:
    • .zshenv: brew shellenv sets HOMEBREW_PREFIX and adds homebrew to PATH; paths.zsh prepends ~/.local/bin, GOPATH/bin, and ~/.docker/bin. For non-interactive shells only ([[ ! -o interactive ]]): mise env output is parsed to prepend mise-specific paths (installs/ and shims/) to PATH and apply env vars as defaults (see "mise env vars in non-interactive shells" below). mise activate --shims is avoided because it also prepends /opt/homebrew/bin, which demotes ~/.local/bin wrapper scripts. Interactive shells skip mise here entirely — they get full activation in .zshrc.
    • /etc/zprofile (macOS system file, login shells only): path_helper reorders PATH, demoting user-added entries behind system paths. Because .zshenv skips mise for interactive shells, there are no mise entries for path_helper to demote.
    • .zshrc: paths.zsh is sourced again to re-prepend user paths after path_helper's reordering; mise activate zsh installs interactive precmd/chpwd hooks and prepends tool paths via its built-in _mise_hook.
    • paths.zsh uses typeset -aU path for deduplication then typeset +U path to remove the permanent unique constraint — without this, zsh silently blocks re-prepending entries that already exist elsewhere in PATH, which prevented mise tools from being moved back to the front after path_helper demoted them.
    • The homebrew lines in paths.zsh look redundant with brew shellenv but are required: path_helper demotes them for login shells, and the .zshrc re-source restores the correct order.
  • mise env vars in non-interactive shells: .zshenv uses mise env (not mise hook-env) for non-interactive shells, applying env vars from mise.toml as defaults only — if a var is already set in the inherited environment, the inherited value is preserved. This matters for OpenCode and other tools that spawn zsh -c subprocesses: inline overrides like AWS_PROFILE=repone-admin <cmd> work correctly because the inherited value takes precedence over the mise.toml default. mise hook-env cannot be used here because it unconditionally exports env vars (it does unset VAR then export VAR=value), which clobbers any inherited value. Interactive shells are unaffected — mise activate zsh in .zshrc uses precmd hooks that run after each command, so inline overrides naturally work.
  • OpenCode shell invocation model: OpenCode spawns each bash tool command as a new zsh -c <command> process (inheriting the user's login shell, not bash). Each invocation sources .zshenv (the only file zsh sources for non-interactive, non-login shells) and then runs the command. This means:
    • No persistent state between commands. export VAR=value in one command does not carry over to the next — each starts fresh from .zshenv.
    • Inline env var overrides work (after the .zshenv fix above) because the inherited value takes precedence over mise defaults.
    • To override an env var for a single command, use the inline prefix: AWS_PROFILE=repone-admin aws sts get-caller-identity.
    • To override for a chain of commands in one invocation, use export at the start: export AWS_PROFILE=repone-admin && aws sts get-caller-identity && cdk diff.

Important Notes

  • The dot_vimrc is a legacy config superseded by the Neovim/LazyVim setup in private_dot_config/nvim/.
  • The .chezmoiignore file prevents repo-level and development-only files from being deployed to the home directory. When adding files that should exist only in the source repo (e.g., documentation, linter configs, plan documents), add them to .chezmoiignore.
  • The primary target is macOS arm64; Linux support is handled via {{ if eq .chezmoi.os "linux" }} template conditionals.
  • The user works with: Python, Go, TypeScript/JavaScript, Lua, C/C++, C#/.NET, Docker, SQL, LaTeX, Markdown, Terraform, Ruby, and more — filetype configs exist for all of these.