Skip to content

Latest commit

 

History

History
434 lines (323 loc) · 25.1 KB

File metadata and controls

434 lines (323 loc) · 25.1 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Repository Overview

This is a comprehensive macOS dotfiles repository that uses dotbot for configuration management and a unified dot CLI for day-to-day operations. A bootstrap-only Makefile exists for fresh clones (before ZSH is configured). The repository follows a modular architecture with "ingredients" (individual configuration modules) and "recipes" (combined installation profiles).

AI Coding Harness Configuration

Shared skills and harness-specific instructions are managed in a private repository (~/repos/claude-config). Dotbot installs and links each harness through its own ingredient:

  • claude~/.claude/
  • opencode~/.config/opencode/
  • pi-coding-agent~/.pi/agent/

Claude Code configuration is installed via the claude ingredient:

  • Repository: github.com:kahl-dev/claude-config (private)
  • Location: ~/repos/claude-config/
  • Runtime: Symlinked to ~/.claude/ during installation
  • Installation: Automatic via ./install-standalone claude or ./install-profile <profile>

The configuration includes:

  • Global instructions (GLOBAL.CLAUDE.md)
  • Custom skills (LIA framework, Jira, productivity tools)
  • Slash commands (experts, productivity, analysis)
  • Automation hooks (smart-lint, notifications)
  • MCP server configurations

Pi keeps GLOBAL.CLAUDE.md, settings.json, and prompt templates under ~/repos/claude-config/pi/. Its CLI is installed by mise from .config/mise/config.toml; authentication, sessions, and caches remain machine-local under ~/.pi/agent/.

This separation keeps agency-specific intellectual property private while allowing the public dotfiles to remain shareable.

Private Dotfiles

Host-specific and confidential configuration (work identity, SSH hosts, secrets) lives in a separate private repository and is wired in via the private dotbot ingredient:

  • Repository: github.com:kahl-dev/dotfiles-private (private)
  • Location: ~/repos/dotfiles-private/
  • Installation: Automatic via the private ingredient (included in all recipes). The private-macos ingredient adds macOS-only symlinks.
  • Failure mode: If cloning fails (SSH key missing, no access), the ingredient prints a warning and continues — the install does not abort.

Universal symlinks (all platforms):

  • ~/.zshenv-private → private env vars
  • ~/.gitconfig-local → work git identity

macOS-only symlinks (via private-macos):

  • ~/.ssh/config + ~/.ssh/config.d/private-hosts
  • ~/.hammerspoon/config/private.json + ~/.hammerspoon/config/devices.json
  • ~/.config/audio-manager/config.json

Privacy rule: Identifying or confidential config — work hosts/emails/keys, the whole .ssh/config, secrets, and device/home identifiers (display UUIDs, Bluetooth MACs, Home Assistant URLs/tokens/entity IDs) — belongs in dotfiles-private, never the public repo. If any part of a file is identifying, the whole file moves (a half-public split is how a work host once leaked into the public .ssh/config). Wiring and the move pattern live in meta/ingredients/private-macos.yaml.

Architecture & Structure

Core Components

  • meta/ - Contains dotbot configuration system
    • base.yaml - Base dotbot configuration with shell defaults
    • ingredients/ - Individual configuration modules (e.g., bat.yaml, neovim_build.yaml, tmux.yaml)
    • recipes/ - Installation profiles combining multiple ingredients (liadev, macos, pi, server)
  • scripts/ - Shell scripts for installation, setup, and maintenance
  • Configuration directories organized by tool:
    • zsh/ - Shell configuration and utilities
    • .config/ - XDG-compliant application configurations
    • tmux/ - Terminal multiplexer configuration
    • brew/ - Homebrew package management (separate for macOS/Linux)

Installation System

The repository uses a two-tiered installation approach:

  1. Profile-based: ./install-profile <recipe> (e.g., ./install-profile macos)
  2. Standalone modules: ./install-standalone <ingredient> (e.g., ./install-standalone neovim_build)

