Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,29 @@ At the end of a run it also does a read-only **commit-signing check** (`check_gi
- **`config/starship.toml`** — Single-line Starship prompt with Nerd Font symbols
- **`config/mise/config.toml`** — Global mise settings (legacy version files, experimental features)
- **`claude/`** — Claude Code config symlinked into `~/.claude`: `settings.json` (permissions, status line, `SessionStart` hook, plus the enabled plugins and known marketplaces that drive the install-script plugin step), `statusline.sh` (hostname-led status line), `hooks/machine-context.sh` (a `SessionStart` hook that injects the hostname into the model's context so it knows which machine it's on), and `bin/rubocop-mcp` (a per-repo-adaptive rubocop MCP launcher, registered user-scope by the install script). Note: `settings.json` is a tracked file but Claude also writes to `~/.claude/settings.json` at runtime (theme, plugin state), so machine-local tooling that rewrites it — e.g. peon-ping — is intentionally **not** tracked here; its hooks land in `~/.claude/settings.json.backup` after an install run
## Defender / ~/dev

Microsoft Defender for Endpoint (managed, can't be disabled) scans every file dev
tools touch during a build — millions of filesystem ops that peg the CPU and push
the machine into swap. The agreed fix with endpoint security is a single low-risk
Defender exclusion of `/Users/*/dev`, so **everything a build writes must live under
`~/dev`**.

`config/fish/config.fish` enforces this by pointing the XDG base dirs there —
`XDG_CACHE_HOME=~/dev/.cache`, `XDG_DATA_HOME=~/dev/.local/share`,
`XDG_STATE_HOME=~/dev/.local/state` — while `XDG_CONFIG_HOME` stays at `~/.config`
(config doesn't churn and these dotfiles live there). Tools that ignore XDG get an
explicit override in the same file (npm cache, the pnpm store, cargo/rustup, go,
uv, bundler). Anything XDG-aware (mise, podman, pnpm's cache/state/global) just
inherits the base dirs.

Consequences to keep in mind:
- **Projects go in `~/dev`**, not `~/Code` — `node_modules`/`dist`/`build`/`target`
only fall under the exclusion if the project itself does.
- These vars are set in fish, so they only apply to processes launched from the
shell. A build kicked off from a GUI app (IDE, Podman Desktop) won't see them
unless the vars are also exported at the launchd/session level.

## Conventions

- Commit messages: **short, imperative, title case** (e.g. `Add Pagination to Query Endpoint`). No bullet-point bodies unless a single sentence of context is genuinely needed.
Expand Down
32 changes: 32 additions & 0 deletions bin/xdg-launchd-env
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/sh
# Export the ~/dev storage vars into the GUI (launchd) session so apps
# launched from the Dock/Finder — IDEs, Podman Desktop — write their build
# churn under ~/dev too, not just shells. Without this, a GUI-triggered build
# uses the default locations (~/.npm, ~/.local/share, …) and escapes the
# Microsoft Defender exclusion of /Users/*/dev.
#
# Run at login by the nl.iain.xdg-env LaunchAgent (installed by install.rb),
# and safe to run by hand to refresh a session without logging out. Keep the
# values in sync with config/fish/config.fish — that's the shell counterpart.
# See dotfiles CLAUDE.md § "Defender / ~/dev".

set -eu

cache="$HOME/dev/.cache"
data="$HOME/dev/.local/share"
state="$HOME/dev/.local/state"

launchctl setenv XDG_CONFIG_HOME "$HOME/.config"
launchctl setenv XDG_CACHE_HOME "$cache"
launchctl setenv XDG_DATA_HOME "$data"
launchctl setenv XDG_STATE_HOME "$state"
launchctl setenv npm_config_cache "$cache/npm"
launchctl setenv PNPM_HOME "$data/pnpm"
launchctl setenv PNPM_CONFIG_STORE_DIR "$data/pnpm/store"
launchctl setenv CARGO_HOME "$data/cargo"
launchctl setenv RUSTUP_HOME "$data/rustup"
launchctl setenv GOPATH "$data/go"
launchctl setenv GOMODCACHE "$data/go/pkg/mod"
launchctl setenv GOCACHE "$cache/go-build"
launchctl setenv UV_CACHE_DIR "$cache/uv"
launchctl setenv BUNDLE_USER_HOME "$data/bundle"
31 changes: 28 additions & 3 deletions config/fish/config.fish
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
# ── XDG base directories ──────────────────────────────────────────────
# Config stays in ~/.config (these dotfiles). Everything else that churns
# during builds — caches, package stores, toolchains, state — is redirected
# under ~/dev so a single Microsoft Defender exclusion (/Users/*/dev) covers
# it all. See CLAUDE.md § "Defender / ~/dev" for the why.
set -gx XDG_CONFIG_HOME "$HOME/.config"
set -gx XDG_CACHE_HOME "$HOME/dev/.cache"
set -gx XDG_DATA_HOME "$HOME/dev/.local/share"
set -gx XDG_STATE_HOME "$HOME/dev/.local/state"

# ── Dev-tool storage → ~/dev ──────────────────────────────────────────
# Tools that don't honour the XDG base dirs on their own get pointed at
# ~/dev explicitly so nothing escapes the Defender exclusion. Tools that
# already follow XDG (mise, pnpm cache/state/global, podman) inherit the
# vars above and need nothing here.
set -gx npm_config_cache "$XDG_CACHE_HOME/npm" # default ~/.npm
set -gx PNPM_CONFIG_STORE_DIR "$XDG_DATA_HOME/pnpm/store" # store ignores XDG
set -gx CARGO_HOME "$XDG_DATA_HOME/cargo" # default ~/.cargo
set -gx RUSTUP_HOME "$XDG_DATA_HOME/rustup" # default ~/.rustup
set -gx GOPATH "$XDG_DATA_HOME/go" # default ~/go
set -gx GOMODCACHE "$XDG_DATA_HOME/go/pkg/mod"
set -gx GOCACHE "$XDG_CACHE_HOME/go-build" # default ~/Library/Caches/go-build
set -gx UV_CACHE_DIR "$XDG_CACHE_HOME/uv" # default ~/Library/Caches/uv
set -gx BUNDLE_USER_HOME "$XDG_DATA_HOME/bundle" # default ~/.bundle

if test -x /opt/homebrew/bin/brew
/opt/homebrew/bin/brew shellenv fish | source
Expand Down Expand Up @@ -119,8 +142,8 @@ end

test -e {$HOME}/.iterm2_shell_integration.fish ; and source {$HOME}/.iterm2_shell_integration.fish

# pnpm
set -gx PNPM_HOME "/Users/iain/Library/pnpm"
# pnpm — global bin dir under ~/dev (matches pnpm's XDG-derived globalBinDir)
set -gx PNPM_HOME "$XDG_DATA_HOME/pnpm"
if not string match -q -- "$PNPM_HOME/bin" $PATH
set -gx PATH "$PNPM_HOME/bin" $PATH
end
Expand All @@ -131,6 +154,8 @@ if command -q mise
set -gx MISE_EXPERIMENTAL "1"
mise activate fish | source
# pitchfork activate fish | source
fnox activate fish | source
if command -q fnox
fnox activate fish | source
end
abbr -a pf "pitchfork"
end
41 changes: 41 additions & 0 deletions install.rb
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,47 @@ def symlink(src, dest, dry_run:, counts:)
end
end

# LaunchAgent that exports the ~/dev XDG storage vars into the GUI (launchd)
# session, so apps launched outside the shell — IDEs, Podman Desktop — keep
# their build churn under ~/dev too and stay out of Microsoft Defender's way
# (see CLAUDE.md § "Defender / ~/dev"). Generated per-machine because launchd
# plists take absolute paths, not ~; it runs bin/xdg-launchd-env, symlinked
# into ~/.local/bin above.
xdg_agent = HOME / "Library" / "LaunchAgents" / "nl.iain.xdg-env.plist"
if xdg_agent.exist?
puts " skip #{xdg_agent} (already exists)"
elsif dry_run
puts " [dry-run] would create and bootstrap #{xdg_agent}"
else
FileUtils.mkdir_p(xdg_agent.dirname)
xdg_agent.write(<<~PLIST)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>nl.iain.xdg-env</string>
<key>ProgramArguments</key>
<array>
<string>#{BIN_DEST / "xdg-launchd-env"}</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
PLIST
puts " created #{xdg_agent}"

domain = "gui/#{Process.uid}"
# Re-bootstrap cleanly in case a stale copy is already loaded.
system("launchctl", "bootout", domain, xdg_agent.to_s, out: File::NULL, err: File::NULL)
if system("launchctl", "bootstrap", domain, xdg_agent.to_s)
puts " bootstrapped nl.iain.xdg-env (#{domain})"
else
warn " WARNING: could not bootstrap #{xdg_agent}; it will load at next login"
end
end

puts "\nSymlinks: #{counts[:linked]} linked, #{counts[:skipped]} skipped, #{counts[:backed_up]} backed up."

# Install Homebrew if missing
Expand Down