Skip to content

Latest commit

 

History

History
136 lines (97 loc) · 9.73 KB

File metadata and controls

136 lines (97 loc) · 9.73 KB

CLAUDE.md

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

Repository Overview

Minimal dotfiles for zsh, git, vim, and tmux. Supports macOS (Homebrew), Debian/Ubuntu (apt), and SteamOS (binary downloads to ~/.local/bin). Dotfiles in home/ are symlinked to ~ on install. External themes are git submodules in vendor/.

Commands

./bootstrap.sh           # Sync dotfiles (symlink home/ to ~, prompts for confirmation)
./bootstrap.sh -f        # Sync dotfiles without confirmation
./bootstrap.sh -p        # Also pull latest and install/update packages
./bootstrap.sh -f -p     # Full sync without confirmation
./uninstall.sh           # Remove symlinks
git submodule update --init --remote  # Update theme submodules

Quality Gates

make check          # Run all quality gates (lint, test, hooks, bootstrap)
make lint           # Run shellcheck on all .sh files
make test           # Syntax check bash/zsh scripts
make test-hooks     # Test hook graceful degradation
make test-bootstrap # Test naming consistency, URL migration, cron

CI runs: Lint, Test, Hooks, Bootstrap, claude-review.

Architecture

Zsh Loading Order

.zshrc sources in order:

  1. .exports - Environment variables, PATH
  2. .zsh_prompt - Prompt with command timer (preexec/precmd hooks)
  3. .aliases - Command aliases
  4. ~/.extra - Personal customizations (not tracked)

Bootstrap Process

bootstrap.sh runs two phases (see script for full details):

Phase 1 — Pull & packages (only with --pull/-p):

  1. Pulls latest from git
  2. Installs packages: Homebrew (macOS), apt (Debian/Ubuntu), or binary downloads to ~/.local/bin (SteamOS). On Linux/SteamOS, Tailscale is gated behind prompt_tailscale_install (skipped with a notice on non-TTY stdin unless INSTALL_TAILSCALE=true is set).
  3. Refreshes package metadata (brew update), then runs brew bundle, which installs missing packages and upgrades outdated ones that the Brewfile lists (modern brew bundle upgrades by default). Packages not in a Brewfile — transitive dependencies, anything installed by hand — are left alone, as are Brewfile entries behind a false conditional (e.g. swiftlint/xcodegen, gated on Xcode being installed). brew cleanup is never run, so old versions are not reclaimed.

Phase 2 — Sync dotfiles (always runs):

  1. Sets default shell to zsh
  2. Symlinks home/ files to ~
  3. Symlinks .claude/{hooks,commands,contrib,agents,skills}/ — this and steps 4–6 sit behind prompt_ai_install, which skips them (with a notice) on a non-TTY stdin unless INSTALL_AI=true is set
  4. Installs Claude Code MCP servers
  5. Configures Claude Code settings.local.json with remote MCP URLs (skipped on gateway host)
  6. Configures Antigravity (agy) global rules (GEMINI.md) and MCP servers
  7. Installs TPM (manual prefix + I for plugins)
  8. Installs bat/yazi/zellij themes from vendor/
  9. Installs LaunchAgents (macOS) and cron jobs
  10. Clears stale symlinks for configs the repo no longer tracks (OpenClaw, spotify-player, iTerm2 dynamic profile) — see cleanup_legacy_configs

Symlink Pattern

Files in home/ are symlinked to ~ by bootstrap.sh. This allows version control of dotfiles while keeping them in their expected locations.

Adding new configs:

  1. Create the file under home/ mirroring the ~ path (e.g., home/.config/zellij/config.kdl → ~/.config/zellij/config.kdl)
  2. Run ./bootstrap.sh -f to create the symlink
  3. The existing file will be replaced with a symlink to the dotfiles version

Symlink-breaking tools: Some tools (CC, btop, zellij) do atomic writes that replace symlinks with regular files. Re-running ./bootstrap.sh -f restores them.

Handling sensitive data:

  • Never commit secrets (tokens, API keys, passwords)
  • Use environment variable substitution if the tool supports it (e.g., ${OBSIDIAN_API_KEY})
  • Store actual secrets in ~/.extra (sourced by zsh, not tracked)

Examples:

Tool Tracked Config Secrets Location
Claude Code home/.claude/settings.json ~/.claude/.credentials.json (not tracked)
Claude Code (per-machine) ~/.claude/settings.local.json (not tracked) Auto-generated by bootstrap for remote clients
obsidian-mcp home/.bin/obsidian-mcp-start ~/.extra via ${OBSIDIAN_API_KEY}

Key Components

Prompt (home/.zsh_prompt) - Timer displayed if command takes >0s.

Claude Code (home/.claude/) - Global config, agents, commands, contrib scripts, hooks, skills, settings.

Hooks (home/.claude/hooks/) - Shell scripts for Claude Code lifecycle events. See hooks/README.md for architecture and details. When adding or modifying hooks, update the README.

