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
|
# shellcheck shell=sh
# config/env — environment for EVERY shell: zsh and bash, login or not,
# interactive or not. This is the file `config/profile` was always meant to be.
#
# How it is reached:
# zsh ~/.zshenv — every zsh, including `ssh host cmd`,
# `zsh -c ...` and plain scripts. That is the
# ONLY startup file zsh reads in those modes:
# ~/.zprofile and ~/.zshrc are both skipped,
# and on macOS so is /etc/zprofile (path_helper).
# zsh ~/.zprofile — login shells, AFTER /etc/zprofile has run
# path_helper. Loading this file a second
# time is what puts our PATH entries back in
# front of the system ones; see below.
# bash ~/.profile — login shells
# bash $BASH_ENV — non-interactive shells (exported below)
# both config/profile — sources this first thing, so interactive
# zsh gets it too, via ~/.zshrc
#
# HARD RULES, because this runs for every shell process:
# * POSIX sh only — bash and zsh both source it. No [[ ]], no arrays.
# * No forks. No $(...), no pipes, no external commands. Use `case` and
# parameter expansion. `command -v` is a shell builtin and is allowed.
# * No side effects: no mkdir, no stty, no tty, no aliases, no history
# settings, no secret decryption, nothing that wants a terminal.
# * Idempotent — config/profile sources it again, and nested shells re-run it.
# * `set -u` safe — always use the ${VAR:-} form.
#
# Anything that breaks a rule belongs in config/profile (interactive) instead.
# See config/profile's header for the split.
# ---------------------------------------------------------------- PATH helpers
# Fork-free equivalents of profile's pathadd/pathapp, with the same semantics:
# an entry already on PATH is MOVED, not left where it is. Non-existent
# directories are skipped, which is what makes one list work on both macOS and
# Linux.
#
# Moving matters, and an earlier version of this file skipping it is what put
# /usr/bin ahead of ~/.bin and /opt/homebrew/bin. macOS runs path_helper from
# /etc/zprofile, which rebuilds PATH with /etc/paths (/usr/bin, /bin, ...) at
# the front and everything that was already there appended behind it. zsh reads
# ~/.zshenv BEFORE /etc/zprofile, so whatever precedence this file establishes
# is demoted moments later. Only a second pass after path_helper can undo that,
# and a second pass only does anything if the helpers move existing entries.
#
# The consequence is that PATH order is decided entirely by the sequence of
# calls below, from any starting PATH — which also makes re-sourcing a fixed
# point. The flip side: never hand _env_append a system directory, or it will
# be demoted to the end of PATH.
# Drops every occurrence of $1 from PATH into $_env_p, without the surrounding
# colons. Callers re-add it at whichever end they want.
_env_drop() {
_env_p=":${PATH}:"
while :; do
case "$_env_p" in
*":$1:"*) _env_p="${_env_p%%":$1:"*}:${_env_p#*":$1:"}" ;;
*) break ;;
esac
done
_env_p="${_env_p#:}"
_env_p="${_env_p%:}"
}
_env_prepend() {
[ -d "$1" ] || return 0
_env_drop "$1"
PATH="$1${_env_p:+:$_env_p}"
}
_env_append() {
[ -d "$1" ] || return 0
_env_drop "$1"
PATH="${_env_p:+$_env_p:}$1"
}
# ------------------------------------------------------------------------- XDG
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" # Config stuff should go here.
export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" # Cache stuff should go here.
export XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" # Data should go here.
# ------------------------------------------------------------------- core PATH
# Prepended in this order, so the effective precedence ends up:
# ~/.local/bin, ~/.bin, /opt/homebrew/bin, /usr/local/bin, ...
_env_prepend "/usr/local/bin"
_env_prepend "/opt/homebrew/bin"
_env_prepend "$HOME/.bin"
_env_prepend "$HOME/.local/bin"
# ------------------------------------------------------------------------ less
export LESS='--tabs=4 --ignore-case -RFX'
export LESSKEY="$XDG_CACHE_HOME/less"
export LESSHISTFILE="${LESSHISTFILE:-$XDG_CACHE_HOME/less/history}"
export PAGER=less
# ---------------------------------------------------------------------- editor
# Quoted so shellcheck reads these as strings, not commands to run (SC2209).
if command -v nvim >/dev/null 2>&1; then
_env_vim="nvim"
elif command -v vim >/dev/null 2>&1; then
_env_vim="vim"
else
_env_vim="vi"
fi
export VISUAL="${VISUAL:-$_env_vim}"
export EDITOR="${EDITOR:-$_env_vim}"
unset _env_vim
# ---------------------------------------------------------------------- locale
# Note: mosh refuses to start without a UTF-8 locale, so this genuinely needs to
# be here and not in the interactive layer.
export LANG="en_US.UTF-8"
export LC_COLLATE="en_US.UTF-8"
export LC_CTYPE="en_US.UTF-8"
export LC_MESSAGES="en_US.UTF-8"
export LC_MONETARY="en_US.UTF-8"
export LC_NUMERIC="en_US.UTF-8"
export LC_TIME="en_US.UTF-8"
export LC_ALL=
# ------------------------------------------------------------------------ node
export npm_config_devdir="${npm_config_devdir:-$XDG_CACHE_HOME/node-gyp}"
export npm_config_cache="${npm_config_cache:-$XDG_CACHE_HOME/npm}"
export npm_config_userconfig="$XDG_CONFIG_HOME/npm/config"
export NODE_REPL_HISTORY="${NODE_REPL_HISTORY:-$XDG_CACHE_HOME/node_repl_history}"
export N_PREFIX="$XDG_DATA_HOME/n"
_env_prepend "$N_PREFIX/bin"
# ---------------------------------------------------------------------- python
_env_append "$HOME/Library/Python/3.8/bin"
# uv hardlinks wheels from its cache into venvs, which only works within one
# volume; with src on Sense the default ~/.cache/uv forces a full-copy fallback.
[ -d "/Volumes/Sense" ] && export UV_CACHE_DIR="${UV_CACHE_DIR:-/Volumes/Sense/.cache/uv}"
# -------------------------------------------------------------------------- go
[ -d "/Volumes/Sense/src/go" ] && export GOPATH=/Volumes/Sense/src/go
# Guarded: an unset GOPATH would otherwise make this "/bin".
[ -n "${GOPATH:-}" ] && _env_append "$GOPATH/bin"
_env_append "$HOME/go/bin"
_env_append "/usr/local/opt/go/libexec/bin"
_env_append "/usr/local/go/bin" # for Linux
# ------------------------------------------------------------------------ rust
export CARGO_HOME="${CARGO_HOME:-$XDG_DATA_HOME/cargo}"
export RUSTUP_HOME="${RUSTUP_HOME:-$XDG_DATA_HOME/rustup}"
_env_prepend "$XDG_DATA_HOME/bin"
_env_prepend "/usr/local/opt/rustup/bin"
# Prepending, in case a system-installed rustc needs to be overridden. This is
# all rustup's own $CARGO_HOME/env does, which is why ~/.zshenv can be ours.
_env_prepend "$CARGO_HOME/bin"
# ------------------------------------------ GNU Arm Embedded Toolchain, android
_env_append "/Applications/ARM/bin"
if [ -d "$HOME/Library/Android/sdk" ]; then
export ANDROID_HOME="$HOME/Library/Android/sdk"
_env_append "$ANDROID_HOME/emulator"
_env_append "$ANDROID_HOME/platform-tools"
fi
# ------------------------------------------------- colima / testcontainers
# DOCKER_HOST: Standard Docker env var telling clients where to find the daemon socket.
# TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE: Testcontainers mounts the socket inside containers.
# The host path doesn't exist inside the colima VM; inside the VM, it's at /var/run/docker.sock.
if command -v colima >/dev/null 2>&1; then
export DOCKER_HOST="unix://$HOME/.config/colima/default/docker.sock"
export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock
fi
# ---------------------------------------------------------------- agent CLIs
export KIMI_CODE_HOME="$XDG_DATA_HOME/kimi"
export KIMI_INSTALL_DIR="$KIMI_CODE_HOME"
export KIMI_NO_MODIFY_PATH="no"
_env_append "$KIMI_CODE_HOME/bin"
# cursor-agent CLI: settings in XDG config, runtime data in XDG data.
# The Cursor IDE still uses ~/.cursor for editor state (extensions, argv.json, etc.).
export CURSOR_CONFIG_DIR="$XDG_CONFIG_HOME/cursor"
export CURSOR_DATA_DIR="$XDG_DATA_HOME/cursor"
# Claude Code renderer — pinned to the classic one, so scrollback belongs to
# tmux and nothing else. Do not rely on the `tui` key in
# config/claude/settings.json for this: every running `claude` rewrites that
# whole file from the copy it loaded at *its* start, so with several long-lived
# sessions open the last one to write wins and silently reverts the key — and
# this repo syncs that file to every host, so another machine's flip lands here.
#
# The renderer decision is made in this order, first match wins:
# 1. CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN truthy, or NO_FLICKER falsey → off
# 2. CLAUDE_CODE_NO_FLICKER truthy → on
# 3. settings.tui → whatever it says
# 4. CLAUDE_CODE_TUI_TRIAL / remote feature gates → anything at all
# The two vars below both land in step 1, above the setting *and* above the
# gates and the fullscreen trial, so fullscreen cannot come back on by itself.
# Setting both rather than one is deliberate: a tmux server started while the
# old NO_FLICKER=1 was exported keeps it in its global environment and hands it
# to every new pane forever, and only an explicit export overrides that.
#
# Accepted cost: the classic renderer repaints into tmux's normal buffer and
# duplicates lines whenever its live region is taller than the pane.
export CLAUDE_CODE_NO_FLICKER=0
export CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1
# Give the wheel to tmux too. CLAUDE_CODE_DISABLE_MOUSE suppresses mouse capture
# altogether and takes precedence over _MOUSE_CLICKS (which is why that var is
# gone from here). The old comment against this said it left no wheel scrolling
# at all — that was only true under fullscreen; off the alternate screen tmux
# keeps the scrollback and the wheel enters copy-mode as in any other pane.
export CLAUDE_CODE_DISABLE_MOUSE=1
# ------------------------------------------------------------------------ misc
_env_append "/Applications/Ghostty.app/Contents/MacOS"
_env_prepend "$HOME/bin"
# Read by the shell/prompt machinery, not by this file. Deliberately not
# exported, as it never was.
# shellcheck disable=SC2034
DISABLE_AUTO_TITLE=true
export PATH
# Make non-interactive bash read this file too. bash consults $BASH_ENV only
# when non-interactive, so this never double-fires in an interactive shell.
# Missing file => bash silently skips it, so this is safe to export anywhere.
[ -r "$HOME/.env" ] && export BASH_ENV="$HOME/.env"
# Environment linked in from elsewhere: every $XDG_CONFIG_HOME/shell/env.d/*.sh,
# in name order. This repo ships none; it is where another layer plugs in. Same
# rules as this file -- POSIX, no forks, cheap.
#
# A function only to scope zsh's null_glob: zsh aborts on a glob that matches
# nothing (and a glob qualifier like (N) breaks under `emulate sh`), while dash
# and bash hand back the literal pattern, which the -r test skips. The files are
# sourced inside it, so their exports and plain assignments are global, but a
# `local` -- or under zsh a `setopt` -- would not outlast it.
_env_source_dir() {
if [ -n "${ZSH_VERSION-}" ]; then
setopt local_options null_glob glob
fi
for _env_f in "$1"/*.sh; do
if [ -r "$_env_f" ]; then
. "$_env_f"
fi
done
unset _env_f
}
_env_source_dir "$XDG_CONFIG_HOME/shell/env.d"
# Host-local environment. Same rules as this file — keep it cheap.
# Interactive-only local setup goes in ~/.localprofile instead.
# Sourced before the helpers are dropped, so it can use them too.
# shellcheck source=/dev/null
[ -r "$HOME/.localenv" ] && . "$HOME/.localenv"
# Don't leak the helpers into every shell. config/profile keeps its own
# pathadd/pathapp for the interactive layer.
unset -f _env_drop _env_prepend _env_append _env_source_dir
unset _env_p
# Ensure a successful status: the last conditional above may have been false,
# and this file is sourced at the top of scripts that check $?.
:
# vim: filetype=sh
|