Both scripts use dotbot to process the appropriate YAML configurations, combining base settings with specific ingredients.

Profile Lock (.dotbot-profile)

On a successful ./install-profile <name>, the recipe name is written to .dotbot-profile at the repo root (git-ignored). This locks the machine to that profile:

  • ./install-profile <other> — hard-fails with the unlock hint (rm .dotbot-profile)
  • ./install-standalone <ingredient> — refused if the ingredient is not listed in the locked recipe
  • dot install (no args) — reads the lock and re-runs the locked profile; errors if no lock exists
  • The fzf dot menu header shows Profile: <name> (locked) or Profile: none

Switching profiles requires manually deleting .dotbot-profile. The lock is also guarded against corruption: an empty lock file or a lock pointing at a missing recipe both hard-fail with reset guidance.

Applied-Profile Stamp (~/.cache/dot-profile-applied)

A clean working tree says nothing about whether the commits in it have been through dotbot. As its last step, install-profile writes <sha> <profile> to ~/.cache/dot-profile-applied — last on purpose, so a run aborted by set -e never counts as converged.

  • dot status compares the stamp against HEAD and reports N unapplied when they differ
  • git/hooks-dotfiles/post-merge (and post-rewrite, a symlink to it) warns after any pull or rebase that moved HEAD past the stamp. Wired through core.hooksPath by meta/ingredients/base.yaml; warn-only, because a full profile run is 15 ingredients and too expensive to force on every merge
  • dot sync sets DOT_SYNC_RUNNING=1 around its own pull, so the hook stays quiet while sync is already applying

Use dot sync, never a bare git pull. A bare pull moves HEAD without running a single dotbot step, and nothing else notices. That is how zod, added to remote-bridge/package.json in July, stayed uninstalled on the Mac until a lazy require surfaced it a month later.

make install is a macOS-only bootstrap entry point (fails on non-macOS with instructions to use ./install-profile directly).

Tab Completion System

