Personal macOS and Arch/Omarchy configuration for Bash, Zsh, Neovim, Herdr, tmux, terminal emulators, and desktop tools.
No configured Git identity, GitHub login, SSH key, Homebrew, or alternate shell is required. In Terminal, use your normal macOS administrator account and run:
bootstrap="$(mktemp)" &&
curl --disable -fsSL --connect-timeout 10 --max-time 120 \
https://raw.githubusercontent.com/teddyhwang/dotfiles/main/bootstrap.sh -o "$bootstrap" &&
/bin/sh "$bootstrap"This downloads the standalone script before executing it; a failed download
never runs a partial script. You are trusting code from this repository—inspect
the downloaded file first if you prefer. Do not use sudo.
The bootstrap opens Apple's Command Line Tools installer if needed. Finish the
installation window, then press Return in Terminal. It verifies Git, clones
this public repository over HTTPS, and runs setup.sh with interactive prompts.
Rerunning it reuses a verified existing checkout without pulling or changing local
work. Failed clones are cleaned up; failed setup runs keep the checkout for retry.
If Command Line Tools are not ready yet, finish their installation and rerun the
same command. After success, open a new Terminal window or run exec /bin/zsh -l.
If you prefer to run the individual steps, install Apple's Command Line Tools first:
xcode-select --installWait for the installer to finish, then verify Git actually runs. If macOS says the tools are already installed, proceed to this check:
git --versionClone this public repository over HTTPS, not SSH. Cloning does not require
user.name, user.email, a signing key, or GitHub authentication:
mkdir -p ~/src/github.com/teddyhwang
git clone https://github.com/teddyhwang/dotfiles.git ~/src/github.com/teddyhwang/dotfiles
cd ~/src/github.com/teddyhwang/dotfiles
./setup.shRun setup normally, not with sudo. Homebrew will request administrator
authorization when needed. Allow time for downloads, applications and source
builds. If a step fails, fix the reported problem and rerun ./setup.sh from the
same checkout; do not clone over an existing directory. A failed Brewfile install
stops setup before linking, rather than claiming an incomplete install succeeded.
After success, open a new Terminal window, or replace the current shell:
exec /bin/zsh -lThe new Mac checklist covers preparation, first-run checks, Git/GitHub access, keys, application permissions, and what setup does not restore.
Run ./setup.sh from the checkout (or by its absolute path). Linux requires
working Git, curl, readlink, and the Arch/Omarchy package-management prerequisites.
The setup is rerunnable and can be run from any working directory. Existing
configuration files are kept unless replacement is confirmed; declining a prompt
keeps that configuration unmanaged, so review any skipped items. Confirmed replacements are
moved to ~/.local/state/dotfiles/backups/replaced.*/ rather than deleted.
Targets with traversal segments or parents resolving outside HOME are refused.
On macOS, setup uses Brewfile.work when devx is available as a command;
otherwise it uses the personal Brewfile. Missing dependencies are installed
with --no-upgrade; workstation-wide upgrades are intentionally left as a
separate maintenance action.
- macOS: Homebrew packages, app configuration, launch agents, and shared shell/editor configuration.
- Arch/Omarchy Linux:
pacman/yaypackages, Hyprland, keyd, Omarchy, and shared shell/editor configuration. ShellCheck already available onPATH(for example, through Neovim's Mason) is reused instead of installing its pacman package.
Linux T2 suspend support requires an explicit privileged run after the normal user setup. The same step keeps a closed laptop awake on external power even when an EDID-less KVM briefly drops its display connection:
sudo ./scripts/macbook_t2_linux.sh./scripts/check.shThis runs shell syntax checks, ShellCheck, structured-config parsing, Node and Lua syntax checks when available, and the Node test suite. GitHub Actions runs the same check on Linux and macOS. Validation needs Node 24+, jq, taplo, yq (mikefarah's Go version), ShellCheck, Bash, and Zsh. Neovim and tmux enable additional isolated integration tests.
The repository's own code is shell or Node only; see AGENTS.md for the
language rules.
Measure warm startup locally (no cache deletion or dependency installation):
node scripts/benchmark.mjs --runs 21This measures startup plus immediate exit, without a TTY. It does not measure first-prompt readiness, deferred plugins, or LSP initialization.
Brewfile is the curated personal package list. Brewfile.work is a
generated snapshot of everything installed through Homebrew on the work Mac.
During setup.sh, an available devx command selects Brewfile.work; without
devx, setup selects Brewfile. Refreshing the snapshot never changes the
personal file.
# Refresh the work Mac snapshot from the current machine
./scripts/backup_brewfile.sh
# Restore the generated snapshot on another Mac
brew bundle install --file ./Brewfile.work
# Install anything newly added to the personal Brewfile without upgrading everything
brew bundle install --no-upgrade --file ./Brewfile
# Deliberately upgrade managed personal Homebrew dependencies
brew bundle upgrade --file ./Brewfile
# Start the local model service only when needed
brew services start ollama
# Optional databases are no longer in the personal Brewfile; install on demand
brew install postgresql@14 # or mysql@8.4, redis, memcached
brew services start postgresql@14
# Update Neovim plugins (lazy-lock.json stays local to this machine)
nvim '+Lazy update'The backup script records taps, formulae, casks, and any Mac App Store entries
that Homebrew Bundle can detect. It excludes VS Code extensions and global Go,
Cargo, uv, and npm packages so Brewfile.work remains a Homebrew inventory.
The snapshot is generated without package-description comments to keep updates
easy to review in Git.
macOS launch-agent setup is separate from package installation and runs after
binaries are linked. Re-running scripts/services_mac.sh also repairs unloaded
managed jobs. Clipboard agents remain opt-in. tmux plugin installation uses a
private, sessionless server and checks every declared plugin without touching
your active tmux sessions.
Prefer granting the actual terminal/Pi execution context Local Network access
in macOS System Settings → Privacy & Security, verifying direct connectivity
from that context, then setting the MCP endpoint to 192.168.1.1:443 and
reconnecting MCP. Apple nc success alone does not prove Node/uv connectivity.
If you explicitly choose a persistent relay instead, run from this checkout:
./scripts/services_mac.sh --unifi-relayConfirm installation when prompted. Normal setup never installs this relay.
It installs ~/Library/LaunchAgents/unifi-relay.plist (localhost.unifi-relay):
launchd listens only on 127.0.0.1:12443 and starts /usr/bin/nc -G 5 192.168.1.1 443 per connection. TLS bytes pass through unchanged; this provides
no authentication for other local users/processes and does not change TCC.
The command does not edit MCP settings; the relay endpoint is
UNIFI_NETWORK_HOST=127.0.0.1, UNIFI_NETWORK_PORT=12443.
Setup requires system nc, launchctl, and lsof. For an unloaded relay it
refuses any listener on TCP 12443, without stopping processes. Inspect with
lsof -nP -iTCP:12443 -sTCP:LISTEN; if an old manual relay owns the port, identify
its owner and arrange an explicit manual handoff before retrying. Never stop an
unidentified listener. A matching loaded agent is left alone; differing plist
files require consent and are backed up under ~/.local/state/dotfiles/backups.
Diagnose with launchctl print gui/$(id -u)/localhost.unifi-relay and the lsof
command above. A listener is not proof of controller connectivity; reconnect
MCP and try a read-only request. If bootstrap fails (including a port race),
resolve the reported conflict and rerun the opt-in command. To disable manually:
launchctl bootout gui/$(id -u)/localhost.unifi-relay, then remove only the
verified managed ~/Library/LaunchAgents/unifi-relay.plist. No live deployment
or endpoint migration is performed by adding this repository wiring.
Ollama remains in the personal Brewfile but is not enabled at login by this
setup. Optional database servers remain in the work snapshot, not the personal
list. Pruning the personal Brewfile does not uninstall existing packages, delete
application/database data, or stop previously enabled services. Avoid treating
brew bundle cleanup --force as part of this pruning: installed packages outside
the curated list can still be in use.
home/config/nvim/lazy-lock.json is machine-local and ignored by Git. Herdr setup
uses its herdr-splits.nvim revision when present, or a pinned fallback from
scripts/herdr.sh before Neovim has installed the plugin. Neovim's Herdr plugin
also synchronizes the Herdr side when it builds or loads inside Herdr.
Mosh is included in Brewfile. Enable Remote Login in System Settings →
General → Sharing for the intended users, then connect from another machine:
mosh teddyhwang@teddys-mac-mini.localMosh authenticates through SSH and starts mosh-server per session; there is no
persistent service to enable. home/.zshenv exposes Homebrew binaries to
noninteractive SSH commands without loading interactive shell configuration.
Existing SSH passwords/authorized keys remain unchanged. The client needs SSH
(TCP 22) and direct access to Mosh's UDP ports (60000–61000); allow mosh-server
in the macOS firewall if prompted. No router port forwarding is set up here.
If a Homebrew dependency upgrade leaves Mosh with a missing protobuf library,
run brew reinstall --yes mosh to rebuild the existing HEAD installation.
Superfile (spf) uses our custom seti theme in
home/config/superfile/theme/seti.toml, matching tinty's base16-seti palette.
It uses charcoal backgrounds, blue active borders, green selections/success,
and red errors. The theme is selected with theme = 'seti' in
home/config/superfile/config.toml.
Custom themes are ordinary TOML
files under ~/.config/superfile/theme/; the config selects the filename without
.toml. They control panels, borders, selections, dialogs, and status colors.
Restart spf after editing a theme. Seti is a fixed palette, not a tinty hook:
changing tinty's scheme does not change Superfile's UI theme.
Code previews use code_previewer = 'bat' and our existing
home/config/bat/config (base16-256) to follow the terminal's palette. This
requires bat, already included in our setup. Superfile's built-in Chroma
highlighter has no Seti style; if you switch back to code_previewer = '', the
theme selects base16-snazzy as a non-Seti alternative.
Superfile uses a customized Vim-like preset in
home/config/superfile/hotkeys.toml, based on upstream
vimHotkeys.toml at v1.6.0.
Setup links home/config/superfile to ~/.config/superfile, so no shell alias or
extra launch flag is needed. Restart Superfile after changing the file.
Superfile v1.6.0 has no built-in Vim preset selector: its
documented configuration uses a
hotkeys file. Removing ours would restore the standard defaults, not Vim keys;
--hotkey-file only selects an alternate file. When updating the preset, use
src/superfile_config/vimHotkeys.toml from the matching upstream release,
retain our navigation customizations below, and update the source version above.
h and Backspace go to the parent directory; l or Enter opens the selected
folder/file. Uppercase H/L move to the previous/next file pane (left/right);
Shift+Tab/Tab still work too. While typing in a prompt, h/l/H/L remain
text, Backspace deletes text, and only Enter confirms. Other bindings include
j/k to move down/up, q to close a file panel, Ctrl+C to quit, f to toggle
the preview, and m to toggle selection mode.
Zed follows the Neovim config; change Neovim first, then mirror the change in
home/config/zed. Setup links settings.json, keymap.json and tasks.json
into ~/.config/zed one file at a time, because Zed writes other state there.
settings.jsonturns on vim mode and copies Neovim's options. Comments name the Neovim option or plugin behind each setting.keymap.jsonuses\as the leader, as Neovim does. It has the custom mappings fromlua/config/keymaps.lua, the plugin mappings, and the LazyVim defaults that have a Zed action.tasks.jsonruns the terminal programs: lazygit, yazi (the chosen file opens in Zed), the AI CLIs,zed-test-filefor\T, andzed-other-filefor the other.nvim alternate file (\oo).
The tinty hook tinty-zed-hook installs each scheme as the theme "Tinted
Theming", with the highlight overrides from Neovim's config/colorscheme.lua.
tinty apply therefore switches Zed and Neovim together.
Personal Pi extension code and tests live in
teddyhwang/pi-extensions.
Once the Pi CLI is installed, setup.sh provisions the shared package baseline
in apps/pi/packages.json: personal extensions, Vim
mode, Herdsman, Autoresearch and Anthropic authentication support. Git sources
use HTTPS rather than requiring SSH keys. The personal pi-extensions repository
is private: authenticate on the new machine with gh auth login followed by
gh auth setup-git before installing it. Public packages need no GitHub login;
setup never copies credentials from another computer.
If Pi is installed after running setup, install the packages without rerunning the rest of workstation setup:
cd ~/src/github.com/teddyhwang/dotfiles
sh scripts/pi.shThe helper also finds Pi in ~/.local/bin or ~/.pi/agent/bin before those
locations reach a new shell's PATH. Without Pi, setup prints a skip/rerun hint;
it does not install or update the CLI. It installs missing packages and repairs
missing package/build artifacts, not routine upgrades. Existing package sources,
pins, resource filters and unrelated settings are preserved. New entries get the
baseline resource filters; top-level resource/tool defaults are filled only when
absent. Settings changes preserve a private backup under
~/.local/state/dotfiles/backups/.
Authentication, MCP connections, model choices and other machine-local settings
are not copied from another machine. See the new Mac checklist.
This repository manages Pi configuration and integration wiring only; do not
add extension implementations here or maintain separate personal copies in
~/.pi/agent/extensions. After package changes, use /reload in active sessions.
home/pi-agent/AGENTS.md holds the global Pi rules: use edit for file
changes, do not poll with sleep, batch independent calls, and how to read
links. Setup links it to ~/.pi/agent/AGENTS.md, and Pi loads it in every
working directory when a session starts. These portable rules assume only
pi-extensions: other tools, agents or CLIs may still be absent or deliberately
filtered on an existing machine, so references to them stay conditional
("if bg_wait is available"). tests/pi-agents-md.test.mjs enforces this.
The same file carries the always-on Toil Tracker trigger line, so every Pi
session (including Herdsman agents) journals avoidable friction. Setup links the
skill from .agents/skills/toil-tracker to ~/.agents/skills/toil-tracker
(keeping an existing install) and seeds an empty machine-local journal at
~/toil/index.md from the skill's template, never overwriting one. Change the
journal's action map in that index header to grant direct fixes or filing.
The package's /skill:home-assistant is the maintained Home Assistant workflow:
access, safe updates, dashboards, HomeKit, backups and verification. This repo
owns only the nonsecret site configuration and runbook at
home/config/home-assistant/{agent.json,site.md}, linked to
~/.config/home-assistant/. The generic skill/API helper and its behavioral
tests live in the canonical Pi package. Read and update the site runbook during
HA work instead of rediscovering connection details and decisions from memory.
Never store tokens, PINs, passwords or full auth/integration-storage dumps in
these files. /reload makes a newly installed or updated skill available.
Inside Herdr, the pi shell function in home/shared/functions handles
pi --worktree [name] before pi starts. Herdr creates the checkout on branch
pi-worktree/<name> from the current HEAD and focuses its new workspace.
The function then types pi and the remaining arguments into that
workspace's pane, in the same subdirectory, so no pi starts in the launching
pane. When that pi exits, the pane asks whether to remove the worktree and
close its workspace. Enter confirms only when nothing would be lost. Commits
always survive: the branch is kept unless it has no new commits, and a
detached HEAD gets a safety branch. Resumed sessions (-c, -r,
--session, --fork) and print/JSON/RPC runs fall through to the package's
in-session worktree.
The former dotfiles session-tab-name extension now belongs to that package.
Setup removes only its exact old dotfiles symlink, preserving third-party
extensions and user-maintained files. See AGENTS.md for the ownership guardrail.
Pi Herdsman uses the tested Git revision
in the package baseline, rather than the older npm runtime. Its Git checkout
needs an explicit build: Pi's production-only dependency install does not run
Herdsman's prepack hook or install esbuild. The setup helper installs only
esbuild@0.28.2 in ~/.cache/pi-herdsman-build, links a missing build dependency,
and runs Herdsman's own build script when its bundled outputs are missing. It
does not patch third-party source or install another copy of Pi's host packages.
Existing local Herdsman clones and their package declarations remain supported.
When herdr is available, the helper also runs herdr integration install pi;
Herdr continues to own that generated integration. No sessions are started or
restarted. Do not use the upstream Herdsman install.sh: it installs a different
Pi/herdr runtime.
Updates remain explicit. After changing/updating a Git package's ref, rerun
sh scripts/pi.sh to rebuild any removed Herdsman outputs, then /reload.
pi update --extensions updates managed packages, not local clones; a configured
Git commit stays pinned. Local clones still need their own deliberate Git update.
Keep Pi's default strict tool calling for Anthropic models; a test enforces this. Strict calling stops agents from calling tools they do not have. Before Herdsman v0.20.0, its tools exceeded Anthropic's grammar limit and every request failed with HTTP 400. If that error comes back, update Herdsman and restart old sessions. Do not turn strict tools off.
Herdsman is the default bounded-delegation executor; do not expose both it and
legacy agent-teams for routine delegation. In ~/.pi/agent/settings.json, use
Pi's package resource filters (merge into existing entries, preserving their
sources and other filters):
{
"packages": [
{
"source": "git:git@github.com:teddyhwang/pi-extensions",
"extensions": ["-extensions/agent-teams/index.ts"]
},
{
"source": "https://github.com/davebcn87/pi-autoresearch",
"skills": []
}
],
"skills": [
"-skills/find-skills/SKILL.md",
"-../../.agents/skills/find-skills/SKILL.md"
]
}This is a selection fragment, not a replacement settings file. Keep the
Herdsman, Vim, search, authentication and other package entries. Remove an old
explicit +…find-skills/SKILL.md inclusion before adding its exclusions; both
skill roots are excluded because personal Pi skills may symlink to .agents.
Fresh installs receive these selections automatically. Existing package filters
and top-level resource selections remain machine-local and are not overwritten;
merge this fragment when deliberately changing an existing installation.
The generic finder routes ordinary how-to questions through skill discovery;
keep it opt-in. Autoresearch's recipes are also opt-in until their correctness
checks and finalization safeguards are reviewed: the inspected create recipe
masks typecheck failures with | grep -i error || true, and finalization can
fail on deleted paths. This filters the skills only, preserving the gated
/autoresearch runtime and experiment data; it does not patch third-party
installer-managed files. Deliberately load a reviewed recipe for one session:
pi --skill ~/.pi/agent/git/github.com/davebcn87/pi-autoresearch/skills/autoresearch-create/SKILL.md
pi --skill ~/.pi/agent/skills/find-skills/SKILL.mdAn excluded skill is not advertised or available through its normal /skill:
command unless loaded explicitly. After changing selections, use /reload;
start a fresh session if previously activated tools remain declared. Legacy
agent-teams code remains installed for workflows explicitly needing its task
board/budget controls; select a single delegation owner rather than silently
substituting APIs.
Herdsman agents inherit the lead's model unless a definition pins one. Setup
copies the matching profile into ~/.pi/agent/agents/. Personal machines use
home/pi-agent/agents/personal/: implementer runs OpenAI Codex GPT-6.1 Sol
with medium thinking; scout, researcher and reviewer run GPT-6.1 Sol
with high thinking. In October sessions Sol finished bounded work several
times faster and cheaper than an inherited Opus lead, while GPT-6 Luna at
medium stopped part-way in 10 of 11 implementer runs, so Luna is not used for
delegated work. All keep the bundled prompts and tools; generalist still
inherits the lead's model. These providers need their normal Pi authentication.
On work machines (devx detected), setup uses home/pi-agent/agents/work/
instead:
researcheruses Claude Fable 5.1, the strongest model, with xhigh thinking. Its tool list uses the installed shop-pi-fy search and fetch tools, so it can read primary sources instead of only search snippets.scoutuses Claude Sonnet 5.5 andrevieweruses Claude Opus 5.5, both with high thinking. Their appended bodies say they have no shell, because project instructions full of shell commands led them to callbash, which they do not have. Herdsman v0.27.0 removed its own shell text, but scouts still calledbashwithout these bodies.implementeruses Claude Sonnet 5.5 with medium thinking. It also loads the shop-pi-fybackground-jobsextension and allowsbg_*tools, so it waits on long jobs withbg_waitinstead of polling withsleep. If you removebackground-jobswith/pkg rm, remove it from this definition too. It also loadsshell-guardfrom pi-extensions, which blocks sleep polling, bash edits of tracked files, and destructive git andpkill -fcommands.data-auditoris a new read-only role for checking claims against local data, such as memory-sync audits. It uses Claude Sonnet 5.5. Its only shell isreadonly_bashfrom pi-extensions'readonly-shell, which runsrg,jqandpython3in a macOS sandbox without network or filesystem writes outside a per-call temp directory. It requires installed Bash 4+; Linux execution fails closed until host control sockets can be isolated. This is not a confidentiality or complete process-isolation boundary. A new role has its own prompt and must not setbodyMode.
home/pi-agent/agents/generalist.md installs on every machine. It keeps the
lead's model and appends edit-tool and no-polling rules. The bundled
implementer and generalist bodies replace Pi's system prompt, which drops its
rule to change files with edit.
Sonnet 5.5 is the weakest Claude model allowed for delegated work; do not pin
Claude 4.x or Haiku models. Tests enforce this floor and both profiles' explicit
implementer/reviewer model and thinking levels. The copies are not symlinks
because Herdsman ignores symlinked definitions. A pinned model keeps the
bundled noExtensions policy, so the scout and reviewer overrides load
the Shopify AI proxy extension explicitly. The bundled researcher keeps
extension discovery and needs no extension entry. An override with a body
must set bodyMode: append, or its body replaces the bundled prompt; a test
enforces this. Setup replaces or removes only files that carry the
# Managed by dotfiles: marker; unmarked interactive overrides are preserved.
Changes affect new agent assignments, not agents already running. Use
/agents definitions to customize models locally.
Generated theme files, caches, machine-local configuration, and secret-bearing environment files are ignored. Never add credentials to the repository; use the system keychain, 1Password, or untracked local environment files instead.