all repos — dot @ f31a7a6d69713d45463d53765c9e39aa06290f8c

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

f31a7a6d
hammerspoon: the example's header is init.lua's, so bindings move either way

It unpacked only hotkey and mods, so a line copied from init.lua -- which
writes `hotkey('9', csa, type_text(...))` with bare names -- did not run here,
and a line moved the other way had to be rewritten. The header is now the same
block init.lua has, name for name, and the examples use the bare names too.

Unused names in it are harmless, and keeping them whole is the point: paste a
binding in either direction and it works as written.
Igor Soarez igor@soarez.org
Mon, 21 Sep 2026 18:53:13 +0300
94a8f1ff
hammerspoon: shared helpers move to dotlib.lua, which local.lua can require

A machine's local.lua is a separate chunk, so init.lua's locals were invisible
to it and anything it wanted -- hotkey(), the mod tables, type_text -- had to
be copied. They live in dotlib.lua now, linked beside init.lua, and both files
reach them the same way: require("dotlib").

init.lua keeps its own vocabulary by aliasing what it uses, so the 500 lines
below are untouched. It prepends its own directory to package.path first:
Hammerspoon already puts the config directory there (Contents/Resources/
setup.lua), but taking it from this file's path holds however the config was
found. A machine that pulls without re-linking has no dotlib.lua beside
init.lua, which would lose every hotkey silently -- that case notifies and
names the fix.

Checked with a stubbed hs: the module's hotkey, focus, execute, type_text and
type_current_date all behave, and local-example.lua binds its four keys
through it.
Igor Soarez igor@soarez.org
Mon, 21 Sep 2026 18:30:47 +0300
7f004995
hammerspoon, ghostty: example files for their machine-local configs

ssh and git each ship one; these two hooks were discoverable only by reading
the config that loads them. Both examples carry what is easy to get wrong:
init.lua's hotkey() helper and mod tables are locals, so a local.lua calls
hs.hotkey.bind directly, and a binding there can only be shadowed, never
deleted; in Ghostty a single-value key replaces while keybind accumulates, and
`ignore` is how a binding is switched off rather than added to.

Checked rather than assumed: the lua parses, and the Ghostty example validates
and, dropped in as ~/.config/ghostty-local, does what it says -- font-size,
command and background-opacity replaced, super+shift+n added, alt+enter turned
off.
Igor Soarez igor@soarez.org
Mon, 21 Sep 2026 17:41:45 +0300
f957dd3a
CLAUDE.md: where a single machine's changes go, and what to do without a hook

The hooks existed but were documented nowhere together: ~/.localenv and
~/.localprofile were mentioned in passing, the skip file under the link
system, and the ssh, git, Hammerspoon, Ghostty and Alacritty ones only in the
config files that read them. Anyone -- an agent especially -- asked to change
one machine had to read the whole repo to find out whether that was possible
without committing to it.

They are one table now, with what each overrides and what reads it, plus what
has no hook at all. And the rule for everything else: say what is being asked
for and where it would have to live, rather than putting one machine's
preference in the config every machine shares, or leaving that machine's
checkout dirty and its sync failing.
Igor Soarez igor@soarez.org
Mon, 21 Sep 2026 17:40:22 +0300
6c159347
ghostty: read ~/.config/ghostty-local last, where a machine has one

Ghostty had no way to differ per machine. It cannot take the shape the other
tools use -- a file beside the linked one -- because link.sh links this whole
directory, so ~/.config/ghostty is the checkout itself and a sibling file
would be an untracked file in the repo every machine shares.

An optional include at the end does the same job: later values replace earlier
ones, so a local file overrides anything here, the theme colours included, and
its keybinds add to these. Verified against the shipped config: font-size 12
becomes 18 with a local file, back to 12 without, and the config still
validates when there is none.
Igor Soarez igor@soarez.org
Mon, 21 Sep 2026 17:18:53 +0300
b3108c15
hammerspoon: load ~/.config/hammerspoon/local.lua where a machine has one

The shell has ~/.localprofile and ssh and git their config-local; Hammerspoon
had nothing, so a machine wanting one different hotkey had to edit the file
every machine shares. Only init.lua is linked, so the directory around it is
already the machine's own -- a sibling file there is in no repo.

Loaded last, so it can override anything above, and through pcall: a mistake
in a local file must not take the rest of the config down. Reload with
ctrl-shift-alt-h as usual.
Igor Soarez igor@soarez.org
Mon, 21 Sep 2026 15:51:53 +0300
d312cb2a
zshrc: find Homebrew's completions on the prefix this machine uses

fpath carried /usr/local/share/zsh-completions, which is wrong twice over:
Apple Silicon puts Homebrew under /opt/homebrew, and formulae install their
completions into share/zsh/site-functions, not the share/zsh-completions of
the formula by that name. The directory existed on neither Mac, so _brew,
_bat, _deno and the rest have been missing all along -- silently, since a
missing fpath entry says nothing.

