Conversation
Pins every app to a fixed workspace slot across two horizontal tracks, so closing and reopening an app never shifts anything else: Work 1 Chrome 2 Orca 3 Slack 4 Calendar 5 Warp 6 VS Code Personal 7 Chrome Canary 8 cmux 9 VS Code Insiders Overflow 0 anything not in the layout persistent-workspaces keeps empty slots alive, so slot 4 stays slot 4 whether Calendar is running or not. on-window-detected routes each app back to its own slot on launch. AeroSpace matches windows by bundle ID, so the two tracks use distinct apps rather than distinct profiles — Chrome/Chrome Canary and VS Code/ Insiders. Chrome profiles could not work here: a Chrome window title carries no profile information, so the two profiles are indistinguishable. The rules use '=' (exact) rather than '~=' (substring) so that com.google.Chrome does not capture com.google.Chrome.canary. aerospace-track adds the spatial navigation AeroSpace has no concept of: alt-a/alt-d step within a track and clamp at its edges, alt-w/alt-s swap tracks and resume each one's last slot. Per-track memory is recorded from exec-on-workspace-change into ~/.local/state/aerospace/. Written for bash 3.2, since that is what macOS ships. Accordion layout with zero padding keeps one app per screen instead of tiling, and every slot is pinned to the main display so the layout is identical on the laptop screen and on an external monitor. Orca has no Homebrew cask (the 'orca' cask is Plotly's unrelated tool), so it stays a manual step alongside granting Accessibility access. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…file
Replaces the two-track Work/Personal model with one linear track of slots:
1 Desktop 2 Calendar 3 Slack 4 Orca 5 Chrome Work 6 Chrome Me
7 Warp 8 VS Code 0 Overflow
Slot 1 is deliberately empty, and login lands there.
Chrome now splits by profile rather than by app. Chrome appends the signed-in
account name to every window title once more than one profile is running, so
the title is a usable discriminator where the bundle ID is not:
test %{app-bundle-id} = com.google.Chrome && test %{window-title} ~= "..."
That supersedes the earlier claim that Chrome window titles carry no profile
information, which was wrong. It also removes the reason for running a second
browser and a second editor, so Chrome Canary, cmux and VS Code Insiders are
uninstalled and dropped from the Brewfile.
The patterns must be anchored: bare 'Freddy' is a prefix of 'Freddy
(dotcms.com)' and matches both profiles. A third rule matching the bundle ID
alone catches the single-profile case, where Chrome drops the suffix, so an
unlabelled window lands on Work instead of falling through to overflow.
With one track there is nothing to move up and down between, so ctrl-w and
ctrl-s are unbound (returning both to the shell) and the per-track memory is
gone entirely — no record subcommand, no exec-on-workspace-change hook, no
state under ~/.local/state. ctrl-a and ctrl-d now loop around the ends of the
single track rather than clamping at its edges.
Adds `aerospace-track overview`, aliased to `spaces`, because AeroSpace has no
overview GUI: its tray icon shows only the active workspace, and it parks
inactive windows off-screen rather than using native macOS Spaces, so Mission
Control cannot stand in either. The focused slot is marked with brackets as
well as colour so the output survives being piped.
Adds alt-/ to split the current slot side by side and back, for the occasional
two-window comparison. It affects only the focused slot; everywhere else stays
stacked, since the point of the layout is one app per screen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three slots were empty at once (the desktop, Warp, VS Code) and all three looked identical: a bare screen. Landing on one gave no clue where you were, so reaching Calendar took one keypress from the desktop but three from Warp's empty slot. ctrl-a / ctrl-d now skip slots with no windows. Slot 1 is exempt, being the intentional desktop, so there is exactly one blank screen you can land on and it is always the same one. The trade-off is that the number of presses between two apps changes as apps open and close; the slots themselves never move, and alt-N still jumps straight to one. Warp leaves the layout and VS Code takes slot 7. Warp has no rule now, so the catch-all sends it to overflow along with anything else unlisted. 1 Desktop 2 Calendar 3 Slack 4 Orca 5 Chrome Work 6 Chrome Me 7 VS Code 0 Overflow Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
macOS reports the huddle as a utility panel, so AeroSpace floats it and it covers whatever slot you are looking at. A rule matching "^Huddle:" in the window title forces it back into tiling and switches slot 3 to horizontal tiles, so the huddle and the channel sit side by side. The rule has to precede the general Slack rule, since first match wins, and the title anchor keeps it from catching the main window. Slot 3 stays h_tiles once the huddle ends. That is invisible with a single window, which fills the screen under either layout; alt-/ switches it back. Also documents `aerospace run-callback`, which re-applies routing rules to windows that are already open. That is both the way to test a rule against a live window and a replacement for restarting AeroSpace after a config change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three AeroSpace features the setup was not using: alt-shift-e disables window management. Key events stop being intercepted, which is the only way to hand ctrl-a and ctrl-d back to the terminal short of quitting, and hidden workspaces return to the screen — useful when screen sharing. It is one-way: while disabled the server rejects every command except `aerospace enable on`, so the key cannot undo itself. Verified that all windows return to their original slots when re-enabled. alt-1 through alt-7 gain --auto-back-and-forth, so pressing the key for the slot you are already on returns you to the previous one. Every jump key becomes a toggle at no cost. alt-tab moves from workspace-level to window-level back-and-forth, chained as `focus-back-and-forth || workspace-back-and-forth` because the former exits non-zero once the previous window is gone. This matters when a slot holds two windows, such as Slack during a huddle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The "^Huddle:" rule never fired. Slack sets that title a moment after the window appears, so at on-window-detected time the title is something else and the pattern cannot match. Re-running the callback later worked, which made the rule look correct while it did nothing in practice. Confirmed it was a detection-time miss rather than AeroSpace re-floating the window afterwards: once tiled, the huddle stayed tiled across repeated checks, so nothing was undoing the rule. Tiling every Slack window removes the race — there is no title to wait for. The huddle is effectively the only second window Slack opens, and a popped-out thread tiling alongside is no loss. Verified from the exact failing state: slot stacked, huddle floating, rule fired against the huddle window alone — both windows end up h_tiles side by side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Routing runs only when a window is first detected, so a window that ends up in the wrong slot stays there. An accidental alt-shift-N, a window dragged across, or an app opened before its rule existed all leave a window stranded with nothing to correct it. alt-shift-r re-applies every rule to every open window. Verified by moving the main Slack window into slot 2 and pressing it: the window returns to slot 3 and tiles beside the huddle again. This came out of a report that the huddle rule had failed. It had not — the huddle tiled correctly in slot 3, but the main Slack window had drifted to slot 2, so there was nothing in slot 3 to sit beside. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
zsh already routes scp through _remote_files, which reads remote-access with `zstyle -T` — true when unset. So this is on by default and the line changes nothing today; it is here to document that the behaviour is wanted and to survive a framework or plugin that sets the style to false. What actually gates remote completion is non-interactive key auth: _remote_files shells out to ssh with BatchMode=yes, so a host that prompts fails silently mid-Tab. The ~/.ssh/config side (key in the agent, plus the `Host *` ControlMaster block that keeps each Tab near 0.1s instead of a fresh handshake) is machine-local and stays out of the repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These four fell through to the catch-all rule and were dragged to slot 0, which is wrong for transient controls: Cap's recording overlay and krisp's mic widget would jump workspaces mid-recording. Floating leaves them on the slot they open on without letting them claim one. Messages needs com.apple.MobileSMS rather than com.apple.Messages, so that rule carries a note — the wrong ID silently never matches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plural path has never existed, so nothing was ever auto-trusted and any clone carrying an [env] block failed to load until it got a manual `mise trust`. Tool-only configs load untrusted, which is why this went unnoticed for so long — only configs with env, tasks or hooks demand trust. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`try init` defines a shell function named `try`, which then shadows the binary. On the next `source ~/.zshrc` the line called that function, so `init` reached try.rb as a search query and the interactive selector opened instead of any shell code being printed. `command -v` was no guard: it matches the function too. `whence -p` looks only at $PATH and `command try` skips the function, so both halves now name the binary and the line is idempotent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`brew bundle` exits non-zero if any single entry fails, and install.sh runs under `set -e`, so one bad cask killed the bootstrap at step 02 — symlinks, oh-my-zsh, mise, the macOS defaults and post-install never ran. A cask fails this way whenever its app was installed by hand, since Homebrew will not overwrite an app it does not track. Report the unmet entries and continue instead, naming `--adopt` as the fix for that case. Also default DOTFILES_DIR so the script works run directly and not only through install.sh, which is what exports it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Added by the Railway CLI installer; committing it so the block survives a fresh bootstrap rather than being re-appended per machine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The desktop was built as a fixed track: one app per workspace, accordion
layout with zero padding so a second window covered the first at full size.
That is the opposite of a tiling workflow, and the routing rules enforced it
on every window.
Rebuild it around Omarchy/Hyprland's model so the muscle memory transfers.
Tiling:
- 'tiles' root layout with 'auto' orientation, replacing accordion. Wide
containers split side by side, tall ones stack.
- accordion-padding 0 -> 30, since accordion is now the opt-in stack on
alt-/ rather than a disguise for fullscreen.
- 6px gaps, uniform inside and out.
Workspaces:
- Drop all seven app-to-workspace rules. Nothing is assigned anywhere; a
window opens on the focused workspace and tiles into it.
- Drop the 'if = true' catch-all that exiled unmatched windows to slot 0.
It was what stopped a new window ever joining the workspace you were on.
- Drop persistent-workspaces, so a workspace exists only while it holds a
window. alt-N creates one on demand, and ctrl-a/ctrl-d visit only
occupied ones.
Keys, mirroring Omarchy with alt as Super:
- alt+arrows focus, alt+shift+arrows move, alt+-/= resize.
- alt+1..9 / alt+shift+1..9 for workspaces, extended from 1..7.
- alt+f fullscreen, alt+t float toggle, alt+j split orientation.
- alt+tab is now plain workspace-back-and-forth.
- Costs Option+arrow word navigation, traded for arrows matching Hyprland.
Rules kept only where a window needs behaviour it cannot get alone: Slack,
whose huddle macOS mislabels as a panel, and four utilities that float.
Finder, Notes and Messages no longer float -- they are windows you work in.
Focus indicator: AeroSpace draws no borders, so on-focus-changed moves the
cursor to the focused window. JankyBorders was considered and skipped; it
leans on private APIs that major macOS releases break.
Scripts:
- Delete aerospace-track. Its TRACK/LABELS arrays encoded the slot model,
and AeroSpace's own 'workspace --wrap-around next|prev' covers the
navigation now that empty workspaces do not exist. 'spaces' becomes a
plain list-windows alias with nothing to keep in sync.
- Rewrite restore-layout as a reset rather than a router. It moves drifted
windows to their workspace, launches missing apps there, and flattens
what it touched, so the end state depends only on its plan and not on how
far things had drifted.
macOS does not report MacWhisper's window as an ordinary resizable one, so AeroSpace auto-floated it and it hovered over a workspace rather than sharing it -- most visibly next to Chrome. It tiles perfectly well when told to, so give it the same `layout tiling` override Slack's huddle window already uses.
Give each modifier one job: Caps for the window manager, Option for text
navigation, Ctrl for the terminal, Cmd for app shortcuts. Previously every
binding sat on plain Option, which cost Option+arrow word navigation, and
workspace cycling sat on ctrl-a/ctrl-d, which shadowed readline.
Raycast's Hyper Key emits ctrl+alt+shift+cmd for Caps Lock -- measured by
polling CGEventSource.flagsState while it was held, not assumed -- so every
binding is written against that literal four-modifier sequence.
Removed: all 37 alt-* bindings, plus ctrl-a and ctrl-d. Option+arrow word
navigation and terminal ctrl-a/ctrl-d are now untouched, and no bare cmd-*
or ctrl-* binding remains.
Added the same set under Caps, plus two substitutions forced by the modifier
layout:
- Workspace cycling is Caps+tab / Caps+backtick. Caps+Shift+Tab is
unavailable because Hyper already contains Shift; backtick sits directly
above Tab and reads as its reverse. Former workspace moves to Caps+B,
since Caps+Ctrl+Tab is unavailable for the same reason.
- Moving a window becomes a mode, entered with Caps+A. Hyper consumes all
four modifiers AeroSpace supports and it rejects fn, so no second chord
level exists -- Caps+Shift+Left is byte-identical to Caps+Left. In the
mode, arrows nudge the window and numbers send it to a workspace, as i3
and sway do for resize.
Setting Raycast's Hyper to ctrl+alt+cmd without Shift would make Caps+Shift a
distinct combination again and allow plain chords; that is a Raycast-side
change and is left alone.
Moving a window was a toggled mode: Caps+A in, arrows, esc out. Two problems with that. It costs two keystrokes for the most frequent operation, and a toggled mode has no visual indicator -- "am I in move mode?" is indistinguishable from "is this broken?", which is exactly how it read in use. A held chord is what was wanted, and AeroSpace cannot express hold-to-activate modes at all: it fires hotkeys on key down only, its config rejects any key-up option, and its binary contains no hotkey-release event. The only thing that behaves like "hold" is a modifier chord. So move becomes Caps+h/j/k/l -- hold Caps, tap a direction. The arrows stay on focus and cannot be shared, because Raycast's Hyper contains Shift and so Caps+Shift+Left is byte-identical to Caps+Left. hjkl is the same direction set Hyprland and vim use. Split orientation moves from Caps+J to Caps+O, since J is a direction now. Sending a window to another workspace becomes relative, Caps+comma and Caps+period, wrapping around. By-number would need Caps+Shift+N, and no other single key set maps cleanly onto 1-9 without worse mnemonics. The move mode and its table are deleted; there are no modes left but service.
Third and final modifier: plain Option ate Option+arrow word navigation, and Caps Lock via Raycast's Hyper was worse -- Raycast's Hyper is fixed at ctrl+alt+shift+cmd, consuming all four modifiers AeroSpace supports, so Caps+Shift was byte-identical to Caps and moving a window had to become a toggled mode with no visual indicator. Ctrl has neither problem. Shift is free, so move is a plain chord again and direct send-to-workspace comes back, which the Hyper layout could not express at all. The split is by modifier depth: Ctrl window navigation -- arrows and digits ONLY Ctrl + Shift window manipulation -- everything else Option text navigation, untouched Cmd macOS and apps, untouched Ctrl + letter terminal, untouched No plain Ctrl+letter is bound anywhere, so ctrl-a, ctrl-c, ctrl-d, ctrl-r and ctrl-w keep working. Plain Ctrl is confined to arrows and digits, neither of which carries a control character. Dropped rather than rehomed: next/previous workspace cycling. Ctrl+Tab belongs to browsers and editors and Ctrl+backtick is VS Code's integrated terminal; nothing else was clean enough to invent. Former-workspace survives on Ctrl+Shift+B, and the relative window sends on Ctrl+Shift+comma/period. Tiling, workspace model, window rules, monitor assignment and startup are untouched -- the only change outside the Keys section is the header key map.
The Ctrl migration dropped next/previous workspace navigation rather than inventing a scheme: Ctrl+Tab belongs to browsers and editors, and Ctrl+backtick is VS Code's integrated terminal. Brackets are the answer -- prev/next is what they mean nearly everywhere, and they pair with the existing ctrl-shift-comma/period: brackets take you to a workspace, comma/period take the window there. Bound only with Shift on purpose. Plain ctrl-[ is the ESC control character and plain ctrl-] is GS, so neither belongs to a window manager.
on-focus-changed fires on every focus change, not just the ones you asked for. A transient window that appears, takes focus and disappears -- an unarchiving progress dialog is the case that surfaced it -- drags the cursor across the screen and leaves it stranded there. It was there because AeroSpace draws no borders and the cursor was the only focus indicator available. Having no indicator beats having one that yanks the pointer around on its own. on-focused-monitor-changed stays: it fires only when the focused display changes, which no short-lived window on the current display can trigger.
AeroSpace binds plain Ctrl+arrows for focus, Ctrl+Shift+arrows to move a window and Ctrl+1..9 for workspaces. macOS claims every one of those for Spaces and Mission Control, and system shortcuts win over app-registered hotkeys, so on a fresh machine the bindings silently do nothing. Ids 80 and 82 are the trap. System Settings shows one checkbox per "Move left/right a space" row, but macOS registers two hotkeys behind it -- Ctrl+arrow and Ctrl+Shift+arrow -- and the Shift variants have no UI control anywhere. They survive turning the visible ones off, which is exactly what had happened here. Written through `defaults import` rather than `defaults write -dict-add`: the old-style syntax stores the parameters as strings and this table wants integers. Idempotent -- an entry already disabled keeps its parameters untouched, a missing one is created disabled -- and applied with activateSettings so no logout is needed. Also drops the Brewfile's description of the deleted per-app slot model.
Two issues a Codex review of the branch surfaced. home/.zshrc sourced ~/.railway/env unguarded. The Railway CLI writes that file on first login, so on a fresh machine every interactive shell printed "no such file or directory". Every other optional integration in this file already guards with `2>/dev/null || true`; this one now matches. scripts/06-macos.sh only disabled symbolic hotkey ids 118-121, which are Switch to Desktop 1-4, while AeroSpace binds Ctrl+1..9. macOS adds entries for higher desktops as Spaces are created and their ids are not documented, so rather than guess at ids, the script now also scans the existing table and disables anything that is a bare Ctrl or Ctrl+Shift chord on a key AeroSpace claims. Excluding Cmd and Option from that match is what keeps it away from the Ctrl+Shift+Cmd+3 and Ctrl+Shift+Cmd+4 screenshot shortcuts, which share the digit keycodes. Unit-tested against twelve real hotkey shapes. The known-id list stays, so ids macOS has not created yet are pre-disabled.
Codex review passRan P2 — unguarded
P2 — Mission Control shortcuts for desktops 5–9. Right in principle, fixed differently in 91f4011. The observation was correct: the script only disabled ids 118–121 (Switch to Desktop 1–4) while AeroSpace binds Instead the script is now data-driven: it scans the existing table and disables anything that is a bare Excluding P1 — SSH signing key in Codex reviewed the dirty working tree. The real identity and |
The Dock reset already stripped every Apple app a new Mac ships with -- persistent-apps for the left side, persistent-others for the Downloads stack, show-recents for the middle. That part was fine. What was not fine is re-pinning blindly. Apps.app exists only on macOS 26 and later, where it replaced Launchpad.app, so installing on an older release pinned a path that is not there and left a broken "?" tile in the Dock. Check each path before pinning it, and list both Apps.app and Launchpad.app so whichever the release actually has gets used.
Rebuilds the desktop from a fixed one-app-per-workspace track into an Omarchy/Hyprland-style tiling setup, and moves window management onto
Ctrl.maincurrently has noaerospace.tomlat all, so this is the whole config.Tiling
Workspaces are generic contexts, not app slots. Nothing is assigned anywhere — an app opens on whichever workspace has focus and tiles into it. Three things had to go for that to work:
accordion+accordion-padding = 0, which made a second window cover the first at full size — the reason everything looked fullscreen.if = 'true'catch-all that exiled unmatched windows to slot 0, which was what stopped a new window ever joining the workspace you were on.persistent-workspacesis also gone, so a workspace exists only while it holds a window.Ctrl+5creates workspace 5 on demand and it disappears when you close its last window — which is what makes the cycling keys visit only occupied workspaces.Six rules remain, all for windows that need behaviour they cannot get alone: Slack and MacWhisper forced back into tiling (macOS reports their windows as panels), and System Settings, 1Password, Cap and krisp floating. Finder, Notes and Messages no longer float — they are windows you work in.
Keys
Ctrlfor navigation,Ctrl+Shiftfor manipulation:Ctrl+ arrowsCtrl+Shift+ arrowsCtrl+1–9Ctrl+Shift+1–9Ctrl+Shift+[]Ctrl+Shift+,.Ctrl+Shift+FTJ/\Ctrl+Shift+-=No plain
Ctrl+letter is bound anywhere, so^A ^C ^D ^R ^Wkeep working. PlainCtrlis confined to arrows and digits, neither of which carries a control character.Option+←/→ word navigation and allCmdshortcuts are untouched.This modifier took three attempts. Plain
OptionateOption+arrow word navigation. Caps Lock via Raycast's Hyper Key was worse: Raycast's Hyper is fixed atctrl+alt+shift+cmd, consuming all four modifiers AeroSpace supports, soCaps+Shiftwas byte-identical toCaps— no second chord level existed, and moving a window had to become a toggled mode with no visual indicator.Ctrlhas neither problem.The macOS shortcuts this needs off
scripts/06-macos.shnow disables ten Mission Control symbolic hotkeys. Without this the bindings silently do nothing on a fresh machine, because system shortcuts win over app-registered hotkeys.Ids 80 and 82 are the trap: System Settings shows one checkbox per "Move left/right a space" row, but macOS registers two hotkeys behind it —
Ctrl+arrow andCtrl+Shift+arrow — and the Shift variants have no UI control anywhere. They survive turning the visible ones off.Idempotent, and applied with
activateSettingsso no logout is needed.Also
restore-layout— resets apps onto workspaces 1–5, opening what is missing and moving what has drifted. Personal to one machine (it hardcodes Chrome profile directories).aerospace-trackdeleted. ItsTRACK/LABELSarrays encoded the slot model, and AeroSpace's ownworkspace --wrap-around next|prevcovers the navigation now that empty workspaces do not exist.spacesbecomes a plainlist-windowsalias with nothing to keep in sync.on-focus-changedmoving the cursor to each focused window was tried and removed — it fires on every focus change, so a transient window that appears and vanishes drags the cursor away and abandons it.Still manual on a new machine
AeroSpace needs Accessibility permission granted by hand, as do the other apps that require it.
restore-layout's Chrome profile directories (Profile 1= work,Default= personal) are assigned in signup order and may differ.🤖 Generated with Claude Code