Skills (home/.claude/skills/) - Model-invoked domain expertise. Skills auto-apply when Claude detects matching context (e.g., editing hooks triggers hook-authoring patterns). Descriptions should be "pushy" — include natural language trigger phrases to combat undertriggering. Use skill-creator to optimize descriptions via eval loops. Install community skills with npx skills find <query> / npx skills add <owner/repo@skill>.

Commands (home/.claude/commands/) - User-invoked workflows. Explicit /command invocation (e.g., /work, /pr-review).

Zellij (home/.config/zellij/) - Terminal multiplexer config with Catppuccin Mocha theme, zjstatus bar, autolock plugin. Swap layouts in default.swap.kdl. Config changes require killing the session (zellij kill-all-sessions) — hot-reload doesn't work with symlinked configs (zellij-org/zellij#3992). The sysload widget reads from ~/.cache/sysload, populated by the com.evansenter.sysload LaunchAgent every 10s — zjstatus's command_* widgets spawn per zjstatus-plugin-instance (one per tab), so calling top synchronously inside the widget piles up under load and causes pane-frame flicker via hide_frame_for_single_pane.

Statusline (home/.claude/statusline-command.sh) - Custom statusline for Claude Code.

  • Format: [repo/session]:branch ✓/✗/↻ →#issues ● model context% (CI status hidden when dirty)
  • GitHub API calls (repo URL, PR number, PR body, CI status) are cached in $TMPDIR/claude-statusline-gh/ with per-call TTLs
  • Session name cached in $TMPDIR/claude-statusline/ (pre-populated by session-start hook)

Antigravity (agy) Co-existence (home/.gemini/) - Dual-agent support alongside Claude Code:

  • AGENTS.md symlinked to CLAUDE.md in repository roots for shared repo guidelines
  • ~/.gemini/GEMINI.md symlinked to home/.claude/CLAUDE.md in dotfiles for shared global instructions
  • home/.gemini/config/skills.json discovers ~/.claude/skills so skills are defined once
  • home/.gemini/config/hooks.json maps AGY PreInvocation and Stop hooks to Zellij status (zj-status.sh)
  • bootstrap.sh provisions MCP servers (github, obsidian) for agy when installed

Infrastructure Services - LaunchAgents on mac-mini, exposed via tailscale:

Service Port Tailscale Path LaunchAgent
agent-event-bus 8080 /agent-event-bus com.evansenter.agent-event-bus
agent-session-analytics 8081 /agent-session-analytics com.evansenter.agent-session-analytics
agent-memory-store 8083 /agent-memory-store com.evansenter.agent-memory-store
obsidian-mcp 3010 /obsidian-mcp com.evansenter.obsidian-mcp

LaunchAgent plists live in ~/Library/LaunchAgents/. The external service repos (event-bus, session-analytics, memory-store) have their own make install-server; obsidian-mcp is set up by this repo's bootstrap (claude mcp add + plist in LaunchAgents/, wrapper in home/.bin/obsidian-mcp-start, requires Obsidian with the Local REST API plugin running). Reload with launchctl unload + launchctl load.

Note: agent-memory-store is documented infra but is not provisioned by bootstrap (no claude mcp add, no AGENT_MEMORY_STORE_URL in settings.local.json) — it's registered out-of-band where used. The repo-managed LaunchAgents are: com.evansenter.obsidian-mcp (gateway only), com.evansenter.sysload (zellij CPU/RAM widget, if zellij installed), and com.user.cargo-sweep (periodic cargo sweep, if cargo installed).

Adding new LaunchAgents: Plists go in the top-level LaunchAgents/ directory (NOT home/Library/LaunchAgents/). Use __HOME__ as a placeholder for the user's home directory — install_launch_agent() in bootstrap does sed substitution at install time. Logs go to ~/.local/log/. Add an install_launch_agent call in install_launch_agents(), gated on the binary being present.

Host-gating: Services that only run on mac-mini (LaunchAgents, npm installs for server-side packages) must be gated with the is_gateway_host helper in bootstrap.sh. It matches ^mac-mini(-[0-9]+)?$, so the gate still holds if macOS Bonjour appends a -N LocalHostName suffix on an mDNS collision (e.g. mac-mini-2) — a bare == "mac-mini" match silently fails there and the gateway stops behaving as the gateway. Remote machines should only get MCP registration pointing at the Tailscale URL. The GATEWAY_HOST constant at the top of bootstrap.sh holds the Tailscale hostname.

Important: Never place projects in ~/Documents/ — macOS TCC blocks LaunchAgents from accessing it, causing silent PermissionError failures.

iTerm2 (preferences/, vendor/iterm-catppuccin/) - Manual color preset import required. preferences/iTerm.json is a hand-refreshed profile snapshot with no installer — a backup, not a source of truth, and not in Dynamic Profile format.

After Merging

Run ./bootstrap.sh -f to apply changes locally. Update home/.claude/hooks/README.md when adding/modifying hooks.