Personal dotfiles managed with GNU Stow.
- nvim - Neovim configuration
- tmux - Tmux configuration
- zsh - Zsh configuration
- gh - GitHub CLI configuration
- pi - Pi coding agent extensions and config (
.config/pi/) - .claude - Claude Code hooks and scripts
- focus-blocker - Sink social-media domains in
/etc/hostson the Work Focus - Brewfile - Homebrew package list (includes wt, the worktree manager)
Ensure you have GNU Stow installed:
brew install stowClone this repository to your home directory or a dedicated location:
git clone https://github.com/gugahoi/dotfiles.git ~/.dotfiles
cd ~/.dotfilesUse Stow to create symlinks for configuration files:
# Stow all packages
stow .
# If files already exist locally, adopt them into this repo and restow
stow --adopt -R .Note: Stow's default target is the parent directory of wherever the repo lives. If you clone this repo somewhere other than directly under
$HOME(e.g.~/Projects/dotfilesinstead of~/dotfiles), you must set the target explicitly or symlinks will land in the wrong place:stow -t ~ .
This will create symlinks from the repository files to their target locations in your home directory (e.g., .config/nvim, .config/pi/extensions, .zshrc, etc.).
- Place the configuration file in the appropriate directory structure within this repository
- Run
stow <package-name>to create symlinks - Commit your changes:
git add .
git commit -m "Add/update <package> configuration"
git pushSince the files are symlinked, changes made in ~/.config or ~/.<file> will directly modify the files in this repository. Simply commit your changes:
git add .
git commit -m "Update <package> configuration"
git pushIf you no longer want a package symlinked:
stow -D <package-name>This removes the symlinks without deleting the files from the repository.
Stow uses a simple structure:
- Files in this repository should mirror the directory structure of your home directory
- When you run
stow, it creates symlinks from the repo to your home directory - For example:
dotfiles/.config/nvim/init.lua→~/.config/nvim/init.lua
dotfiles/
├── .config/
│ ├── nvim/ # Neovim config
│ ├── gh/ # GitHub CLI config
│ ├── focus-blocker/ # Social-media blocklist for Focus modes
│ └── pi/ # Pi coding agent config
│ └── extensions/ # Pi extensions (plan-mode, subagent, etc.)
├── .local/bin/ # Personal scripts on PATH (focus-blocker, rex-sesh, ...)
├── .tmux.conf # Tmux configuration
├── .tmux/scripts/ # Tmux helper scripts (plugins/ is TPM-managed, gitignored)
├── .zshrc # Zsh configuration
├── .zshenv # Sources .exports for every zsh, incl. non-interactive
├── .exports # Env vars (incl. PI_CODING_AGENT_DIR)
├── .gitconfig # Git configuration
├── Brewfile # Homebrew packages
└── README.md
# Simulate stowing without creating symlinks (dry-run)
stow -n .
# Show what stow would do
stow -v .
# Unstow a package
stow -D .
# Adopt existing files into the stow directory and restow
stow --adopt -R .If Stow reports conflicts, it means files already exist in your home directory that conflict with the symlinks. You can:
- Delete the existing file if it's redundant
- Manually merge the content if needed
- Use
stow --adoptto move existing files into the stow directory
# See what's currently symlinked
ls -la ~/.config/nvim
ls -la ~/.zshrcTmux plugins are managed by TPM and
declared as set -g @plugin '...' lines at the bottom of .tmux.conf.
install.sh installs TPM and every declared plugin into ~/.tmux/plugins/.
That folder is gitignored because ~/.tmux is a stow symlink into this repo.
It's safe to re-run: TPM is only cloned if missing, and plugins that are
already installed get skipped.
To do it by hand (after stow):
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm
~/.tmux/plugins/tpm/bin/install_pluginsInside tmux: prefix + I installs newly added plugins, prefix + U updates
them, and prefix + alt + u removes plugins no longer listed in .tmux.conf.
Pi's config lives under .config/pi/ and is pointed at by PI_CODING_AGENT_DIR
(set in .exports):
export PI_CODING_AGENT_DIR="$HOME/.config/pi"Pi treats that directory as its agent dir and discovers resources at fixed sub-paths directly beneath it:
| Resource | Path pi reads |
|---|---|
| Extensions | ~/.config/pi/extensions/ |
| Settings | ~/.config/pi/settings.json |
| Sessions | ~/.config/pi/sessions/ |
| Auth | ~/.config/pi/auth.json (local, gitignored) |
Gotcha: extensions must sit at
.config/pi/extensions/, not.config/pi/agent/extensions/. The extraagent/layer only applies to pi's default dir (~/.pi/agent); sincePI_CODING_AGENT_DIRalready points at.config/pi, nesting them underagent/makes pi silently find zero extensions (no/plan, etc.). After adding or moving extensions, run/reloadin pi or restart it.
Each extension is either a single *.ts file or a directory with an
index.ts. Current extensions: plan-mode, subagent,
github-issue-autocomplete, questionnaire.
Redirects distracting domains to 0.0.0.0 in /etc/hosts (and flushes the DNS
cache) while the macOS Work Focus is on, and restores them when it turns
off. Hardened: the passwordless-sudo rule points at root-owned copies, not
at scripts inside this (user-writable) repo.
Files
| Repo file (Stow source) | Installed to (root-owned) | Purpose |
|---|---|---|
.local/bin/focus-blocker |
/usr/local/bin/focus-blocker (root:wheel 0755) |
enable/disable/status/setup |
.config/focus-blocker/blocklist.txt |
/usr/local/etc/focus-blocker/blocklist.txt (root:wheel 0644) |
domains to block (one per line, # comments) |
Stow first symlinks the sources into ~/.local/bin and ~/.config;
focus-blocker setup then copies them into the root-owned locations above.
stow -t ~ . # symlink the script and blocklist
focus-blocker setup # copy root-owned binary + blocklist, add sudo rulefocus-blocker setup copies the script to /usr/local/bin/focus-blocker and
the blocklist to /usr/local/etc/focus-blocker/, both root:wheel, then
installs /etc/sudoers.d/focus-blocker granting passwordless sudo for the exact
enable and disable invocations only (validated with visudo first). It
refuses to run if /usr/local/bin is user-writable, and must be run as your
normal user (it prompts for sudo itself). Test it:
sudo focus-blocker enable
focus-blocker status # no sudo needed (reads world-readable /etc/hosts)
sudo focus-blocker disableWhy copies, not symlinks: a passwordless-sudo rule pointing at a symlink into this (user-writable) repo would let anything running as you rewrite the script and gain root without a prompt. Copying into a root-owned dir means the executed code can't be altered by a user-level process.
Re-sync after edits: the root-owned copies are what actually run, so after editing the script or
blocklist.txtin this repo, re-runfocus-blocker setup(one password prompt) to push the changes live.Maximum hardening (optional): to drop the passwordless-sudo rule entirely, use a root
launchddaemon that watches a user-writable trigger file the Shortcut writes with no sudo. Stronger, but more moving parts.
- Shortcuts app → two shortcuts, each a single Run Shell Script action:
- Work Focus - Block:
sudo /usr/local/bin/focus-blocker enable - Work Focus - Unblock:
sudo /usr/local/bin/focus-blocker disable
- Work Focus - Block:
- Automations tab → +:
- When Work turns On → Run Work Focus - Block (Run Immediately, notify off)
- When Work turns Off → Run Work Focus - Unblock (Run Immediately, notify off)
Firefox / Zen can use DNS over HTTPS, which bypasses /etc/hosts. Under
Settings → Privacy & Security → DNS over HTTPS, set it to Default or
Off so lookups go through the system resolver and the block takes effect.
- Keep your dotfiles in version control for easy backup and portability
- Test changes on a new system or in a virtual environment first
- Some applications may cache configs; restart them to see changes take effect