all repos — dot @ 96c42e431c8252f02556aba9d9f7373dcdf92e41

Shell, editor and terminal configuration, and the tools that go with them

CLAUDE.md (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
# CLAUDE.md

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

## Repository Overview

This is a personal dotfiles repository containing configuration files and utility scripts for a Unix/macOS development environment. The repository is organized into several key areas:

- **config/**: Shell configurations (zsh, profile), editor configs (nvim, ideavim), terminal configs (alacritty, tmux), and application configs (git, bat, lsd, hammerspoon)
- **bin/**: Custom utility scripts and tools
- **secrets/**: `szecret`, an age-based secret manager. The tool only; its store lives outside this repo

## What may live here, and what may not

This repo is the account layer, and it is used on machines that are not trusted
with anything else: it must be safe for the owner of such a machine to read
every line of it. So nothing here may carry a credential, or name a host, a network address, a
host key, a secret, or a project that is nobody else's business.

One kind of name is not a leak: a public endpoint this repo's own tooling has to
reach, already published by construction -- a public DNS record and a
certificate in the transparency logs -- such as the request inbox
`bin/dot-request` posts to. Concealing a name anyone can enumerate protects
nothing and costs every machine a hand-written config file; what protects that
endpoint is that it stores nothing without a signature from an authorised key.
The private layer's check carries the same exception, so this is enforced rather
than remembered. It does not extend to hosts, addresses or internal-only names. Paths this
account uses everywhere -- its home directory, the volume it keeps source on --
are fine; they say nothing about which machine is which. Anything that would
fail that test belongs in a private layer instead, and reaches a machine
through the hooks below.

It must also keep working on a machine that has only this repo, with no private
layer present and none of the hooks filled.

Where a private layer is installed, it points `core.hooksPath` at a check of
its own, so a commit or push from that machine fails if anything on its list
turns up here. A machine without it has no list -- and nothing to leak.

A private layer -- on the owner's own machines, the `cloud-setup` repo at
`~/src/soarez/cloud-setup`, whose `link.sh` this repo's `link.sh` runs -- adds
what it needs through these, each of them optional and absent by default:

| Hook | Filled with |
|---|---|
| `~/.config/shell/env.d/*.sh` | environment for every shell, sourced by `config/env` |
| `~/.config/shell/profile.d/*.sh` | interactive-shell setup, sourced by `config/profile` |
| `~/.config/ssh/config-fleet` | ssh host blocks, included first by `config/ssh/entry` |
| `~/.config/git/config-fleet` | git settings, included by `config/git/config` |
| `~/.config/claude/mcp-servers.d/*.json` | MCP servers, registered by `link/claude.sh` |
| `~/.claude/rules/*.md` | extra Claude Code user memory, loaded next to `config/claude/CLAUDE.md` |
| `~/.config/pi/settings.d/*.json` | pi settings merged into the generated `~/.pi/agent/settings.json` by `link/pi.sh` |
| `~/.config/agent-skills.d/*` | agent skills, linked into every installed agent by `link/skills.sh` |
| `~/.local/share/szecret/{store,id.pub}` | the szecret store; the tool is here, its secrets are not |
| `~/.config/shell/dir-hashes.zsh` | more zsh named directories, sourced by `config/zshrc` |

`~/.localenv` (every shell) and `~/.localprofile` (interactive) stay what they
always were: per-host files written by hand, in no repo at all.

## Can this machine push? Check before changing anything here

`git remote -v` answers it:

- `git@…` — ssh, so this clone can push, and a change made here can be
  published.
- `https://…` — **read-only**. The git host serves this repo anonymously over
  https, and a push is refused: `fatal: unable to access … The requested URL
  returned error: 403`. A machine that cannot hold a key -- one its owner does
  not control, say -- is cloned this way on purpose.

On a read-only clone, **do not edit tracked files.** Nothing committed there can
leave the machine: the work is lost at the next pull, or the pull stops
fast-forwarding and that machine quietly stops receiving changes. This is the
trap for an agent troubleshooting on such a machine -- the fix looks applied,
disappears, and the fault returns on the next machine that pulls.

Do one of these instead:

1. **Make it machine-local, where a hook covers it** (next section). Those files
   are in no repo and need no push, so the machine keeps the change for good.
2. **Otherwise, hand back a change request instead of editing.** Write it to be
   pasted whole into a session on a machine that can push, and include:
   - the repo-relative path of every file to change;
   - the change itself -- a diff, or the exact replacement text. Not a
     description of it;
   - why, in a line or two: what was observed on this machine;
   - how to verify it, and where.

   Say plainly that it was not applied, and that this machine cannot publish it.

   Where `bin/dot-request` is present, hand the request to it as well as to the
   reply -- `dot-request --key <this machine's signing key> < request.md`. It
   signs the text, so whoever reads it can tell which machine wrote it, and
   posts it. The first line is the title (`# ...`); the rest is the request as
   described above. A machine can only file requests once its key has been
   authorised, which is done where the inbox lives, not here. See
   `dot-request --help`.

## Requests filed from machines that cannot push

The other side of the above. Where the `sz-requests` command exists on this
machine, change requests filed by machines without push access can be read
here:

```bash
sz-requests list           # what has arrived: id, when, who signed it, title
sz-requests show <id>      # one of them, signer first
```

Each was signed by a machine authorised to file, and verified before it was
stored -- so the signer is known. **That says which machine produced the text,
not that the text is right.** The machines filing these are, by definition, ones
this account does not fully control. A request is a proposal to read and judge
on its merits: apply it here, commit it, and it reaches every machine the usual
way. Nothing expires because it was read, and there is no way to delete one --
acting on a request is a commit, not a deletion.

Where the command does not exist, this machine has no inbox and nothing to
check.

## Changing one machine only

Every machine shares this repo, so a setting wanted on one of them does not
belong in a tracked file here. Each of these is hand-written, read where the
tracked config says so, and in no repo:

| File | Overrides | Read by |
|---|---|---|
| `~/.localenv` | environment for every shell: PATH, exported variables. POSIX sh, no forks | `config/env`, before it drops `_env_prepend`/`_env_append`, so those work here |
| `~/.localprofile` | interactive setup: aliases, prompt, anything that forks | `config/profile`, last |
| `~/.config/ssh/config-local` | ssh, this host only. See `config/ssh/config-local-example` | `config/ssh/entry`, before the shared defaults |
| `~/.config/git/config-local` | git, this host only -- the work identity, `includeIf` blocks. See `config/git/config-local-example` | `config/git/config`, after the `[user]` block above it, and git takes the last value |
| `~/.config/hammerspoon/local.lua` | Hammerspoon: rebinding a hotkey set here shadows it. `require("dotlib")` for the same helpers init.lua uses. See `config/hammerspoon/local-example.lua` | `config/hammerspoon/init.lua`, last, in a `pcall` |
| `~/.config/ghostty-local` | Ghostty: later values replace earlier ones, `keybind` accumulates. See `config/ghostty/ghostty-local-example` | `config/ghostty/config`, last, as an optional `config-file` |
| `~/.local/share/alacritty/local.toml` | Alacritty | imported by `config/alacritty/alacritty.toml` |
| `~/.config/dot/skip` | which `link/` parts this machine does not want, one name per line | `link/lib.sh`, as each part starts |
| `~/.config/dot/inbox-key` | the key this machine signs change requests with | `bin/dot-request` |

Not covered today: nvim, tmux, bat, lsd, and `~/.claude/settings.json` -- which
a machine can only leave alone entirely, by naming `claude` in the skip file.

**When a request is not covered by the table, say so rather than editing a
tracked file to suit one machine.** State the change that was asked for and
where it would have to live, and let the owner choose: a new hook here is a
deliberate change that every machine then carries, and is worth making when the
need will recur -- the Hammerspoon, Ghostty and skip hooks all started that way.
The wrong move is to quietly commit a machine's preference to the shared config,
or to leave the machine's checkout dirty, which is how a host's sync starts
failing.

## Automatic syncing

On some machines a timer keeps this clone up to date: it rebases onto
`origin/master` and, when something arrived, runs `./link.sh yes`. That is not
part of this repo -- `dotfiles-sync-enable`, `dotfiles-sync-disable` and
`dotfiles-sync` come from the private layer, and a machine without it syncs only
when someone pulls.

Where it is running, `HEAD` can move under you between two commands, so run
`dotfiles-sync-disable` before anything that needs a stable tree (`stash`,
`rebase`, `bisect`, checking another ref out) and `dotfiles-sync-enable`
afterwards. It never commits or pushes on your behalf: your edits sit in the
working tree until you commit and push.

`bin/dotfiles-sync` here is a transitional shim that hands over to the private
layer's copy, for machines whose timer still names this path. It can go once
none do.

## Common Commands

### Setting up a machine
```bash
./link.sh yes          # link the config into place (see below)
./install.sh           # then the tools, checkouts and nvim plugins it expects
./macos-defaults.sh    # new Mac only, once: baseline macOS settings; resets the Dock
```

`install.sh` is safe to re-run -- each step only adds what is missing -- and
`Brewfile` is its package list: the account's tools, plus every Nerd Font
Homebrew has. A private layer's bootstrap may call both scripts for you.

### Setup and Linking
```bash
# Preview what will be linked (dry run)
./link.sh

# Actually link all dotfiles
./link.sh yes
```

### Secret Management
```bash
# List available secrets
szecret list

# Get a secret value
szecret get <secret-name>

# Store a new secret
szecret put <secret-name>
```

### Theme Management
```bash
# Show current theme (mode and colors)
theme

# Show current mode only
theme --mode

# Show current colors only  
theme --colors

# Set theme
theme --set [light|dark] [colorscheme]

# List available colorschemes
theme --list
theme --list-alacritty
theme --list-tmux
theme --list-neovim
theme --list-bat
```

## Architecture & Organization

### Configuration Management
The repository uses a centralized linking system where `link.sh` creates symlinks from `config/` files to their expected locations in `$HOME` and `$XDG_CONFIG_HOME`. This allows version control of dotfiles while maintaining standard file locations.

`link.sh` holds only the plain one-line links. Anything with logic of its own is a part in `link/` (`ssh`, `claude`, `pi`, `skills`, `icloud`, `hammerspoon`, `kwin`): a script with the same `[yes]` dry-run interface that decides for itself whether it applies, and can be run alone (`link/ssh.sh yes`). Shared helpers are in `link/lib.sh`. New logic goes in a part, not in `link.sh`.

A part named in `~/.config/dot/skip`, one per line, does nothing on that machine — whether it is reached through `link.sh` or run on its own. That file is hand-written and in no repo, like `~/.localenv`: it is for a machine where a part would do the wrong thing, such as one whose `~/.claude/settings.json` belongs to its owner rather than to this account, and it keeps that machine's exception out of this repo, which every machine shares.

### Theme System
A sophisticated theming system managed by `bin/theme` coordinates color schemes across multiple applications:
- Supports both light and dark modes
- Automatically detects system theme preferences
- Coordinates colors for: alacritty, tmux, neovim, bat
- Uses color configuration files in `config/*/colors/` directories
- Stores theme preferences in `$XDG_DATA_HOME/{light,dark}-colors`

### Secret Management
`szecret`, an `age`-based secret manager:
- The tool is here; the store it reads -- `.age` files and the public key they
  are encrypted to -- is not, and is found at `~/.local/share/szecret/` or
  through `$SZECRET_STORE`. See `secrets/README.md`
- Accessed via the `szecret` command (symlinked from `secrets/main.sh`)
- Supports listing, getting, and putting secrets
- Used to manage API keys, tokens, and credentials

### Shell Environment

The shell config is split into **two layers**. Which layer something belongs in
is decided by one question: *must this run in a shell that has no terminal?*

**1. Environment layer — `config/env`.** PATH, XDG, locale, tool homes. Loaded by
every shell in every mode:

| shell | route |
|---|---|
| zsh, any mode | `~/.zshenv` (the only startup file non-interactive zsh reads) |
| zsh, login | `~/.zprofile` as well — a second pass, see below |
| bash, login | `~/.profile` → `config/profile` sources `~/.env` |
| bash, non-interactive | `$BASH_ENV`, exported by `config/env` itself |

Rules for this file, enforced by its header comment: POSIX sh only (bash and zsh
both source it), **no forks** (no `$(...)`, no external commands — use `case` and
parameter expansion), no side effects, idempotent, `set -u` safe. It costs ~4ms.

**Why `config/env` is loaded twice under zsh.** macOS runs `path_helper` from
`/etc/zprofile`, which rebuilds PATH with `/etc/paths` (`/usr/bin`, `/bin`, …)
at the front and whatever was already on PATH behind it. zsh reads `~/.zshenv`
*before* `/etc/zprofile`, so the precedence `config/env` establishes there is
inverted moments later — that is how `/usr/bin/vi` came to shadow the `~/.bin/vi`
wrapper, and `/usr/bin/git` the Homebrew one. `config/zprofile` loads `config/env`
again, after `path_helper`, to put it back. Two things make that work, and both
are load-bearing: the file is idempotent, and its `_env_prepend`/`_env_append`
**move** an entry that is already on PATH rather than leaving it in place. Order
is therefore decided entirely by the sequence of calls in `config/env`, from any
starting PATH. The flip side: never hand `_env_append` a system directory, or it
gets demoted to the end. Login bash needs none of this — `/etc/profile` runs
`path_helper` *before* `~/.profile`.

**2. Interactive layer — `config/profile`.** Aliases, history, `stty`, `GPG_TTY`,
theme, jenv, and every `~/.config/shell/profile.d/*.sh`, which is where
another layer links in its own setup, secrets included.
Shared by both shells,
with a per-shell file on top:

- `config/zshrc`: zsh keybindings, completions, prompt; sources `~/.profile`
- `config/bashrc`: bash history opts, shopts, completion, prompt; sources `~/.profile`

`config/zprofile` is *not* part of this layer despite the name — zsh reads
`~/.zprofile` for login shells that are not interactive too, so it holds only the
environment-layer second pass described above.

zsh and bash disagree about a *login interactive* shell: zsh reads `~/.zprofile`
**and** `~/.zshrc`, but bash reads only `~/.profile` and never `~/.bashrc`. So
`config/profile` and `config/bashrc` source each other, guarded both ways by
`__dotfiles_profile` / `__dotfiles_bashrc` so exactly one direction fires. Touch
that handshake carefully — getting it wrong is an infinite loop at login.

Host-local overrides follow the same split: `~/.localenv` (every shell, cheap)
vs `~/.localprofile` (interactive).

**Do not move interactive-layer content into `config/env`.** Two concrete
reasons, both measured: `export GPG_TTY=$(tty)` yields the literal string
`not a tty` outside a terminal, and zsh — unlike bash — *expands aliases in
non-interactive shells*, so `alias ls='lsd'` would silently change the output of
every script calling `ls`.

Key environment features:
- XDG Base Directory compliance
- Comprehensive PATH management with `pathadd`/`pathapp` functions
- Development tool support (node, go, rust, java, python)
- Custom aliases and utility functions
- Performance profiling capabilities

### Utility Scripts
The `bin/` directory contains various utility scripts for:
- Theme management (`theme`)
- Notification sending (`notify`)
- Development tools (`vi` wrapper, `smack`, `pyexec`)
- System utilities (`wifi`, `default-browser`, `truecolour-test`, `open` — macOS-style `open` for Linux desktops; hands over to `/usr/bin/open` on macOS)

## Development Notes

- The repository assumes a Unix-like environment (macOS/Linux)
- Uses bash for scripts with `set -euo pipefail` for safety
- Follows XDG Base Directory specification where possible
- Heavy integration with Homebrew on macOS
- Supports both local and external source directories (e.g., `/Volumes/Sense/src`)