Two completion systems coexist:

  1. dot CLI (native ZSH): compdef _dot dot in zsh/config/dot.zsh

    • dot install profile <TAB> — completes from meta/recipes/
    • dot install standalone <TAB> — completes from meta/ingredients/*.yaml (sans extension)
    • dot update <TAB> — completes --yes / -y
    • Top-level commands complete with descriptions
  2. Direct scripts (bash compat): completion/dotfiles-completion.bash

    • Loaded from zsh/config/aliases.zsh after completion init
    • ./install-profile <TAB> and ./install-standalone <TAB>
    • Dynamically discovers options by scanning the filesystem

Common Commands

dot CLI (unified dotfiles command)

The dot command consolidates all dotfiles operations into a single interface with fzf-powered interactive menu and tab completion.

# Interactive menu (fzf)
dot

# Installation
dot install                     # Re-run the locked profile (reads .dotbot-profile)
dot install profile macos       # Install a dotbot profile (writes the lock on success)
dot install standalone tmux     # Install a standalone ingredient

# Homebrew
dot brew update                 # Update, upgrade & cleanup
dot brew dump                   # Export to Brewfile

# Shell
dot shell reload                # Reload ZSH configuration
dot shell reset                 # Reset zinit plugins and reload
dot shell clean                 # Remove stray ZSH files from HOME

# Neovim
dot nvim reset                  # Reset lazy.nvim packages

# Remote Bridge
dot rb start|stop|restart|status|logs

# Repository Fleet
dot repos                       # Show status of all repos (alias for repos status)
dot repos status                # Tabular: branch, dirty, ahead/behind
dot repos list                  # Print registered paths
dot repos pull                  # Fetch + pull --rebase all repos
dot repos push                  # Push repos with unpushed commits
dot repos sync                  # Pull all, then push all

# Maintenance
dot update                      # Interactive update wizard (LazyVim, Homebrew, repos, tmux, mise, herdr)
dot update herdr                # Only herdr: binary via handoff, integration drift, agent skill mirror
dot update --yes                # Skip confirmations
dot sync                        # Pull + re-run the locked profile (use instead of git pull)
dot status                      # Health dashboard, incl. applied-profile state
dot doctor                      # Diagnose broken symlinks, missing deps, stale caches
dot edit                        # Open dotfiles in $EDITOR
dot color-test                  # Terminal color test
dot help                        # Show all commands

Backward-compatible aliases still work: brewup, brewdump, zsh-reload, dotedit.

sm (ssh-mosh) — Mosh with auto tunnel

sm wraps mosh with an autossh tunnel for hosts opted in through Tag remote-bridge in SSH config. The tunnel forwards Remote Bridge and the Mac SSH agent through per-user Unix sockets on the remote host.

sm t3                           # Mosh + auto tunnel (detects Tag via ssh -G)
sm user@t3                      # Works with user@ prefix
sm --ssh="ssh -p 2222" t3       # All mosh args pass through
sm plainserver                  # No tag = plain mosh (no autossh needed)
sm-status                       # List active tunnels with PID, transport, session count
sm-kill t3                      # Force-kill tunnel for a host
sm-kill                         # Kill all active tunnels

Workflow: sm t3 starts the socket tunnel before mosh connects. Remote clients use ~/.ssh/remote-bridge.sock; git and other SSH clients use ~/.ssh/agent-tunnel.sock. Both paths remain constant across autossh reconnects, so tmux needs no refresh hooks or keybinding. The tunnel is removed when the last session closes.

Shared with hr: hr <host> (herdr remote attach) goes through the same lifecycle helpers — _remote_tunnel_ensure / _remote_tunnel_release in zsh/config/mosh.zsh — and counts as a session. Any new remote client must use them too; the failure mode when one doesn't is documented in the herdr.zsh header comment.

Host opt-in: Add Tag remote-bridge to each bridge-enabled host in the private SSH config. Do not add RemoteForward; sm owns both forwards so plain SSH sessions cannot replace a live tunnel. This requires OpenSSH 9.2 or newer on the Mac.

Self-healing: Before every autossh attempt, bin/sm-ssh-wrapper removes stale remote socket files over a forwarding-free SSH connection. Tunnel reuse requires a successful end-to-end /health check, and a 30-second heartbeat eventually destroys half-dead server connections after network roaming.

Future dot CLI Enhancements

Deferred from v1 (reviewed by multi-agent debate):

Command Purpose Complexity
dot uninstall Unified uninstall replacing scripts/uninstall.sh Low

dot doctor, dot status and dot sync have since shipped — see the Maintenance block above.

Bootstrap (Makefile)

For fresh clones before ZSH is configured:

make install                    # Run ./install-profile macos
make uninstall                  # Remove all symlinks
make help                       # Show available targets

Development Workflow

Adding New Configurations

  1. Create ingredient file in meta/ingredients/ (e.g., newapp.yaml)
  2. Add configuration files in appropriate directories (.config/, zsh/config/, etc.)
  3. Update recipe file in meta/recipes/ to include the new ingredient
  4. Test with standalone installation: ./install-standalone newapp

Modifying Existing Configs

  • Application configs: Edit files in .config/ or tool-specific directories
  • Shell configs: Modify files in zsh/config/ (sourced automatically)
  • Homebrew packages: Update brew/osx/.Brewfile then run brew bundle

Shell Configuration

The zsh setup uses modular configuration files in zsh/config/:

  • aliases.zsh - Command aliases and shortcuts
  • atuin.zsh - Atuin shell history integration (init only; binary comes from mise)
  • check_git_cleanup.zsh - Git branch cleanup reminders
  • claude-config.zsh - Claude Code scripts PATH setup (~/repos/claude-config/scripts)
  • fzf.zsh - Fuzzy finder integration
  • history.zsh - ZSH history configuration
  • keybindings.zsh - Custom key mappings
  • mise.zsh - mise runtime version manager activation
  • neovim.zsh - Neovim configuration
  • ngrok.zsh - Ngrok tunnel integration
  • node.zsh - Node.js version management (fnm fallback) and npm aliases
  • opencode.zsh - OpenCode aliases and fzf session picker (oc, occ, ocs, ocu)
  • plugins.zsh - Plugin management via zinit
  • prompt.zsh - Starship prompt configuration
  • mosh.zsh - sm command: mosh with auto Remote Bridge tunnel via autossh (sm, sm-status, sm-kill). Also owns the shared tunnel lifecycle (_remote_tunnel_ensure / _remote_tunnel_release) that hr reuses
  • herdr.zsh - hr command: bare hr starts herdr locally, hr <host> attaches remotely with --remote-keybindings server, without which the [[keys.command]] layers stay dead over --remote. Ensures the same tunnel as sm for the duration of the attach, so remote panes get a live SSH agent and Remote Bridge
  • remote-bridge.zsh - Remote Bridge clipboard/URL/notification integration
  • ssh-forward.zsh - sshl <host> <port>... / t3l <port>...: SSH with 1:1 local port forwards for OAuth login callbacks
  • ssh-agent.zsh - SSH agent management
  • zinit.zsh - Zinit plugin manager initialization
  • dot.zsh - Unified dot CLI with fzf menu, update wizard, and tab completion
  • repos.zsh - Multi-repo fleet management (dot repos status/pull/push/sync)
  • worktree.zsh - Git worktree helpers with git-native signatures (gwta, gwts, gwtl, gwtr, gwtp, gwtmain, gwth)

Node.js Runtime Management

mise is the preferred polyglot runtime version manager, replacing fnm for interactive shell use. fnm is kept as a fallback for non-interactive contexts (e.g., Makefile subshells in LIA/TYPO3 projects).

Architecture:

  • mise.zsh - Activates mise shell hooks (chpwd) for auto-switching on cd
  • node.zsh - Initializes fnm with --use-on-cd only when mise is NOT available
  • mise reads .nvmrc, .node-version, .tool-versions, and .mise.toml files natively (legacy_version_file = true in ~/.config/mise/config.toml)

Why both mise and fnm: LIA/TYPO3 projects use a shared handleNode.sh script (in lia-package/.tools/) that runs inside Makefile subshells. These non-interactive subshells don't trigger mise's chpwd hook, so handleNode.sh falls back to fnm/nvm for version switching. mise handles the interactive shell; fnm handles scripted version switches in team tooling.

Load order in .zshrc (order matters):

  1. Pi fnm PATH setup (adds /home/pi/.local/share/fnm to PATH if present, skipped when mise exists)
  2. mise.zsh - activates mise
  3. node.zsh - initializes fnm only if mise is absent

Raspberry Pi specifics: fnm is installed at a non-standard path (/home/pi/.local/share/fnm) on Pi. The PATH addition happens before node.zsh so that command_exists fnm succeeds and fnm initializes with --use-on-cd (not a bare fnm env which lacks auto-switching).

ZSH Helper Functions (zsh/utils.zsh)

Always use these helpers instead of raw shell commands for consistency:

Function Purpose
command_exists <cmd> Check if command is available
file_exists <path> Check if file exists
folder_exists <path> Check if directory exists
path_exists <path> Check if path exists (file or directory)
is_macos / is_linux Platform detection
is_raspberry_pi Detect Raspberry Pi
is_ssh_client Detect SSH environment
open_command <path/url> Cross-platform open
prompt_user <q> <yes> <no> Interactive prompt

Convention: Use command_exists over which, path_exists over [ -d ].

Git Worktree Helpers (zsh/config/worktree.zsh)

Project-agnostic worktree management with zero configuration. Auto-detects paths from git rev-parse --git-common-dir and discovers gitignored config files to copy.

Command Purpose
gwta [-b <branch>] <name-or-path> [commit-ish] Add worktree with git-native branch semantics
gwts [pattern] Switch worktrees (fzf interactive or pattern match on path/branch)
gwtl List worktrees with current marker, branch, and short SHA
gwtr [path] Remove worktree with safety checks (uncommitted changes, unpushed commits)
gwtp Prune stale worktree entries
gwtmain Jump to main worktree
gwth Show help

gwta branch logic (mirrors git worktree add):

  • gwta <name> — try checkout first (local + remote DWIM), fall back to create from HEAD
  • gwta <path> — git derives branch from basename (/, ./, ../ prefixed = path)
  • gwta <name> <ref> — checkout existing ref (branch, tag, commit) into specified folder
  • gwta -b <branch> <name-or-path> — create new branch from HEAD with custom folder name
  • gwta -b <branch> <name-or-path> <ref> — create new branch from ref with custom folder name

Path detection: bare names → <parent-of-main-worktree>/<name>, .//../// prefixed → resolved as path (git derives branch from basename)

Key design decisions:

  • Config detection uses git ls-files -z --others --ignored with size filter (<100KB), matching against basename only
  • Matches: *.local*, *.env*, config*.php, *config*.yaml, *.conf, *.ini, *.secrets*
  • Skips: *node_modules/*, *vendor/*, var/, .cache/, public/fileadmin/ (nested dirs too)
  • Uses zstat (via zmodload zsh/stat) for portable file size checks
  • Tab completion: gwta completes -b flag + branches/refs, gwts/gwtr complete worktree paths
  • gwts pattern matching searches both worktree path and branch name

Key Tools & Their Configs

Terminal & Shell

  • tmux: Advanced configuration in tmux/ with responsive status bar, remote-friendly defaults, and curated plugin setup
  • herdr: Agent-aware terminal multiplexer, primary for agent work; tmux stays configured alongside it. Binary self-managed by herdr's own updater (install.sh -> ~/.local/bin/herdr), installed by meta/ingredients/herdr.yaml when absent and deliberately not a mise tool: herdr disables herdr update for package-manager installs, and with it live handoff — the only update path that replaces a running server without killing panes and agents. dot update herdr is the whole update: binary, agent integrations, and the vendored agent skill. It runs herdr update --handoff, which hands off only when there is actually a release to install — after a fresh install.sh the binary is already current, so update is a no-op and an older running server must be replaced with herdr server stop (that does exit pane processes). A protocol bump is the other case handoff cannot bridge. There is no local attach handoff: --handoff is valid only on herdr update and on herdr --remote; for a remote server hr <host> --handoff forwards it. A leftover mise copy wins PATH over ~/.local/bin — the ingredient and dot update both warn, the fix is mise uninstall herdr && mise reshim && hash -r. Config in .config/herdr/ linked per file by meta/ingredients/herdr.yaml (herdr writes runtime state — session.json, logs, sockets — into the same ~/.config/herdr, so never link the whole directory). Keybindings mirror the tmux prefix/split/copy muscle memory; apps-menu.sh rebuilds the tmux Prefix+a apps layer as a popup; layout-menu.sh rebuilds the tmux Prefix+v panes-layout layer (grid rearrange via grid-layout.sh). Claude Code integration (skill + hook) lives in the claude-config repo. The pi-coding-agent ingredient installs Herdr's official Pi lifecycle/session extension; rows_by_agent.pi gives Pi the same three-row sidebar structure as Claude. Agent integrations carry their own version, independent of herdr's, and a release bumps only some of them — dot update herdr reinstalls whatever herdr integration status reports as outdated. The Claude hook is a vendored file inside the claude-config repo, so that reinstall surfaces as a diff there.
  • zsh: Modular config in zsh/config/ with automatic sourcing
  • starship: Prompt config in .config/starship.toml

Keybindings — read the docs first

Before changing any keyboard shortcut, read docs/keybindings.md. Bindings are layered (Karabiner → AeroSpace → Ghostty → tmux → TUIs) and an upper layer silently swallows keys the layers below need — the failure is invisible in the file you are editing. That document holds the layer model, the rule for which modifier each layer owns, a register of deliberate deviations with their reasons, and the diagnosis recipe for a key that stopped working.

The complete list of files carrying keybindings lives in that document's "Files that carry keybindings" table — it spans two repositories and is easy to under-count, so use the table rather than a list kept here.

Development Tools

  • atuin: Shell history sync/search. Installed via mise (.config/mise/config.toml), config in .config/atuin/ linked by meta/ingredients/atuin.yaml, shell init in zsh/config/atuin.zsh. Never source ~/.atuin/bin/env — the legacy curl install shadows the mise shim via PATH.
  • mise: Polyglot runtime manager in zsh/config/mise.zsh, config in ~/.config/mise/config.toml
  • fnm: Node.js version manager fallback in zsh/config/node.zsh (used when mise is absent)
  • neovim: Full configuration in .config/nvim/
  • git: Configuration in git/ directory
  • bat: Syntax highlighting themes in .config/bat/

macOS-Specific

  • Homebrew: Package definitions in brew/osx/.Brewfile
  • macOS defaults: System settings via scripts/setup_defaults_write.sh
  • Window management: AeroSpace, Hammerspoon, Karabiner configs

Remote Bridge System

@.claude/instructions/remote-bridge.md

Documentation Maintenance

This repo has three documentation layers (see global CLAUDE.md for general policy):

  • README.md (root) — Human-facing: features, installation, screenshots
  • CLAUDE.md (root) — AI-facing: architecture, file paths, commands
  • Topic READMEs (tmux/, remote-bridge/, meta/, .hammerspoon/) — Component-specific
  • docs/ — Long-form guides (tmux.md)

Update all affected layers in the same commit when changing documented functionality.

Troubleshooting

Common Issues

  • Broken symlinks: Run make uninstall then reinstall
  • Package conflicts: Check Homebrew with brew doctor
  • Neovim issues: Reset with dot nvim reset
  • Shell not updating: Run dot shell reload or restart terminal

System Dependencies

  • Requires macOS with Homebrew installed
  • Git must be available for submodule management
  • Some configurations require specific versions (e.g., PostgreSQL@15)

🎯 Project Management

This project uses GitHub Issues for tracking enhancements, bugs, and future improvements.

View all open issues →

To contribute or suggest improvements, please create a GitHub issue with detailed description and implementation ideas.

🎨 Theme Consistency Guidelines

Catppuccin Theme Implementation

All applications use Catppuccin Mocha flavor for consistent dark theme experience:

✅ Fully Themed Applications:

  • tmux: Catppuccin Mocha via plugin (tmux/tmux.conf)
  • bat: Catppuccin Mocha syntax highlighting (~/.config/bat/config)
  • btop: Catppuccin Mocha theme (~/.config/btop/btop.conf)
  • Neovim: Catppuccin with transparent background (~/.config/nvim/lua/plugins/catppuccin.lua)
  • Ghostty: Catppuccin Mocha terminal theme (~/.config/ghostty/config)
  • WezTerm: Catppuccin Mocha color scheme (~/.config/wezterm/wezterm.lua)
  • Starship: Catppuccin Macchiato palette (~/.config/starship.toml)
  • fzf: Catppuccin Mocha colors (zsh/config/fzf.zsh)
  • lazygit: Catppuccin Mocha theme (~/.config/lazygit/config.yml)
  • atuin: Catppuccin Mocha theme (.config/atuin/themes/catppuccin-mocha.toml, selected via [theme] name)

Color Palette (Mocha):

  • Background: #1e1e2e
  • Surface: #313244
  • Text: #cdd6f4
  • Blue: #89b4fa
  • Pink: #f38ba8
  • Lavender: #cba6f7
  • Yellow: #f9e2af
  • Rosewater: #f5e0dc

Theme Maintenance:

  • All terminal applications should use transparent backgrounds where possible
  • Maintain consistency with Catppuccin's official color values
  • Reference official Catppuccin repositories for updates
  • Test theme changes across all configured applications

Notes

  • The repository uses git submodules for tmux plugins and dotbot
  • Configuration files use XDG Base Directory specification where possible
  • Raycast settings are synced via cloud backup (not stored in dotfiles)
  • The system is designed for personal use and includes specific tools/preferences