This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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/.
./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 submodulesmake 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, cronCI runs: Lint, Test, Hooks, Bootstrap, claude-review.
.zshrc sources in order:
.exports- Environment variables, PATH.zsh_prompt- Prompt with command timer (preexec/precmdhooks).aliases- Command aliases~/.extra- Personal customizations (not tracked)
bootstrap.sh runs two phases (see script for full details):
Phase 1 — Pull & packages (only with --pull/-p):
- Pulls latest from git
- Installs packages: Homebrew (macOS), apt (Debian/Ubuntu), or binary downloads to
~/.local/bin(SteamOS). On Linux/SteamOS, Tailscale is gated behindprompt_tailscale_install(skipped with a notice on non-TTY stdin unlessINSTALL_TAILSCALE=trueis set). - Refreshes package metadata (
brew update), then runsbrew bundle, which installs missing packages and upgrades outdated ones that the Brewfile lists (modernbrew bundleupgrades 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 cleanupis never run, so old versions are not reclaimed.
Phase 2 — Sync dotfiles (always runs):
- Sets default shell to zsh
- Symlinks
home/files to~ - Symlinks
.claude/{hooks,commands,contrib,agents,skills}/— this and steps 4–6 sit behindprompt_ai_install, which skips them (with a notice) on a non-TTY stdin unlessINSTALL_AI=trueis set - Installs Claude Code MCP servers
- Configures Claude Code
settings.local.jsonwith remote MCP URLs (skipped on gateway host) - Configures Antigravity (
agy) global rules (GEMINI.md) and MCP servers - Installs TPM (manual
prefix + Ifor plugins) - Installs bat/yazi/zellij themes from
vendor/ - Installs LaunchAgents (macOS) and cron jobs
- Clears stale symlinks for configs the repo no longer tracks (OpenClaw, spotify-player, iTerm2 dynamic profile) — see
cleanup_legacy_configs
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:
- Create the file under
home/mirroring the~path (e.g.,home/.config/zellij/config.kdl→~/.config/zellij/config.kdl) - Run
./bootstrap.sh -fto create the symlink - 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} |
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.mdsymlinked toCLAUDE.mdin repository roots for shared repo guidelines~/.gemini/GEMINI.mdsymlinked tohome/.claude/CLAUDE.mdin dotfiles for shared global instructionshome/.gemini/config/skills.jsondiscovers~/.claude/skillsso skills are defined oncehome/.gemini/config/hooks.jsonmaps AGYPreInvocationandStophooks to Zellij status (zj-status.sh)bootstrap.shprovisions MCP servers (github,obsidian) foragywhen 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.
Run ./bootstrap.sh -f to apply changes locally. Update home/.claude/hooks/README.md when adding/modifying hooks.