First install xcode:
xcode-select --install
Then install homebrew:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Finally, install dependencies for emacs packages
brew install direnv
Linux is supported by building Emacs 31 from source. Run make build to install.
Clone this repo:
git clone https://github.com/junyi-hou/dotfiles-v2 /path/to/repo
cd /path/to/repo/dotfiles-v2
make build # this builds & installs emacs-mac
make install # places dotfiles to appropriate locations
make java # install java (for clojure/scala development)
Remote machines are set up via the deploy script, which clones the dotfiles repo:
deploy user@host [-p port]After setup, connect via TRAMP in Emacs (C-x C-f /ssh:user@host:/path).
With additional -b flag, we will do make build + make install in the remote server as well:
deploy -b user@host [-p port]Or use -i flag to install but do not build emacs:
deploy -i user@host [-p port]With --use-local-copy, the local dotfiles directory is copied to the remote via rsync instead of cloning from GitHub:
deploy --use-local-copy user@host [-p port]Start a headless agent on a remote machine from your laptop using agent-shell-to-go, which allows you to keep interacting with a code agent from discord/slack on your phone.
To start a remote agent, run
run-agent /path/to/project user@hostThis SSHes into user@host and launches a headless Emacs session that communicates via Slack or Discord. Configure credentials in ~/dotfiles-v2/modules/claude/config.el on the remote before running.
Prerequisites on the remote:
- Dotfiles deployed (see Remote Development)
- Dotfiles fully installed (
make build+make install, now can be automated with adding-bflag to thedeployscript)
The agent runs in the background using emacs --daemon and persists after the SSH connection closes.
Secrets are stored in env.json.enc (a SOPS-encrypted JSON file, not checked into git) and managed via sops with an age key at ~/.config/age/key. Set SOPS_AGE_KEY_FILE=~/.config/age/key in your shell environment (done automatically via modules/profile).
A CLI tools are available on any machine with secrets deployed:
passage [key/path]— print a secret value (or full JSON if no path given)
Secrets can only be edited on the local machine. This is intentional: each remote machine holds its own re-encrypted copy of the secrets, so edits on a remote would diverge and never propagate back. The local machine is the single source of truth.
M-x sops-edit-secret
Decrypts into a scratch buffer. C-c C-c re-encrypts and saves; C-c C-k cancels. sops-retrieve-secret lets you pick a secret by path and copies it to the clipboard (cleared after 30s).
Ctrl+hjkl navigates in any direction regardless of context — across Emacs panes, Kitty splits, or native macOS windows — using a layered dispatch system.
| Keys | Action |
|---|---|
Control+h/j/k/l |
Focus window/pane left/down/up/right |
Cmd+Shift+h/j/k/l |
Move (reposition) the current window |
Karabiner (modules/config/karabiner/karabiner.json) remaps Ctrl+hjkl → Alt+hjkl for all apps except Emacs and Kitty, which handle Ctrl+hjkl directly.
Aerospace (modules/config/aerospace/aerospace.toml) binds Alt+hjkl to focus left/down/up/right followed by move-mouse window-lazy-center. It is the final recipient for all non-Emacs, non-Kitty apps.
Kitty (modules/config/kitty/kitty.conf) intercepts Ctrl+hjkl using native --when-focus-on title matching (no Python kitten):
- If the focused pane title contains "emacs" → sends the raw control byte to Emacs.
- Otherwise → calls
aerospace focus --boundaries all-monitors-outer-frame <dir>thenaerospace move-mouse window-lazy-center.
Emacs (modules/emacs.d/lisp/gatsby-default.el) binds Ctrl+hjkl to windmove-*. An advice on windmove-do-window-select provides fallback when no adjacent Emacs pane exists:
- Local (GUI or terminal with aerospace in PATH) → calls
aerospace focus --boundaries all-monitors-outer-frame <dir>thenmove-mouse window-lazy-centerviacall-process. - SSH terminal (detected via
KITTY_WINDOW_ID) → sends a KittylaunchDCS escape sequence, which triggers Aerospace on the local machine.
Native app focused (e.g. browser):
Karabiner → remaps Ctrl+hjkl to Alt+hjkl → Aerospace focuses the adjacent window.
Kitty focused, no Emacs pane:
Karabiner passes through → Kitty kitten → aerospace focus <dir>.
Emacs pane inside Kitty:
Karabiner passes through → Kitty --when-focus-on detects "emacs" in title → sends raw byte → Emacs windmove-* → if no adjacent pane, Emacs calls aerospace directly (local) or sends Kitty launch DCS (SSH) → Aerospace.
GUI Emacs:
Emacs windmove-* → if no adjacent pane, Emacs calls aerospace focus <dir> directly.
Emacs sets frame-title-format to "emacs: %b" so the Kitty kitten can reliably detect when the active pane is running Emacs and route accordingly.