/etc
Configuration, and the reasoning behind it. The brief here is different from a normal development machine: working across several client environments at once means the expensive mistake is not a broken tool, it is running the right command against the wrong account. Most of what follows is arranged around making that mistake hard rather than merely unlikely.
Three package authorities
Every tool on the machine is owned by exactly one thing. The boundaries matter more than the choices.
- Homebrew
- Owns the machine — everything identical across every client. One declarative manifest, applied and pruned as a unit.
- mise
- Owns the project — the language runtimes that must differ per client. A client on Node 20 and a client on Node 24 is an ordinary Tuesday, and Homebrew can only hold one Node.
- 1Password
- Owns the secrets. Nothing credential-shaped is in the repo, and nothing is exported into a shell profile.
The split between the first two is what makes per-project pinning work. mise
reads the nearest project config on cd, so the correct runtime
version is a property of the repository rather than of my memory. Global
versions exist only as a fallback for scratch work; every real project pins
its own and commits it.
Moving targets
Three tools are deliberately installed outside the main manifest, because their distribution channel is still shifting. The reason is mechanical: one renamed package fails the entire bundle, and a manifest that fails as a unit should only contain things that are stable as a unit. Those three get their own install step with a fallback chain instead.
One of them is worth spelling out. The AI coding CLIs are not installed as global npm packages, because a global npm install is bound to the active Node version — so the next major runtime bump would make the assistant silently disappear. Tools I rely on to do the work should not be downstream of an unrelated version change.
Shell
One dotfile lives in $HOME. It exists only to point at the real
configuration directory; everything else follows the XDG layout, so the home
directory stays legible and the config is one directory that can be read in
full.
Modules load in a fixed order, because several of them depend on the ones before:
- path
- Deduplicated, with GNU tools shadowing the BSD ones so the laptop behaves the way production CI does.
- options
- History, and the small safety flags — no clobbering a file by redirection, no recording a command that was typed with a leading space.
- completions
- Cached, with the security audit skipped except once a day. It is the single slowest thing in shell startup.
- prompt
- Starship only. No framework — a single binary and a single config file, rather than a plugin system to maintain.
- tools
- Runtime manager, then per-directory environment, then navigation and history. Syntax highlighting loads last, which upstream requires.
- aliases, functions
- Covered in /bin.
Startup is held under roughly 150ms, measured rather than estimated. A shell that takes a second to appear gets worked around, and the workarounds are worse than the delay.
The prompt is a blast-radius indicator
The left side is quiet: where I am, what the repository state is. The right side is loud about consequence — which cloud account, which cluster, which infrastructure workspace. Those are the facts that determine what a command is about to affect, so they are never truncated and never hidden.
Anything whose cluster name looks like production renders in bold red with an extra warning marker. It is one rule, and it has justified itself more than every other line in the file.
The subtler one is the per-directory environment indicator. Client account context is bound to the directory, and that binding is revoked automatically whenever the file defining it changes — including when the change arrives in someone else's commit. When that happens the account context is simply absent, which looks exactly like not being in a client repository at all. That silence is expensive: I would believe I have no account context and act accordingly, while the repository believes it granted me one. Showing the loaded-or-blocked state makes the two distinguishable.
Identity fails loudly
There is no global commit email. Git is configured to refuse any identity it was not explicitly given, and identity is derived from where the repository sits on disk. A clone helper enforces the directory layout, so I never choose a path — which means the path is always right, which means the identity derived from it is always right.
The useful property is the failure mode. Committing from a directory that maps to no configured identity does not quietly attribute the work to whichever client was configured first. It stops. Offboarding is then a single deletion: remove the mapping, and any repository left behind refuses to accept commits at all. That tripwire is the point.
Commits are signed with an SSH key held in the password manager's agent, so the private key is never on disk in a form I could accidentally copy.
Guardrails for agents
Working with AI coding tools adds a category of mistake that is easy to prevent and tedious to undo: edits landing directly on a protected branch. A pre-tool hook enforces the branch convention, with four levels set per repository — no check, remind once per session, confirm every time, or block outright. The default is remind; client repositories with a real review process get block.
The reason it is tunable rather than absolute is the more interesting part. A hard block is right for a client repository and wrong for a personal one that legitimately lives on a single branch. A guard that is wrong half the time gets disabled entirely, and then it protects nothing.
The rules themselves — how I want agents to think, write, and hand work off — are in /lib.