All four candidates are probed now, HOMEBREW_PREFIX first where a machine
exports it. No `brew --prefix`: that forks on every interactive shell, and
this file already pays for one of those further down.
Igor Soarez igor@soarez.org
Mon, 21 Sep 2026 15:26:23 +0300
73031b77
Brewfile: keepingyouawake, and say that Mac apps belong here

Installed by hand on machine after machine, which is the signal it belongs in
the list. Nothing in this repo uses it, so the header now says what the casks
were already doing: the few Mac apps this account wants wherever it works,
alongside the tools the config itself needs.
Igor Soarez igor@soarez.org
Mon, 21 Sep 2026 15:16:57 +0300
f671fb67
supermaven off unless asked for; a blocked cask no longer ends install.sh

Supermaven was running everywhere. Its `condition` option reads the other way
round from what the code assumed -- `true` stops it -- so `return false` meant
"never stop", and the plugin loaded on InsertEnter, started its agent and
offered inline suggestions on every machine. Only blink's source for it was
off, which is what made it look disabled. It sends buffer context to a third
party, so it is now opt-in: DOT_SUPERMAVEN=1, in ~/.localenv where it is
wanted. Off, lazy keeps both plugins off disk entirely.

install.sh no longer stops at brew bundle either. A managed machine can refuse
a cask outright -- "disabled because it does not pass the macOS Gatekeeper
check" -- and brew bundle fails the whole run for one such entry, which left
the checkouts, nvim plugins and colour files undone. It warns and carries on;
brew has already named the entry.
Igor Soarez igor@soarez.org
Mon, 21 Sep 2026 14:57:57 +0300
b658c623
Brewfile: say why bash is here, correctly

The reason given named bin/open, which does not need it: on macOS it execs
/usr/bin/open on line 13, before anything 3.2 cannot parse. What does need it
is szecret's age bootstrap -- `${os,,}` in secrets/common.sh, reached only when
age is absent, which is precisely the state a machine is in before this file
has been applied.
Igor Soarez igor@soarez.org
Mon, 21 Sep 2026 14:38:57 +0300
352bd588
install.sh: install Homebrew, add bash, and seed the colour files

Three things a machine set up from this repo alone hit on its first run:

Homebrew was a dead end -- install.sh printed the command and exited, leaving
the one prerequisite everything else needs to be run by hand. It installs it
now, NONINTERACTIVE so it does not wait on a RETURN; the sudo password is still
asked for, which is why this script is run by hand.

bash was missing from the Brewfile, so bin/open and secrets/ ran under the
3.2 macOS ships and failed on `${v,,}`; config/bashrc also looks for the
bash-completion 2.x that wants 4.2.

Ghostty refused to start -- "error opening config-file ... error.FileNotFound"
-- because config-file pointed at the colour file bin/theme writes, which does
not exist until theme has run. The include is optional now (`?`), and this
script seeds the colour files when a machine has none, leaving a machine that
already has a theme alone.
Igor Soarez igor@soarez.org
Mon, 21 Sep 2026 14:37:35 +0300
dec27023
link: a part named in ~/.config/dot/skip does nothing on that machine

A machine may need one of the parts left out: on a laptop it does not own,
~/.claude/settings.json is the owner's, holding their environment and plugins,
and linking this account's over it is wrong in both directions -- theirs is
lost, and anything of theirs worth keeping would have to be carried here, in
the repo every machine shares.

The file is hand-written and in no repo, like ~/.localenv, so the exception
stays on the machine that needs it. The check is in link/lib.sh, as each part
starts, rather than in link.sh's loop, so `link/claude.sh yes` obeys it too;
link.sh and other repos' link.sh source that file as well, and sit outside
link/, so their names can never be read as a part's.
Igor Soarez igor@soarez.org
Mon, 21 Sep 2026 14:09:58 +0300
fec40208
link/claude: secrets in MCP specs, and a separate spec for Desktop

Two things a spec in ~/.config/claude/mcp-servers.d/ may now carry beyond
what Claude Code takes, both resolved at link time:

- "${szecret:NAME}" in any string value, replaced with `szecret get NAME`
  when the server is registered with Claude Code. For a remote server's
  Authorization header: Claude Code's own headersHelper is not run in a
  workspace without persisted trust, which includes $HOME, so a token
  fetched that way is simply missing. A secret that cannot be read skips
  that server, loudly.

- "desktop", a whole spec given to Claude Desktop instead. Desktop only
  starts local stdio servers, so a layer that moves a server to HTTP for
  Claude Code can keep Desktop on a local copy.
Igor Soarez igor@soarez.org
Fri, 18 Sep 2026 13:03:16 +0100
13cc065e
dotfiles

Shell, editor and terminal configuration, the tools that go with them, and
szecret, an age-based secret manager whose store lives elsewhere.

History restarts here. What came before was not written to be read by
anyone else, and carried machine names, addresses and an encrypted secret
store; it is kept privately instead. What is left is meant to be safe on
any machine, including one whose owner is not me: see CLAUDE.md for the
rule it is written to.
Igor Soarez igor@soarez.org
Thu, 17 Sep 2026 19:45:23 +0100