diff --git a/bork/Brewfile b/bork/Brewfile
new file mode 100644
index 00000000..4916e20b
--- /dev/null
+++ b/bork/Brewfile
@@ -0,0 +1,116 @@
+tap "kejadlen/tap", "https://git.kejadlen.dev/alpha/homebrew-tap.git"
+
+tap "homebrew/bundle"
+tap "homebrew/command-not-found"
+tap "homebrew/services"
+
+brew "aider"
+brew "ansible"
+brew "bat"
+brew "chruby"
+brew "curl"
+brew "difftastic"
+brew "direnv"
+brew "docker"
+brew "entr"
+brew "eza"
+brew "fd"
+brew "fzf"
+brew "git"
+brew "git-lfs"
+brew "hyperfine"
+brew "jj"
+brew "luarocks"
+brew "mas"
+brew "minizinc"
+brew "pandoc"
+brew "restic"
+brew "ripgrep"
+brew "ruby-install"
+brew "syncthing", restart_service: :changed
+brew "terraform"
+brew "tmux"
+brew "ykman"
+brew "yt-dlp"
+brew "z3"
+brew "zoxide"
+brew "zsh"
+
+## editor things
+
+brew "ansible-language-server"
+brew "ansible-lint"
+brew "efm-langserver"
+brew "fnlfmt"
+brew "shellcheck"
+brew "terraform-ls"
+brew "yaml-language-server"
+brew "yamlfmt"
+brew "yamllint"
+
+# install from head
+
+brew "tree-sitter", args: ["HEAD"] # install this before neovim
+brew "neovim", args: ["HEAD"]
+brew "universal-ctags", args: ["HEAD"]
+
+# install from taps
+
+brew "fabianishere/personal/pam_reattach"
+brew "felixkratz/formulae/sketchybar"
+brew "kejadlen/tap/fennel-ls", args: ["HEAD"]
+
+cask "font-sauce-code-pro-nerd-font"
+cask "font-source-code-pro"
+cask "font-symbols-only-nerd-font"
+
+cask "1password"
+cask "1password-cli"
+cask "acorn"
+cask "aerospace"
+cask "affinity-designer@1"
+cask "alfred"
+cask "anki"
+cask "anytype"
+cask "arq"
+cask "autodesk-fusion"
+cask "calibre"
+cask "cleanshot"
+cask "daisydisk"
+cask "discord"
+cask "fantastical"
+cask "firefox@developer-edition"
+cask "ghostty"
+cask "hammerspoon"
+cask "keyboardcleantool"
+cask "kicad"
+cask "librewolf", args: %w[ no-quarantine ]
+cask "mailmate"
+cask "neovide"
+cask "obsidian"
+cask "orbstack"
+cask "prusaslicer"
+cask "qlmarkdown"
+cask "qlstephen"
+cask "quicklook-csv"
+cask "quicklook-json"
+cask "sf-symbols"
+cask "signal"
+cask "slack"
+cask "soulver"
+cask "tailscale"
+cask "telegram"
+cask "topnotch"
+cask "vlc"
+cask "yubico-yubikey-manager"
+cask "vmware-fusion"
+cask "zoom"
+cask "zotero"
+
+# TODO Don't assume shared Apple accounts
+# mas "1Password for Safari", id: 1569813296
+# mas "GoodNotes", id: 1444383602
+# mas "Little Snitch Mini", id: 1629008763
+# mas "PDF Viewer", id: 1120099014
+# mas "Shareful", id: 1522267256
+# mas "Xcode", id: 497799835
diff --git a/bork/_defaults.sh b/bork/_defaults.sh
new file mode 100644
index 00000000..2088dc20
--- /dev/null
+++ b/bork/_defaults.sh
@@ -0,0 +1,75 @@
+### defaults
+
+# TODO restart Dock, Finder, and SystemUIServer on changes
+
+### global defaults
+
+ok defaults -globalDomain NSDisableAutomaticTermination bool true # don't quit idle applications
+ok defaults -globalDomain AppleFontSmoothing int 0 # disable font smoothing
+ok defaults -globalDomain AppleKeyboardUIMode int 3 # full keyboard access
+ok defaults -globalDomain AppleShowAllExtensions bool true # show all extensions by default
+ok defaults -globalDomain KeyRepeat int 2 # keyboard repeat rate
+ok defaults -globalDomain InitialKeyRepeat int 25 # delay before keyboard repeat
+ok defaults -globalDomain NSTableViewDefaultSizeMode int 1 # set sidebar item size to small
+ok defaults -globalDomain NSQuitAlwaysKeepsWindows bool false # disable resume
+ok defaults -globalDomain WebKitDeveloperExtras bool true # add debug menu in web views
+ok defaults -globalDomain com.apple.mouse.tapBehavior bool true # tap to click
+ok defaults -globalDomain AppleShowScrollBars string WhenScrolling # only show scrollbars when scrolling
+ok defaults -globalDomain NSWindowShouldDragOnGesture bool true # move windows by holding ctrl+cmd and dragging any part of the window
+
+ok defaults com.apple.dashboard mcx-disabled bool true # disable dashboard
+ok defaults com.apple.desktopservices DSDontWriteNetworkStores bool true # don't write .DS_Store to network volumes
+
+ok defaults com.apple.dock autohide bool true # automatically hide and show the dock
+ok defaults com.apple.dock mineffect string scale # minimize windows using the scale effect
+ok defaults com.apple.dock mru-spaces bool false # don't rearrange spaces
+ok defaults com.apple.dock orientation string left
+ok defaults com.apple.dock wvous-bl-corner int 10 # the bottom left hot corner to sleep the display
+ok defaults com.apple.dock tilesize int 36 # set the icon size to 36 pixels
+ok defaults com.apple.dock autohide-delay float 0 # no dock delay
+ok defaults com.apple.dock showhidden bool true # show hidden apps
+
+# https://nikitabobko.github.io/AeroSpace/guide#a-note-on-displays-have-separate-spaces
+ok defaults com.apple.spaces spans-displays bool true disable
+
+ok defaults com.apple.driver.AppleBluetoothMultitouch.trackpad Clicking int 1
+ok defaults com.apple.driver.AppleBluetoothMultitouch.trackpad TrackpadFourFingerVertSwipeGesture int 0
+ok defaults com.apple.driver.AppleBluetoothMultitouch.trackpad TrackpadThreeFingerDrag bool true
+ok defaults com.apple.driver.AppleBluetoothMultitouch.trackpad TrackpadThreeFingerHorizSwipeGesture int 0
+ok defaults com.apple.driver.AppleBluetoothMultitouch.trackpad TrackpadThreeFingerVertSwipeGesture int 0
+
+# # Finder defaults
+ok defaults com.apple.finder FXEnableExtensionChangeWarning bool false # don't ask when changing file extension
+ok defaults com.apple.finder FXPreferredViewStyle string Nlsv # default to list view
+ok defaults com.apple.finder QLEnableTextSelection bool true # enable text selection in QuickLook
+ok defaults com.apple.finder _FXShowPosixPathInTitle bool true # show full path in Finder
+ok defaults com.apple.Finder NSToolbarTitleViewRolloverDelay float 0 # remove the proxy icon hover delay
+# https://twitter.com/chucker/status/1395843084383043584
+ok defaults com.apple.Finder NSWindowSupportsAutomaticInlineTitle bool false # show the proxy icon and older titlebar
+
+ok defaults com.apple.universalaccess showWindowTitlebarIcons bool true # show the proxy icon always
+
+ok defaults com.apple.Safari IncludeInternalDebugMenu bool true # enable Debug menu in Safari
+
+ok defaults com.apple.screencapture disable-shadow bool true # no window shadows when capturing windows
+ok defaults com.apple.screencapture location string "$HOME/Downloads"
+
+ok defaults com.apple.screensaver askForPassword int 1
+
+ok defaults com.apple.Terminal ShowLineMarks bool false
+
+ok defaults com.google.Chrome AppleEnableSwipeNavigateWithScrolls bool false
+
+# TODO com.freron.MailMate MmAllowedImageURLRegexp
+# ok defaults com.freron.MailMate MmAllowedImageURLRegexp string "https://({{ mailmate.allowed_image_regexps | join('|') }})"
+ # - (i|images|d)\.gr-assets\.com
+ # - www\.goodreads\.com
+ # - massdrop-s3\.imgix\.net
+ # - .*\.cloudfront\.net
+ # - s3\.amazonaws\.com
+ # - files\.convertkitcdn\.com/assets/pictures
+ # - wizardzines\.com
+
+ok defaults com.freron.MailMate MmSendMessageDelayEnabled bool true
+ok defaults com.freron.MailMate MmSendMessageDelay int 60
+
diff --git a/bork/bootstrap.sh b/bork/bootstrap.sh
new file mode 100644
index 00000000..406dabdb
--- /dev/null
+++ b/bork/bootstrap.sh
@@ -0,0 +1,61 @@
+# TODO use scutil to set the machine name
+
+ok directory ~/src
+
+# ok git ~/src/dotfiles https://git.kejadlen.dev/alpha/dotfiles.git
+
+register brew-bundle.sh
+
+# symlink dotfiles directly in $HOME
+declare -a dotfiles=(
+ ".config"
+ ".digrc"
+ ".gemrc"
+ ".gitconfig"
+ ".hammerspoon"
+ ".inputrc"
+ ".local"
+ ".p10k.zsh"
+ ".profile"
+ ".ruby-version"
+ ".tmux.conf"
+ ".zsh"
+ ".zshenv"
+ ".zshrc"
+
+ ".bundle/config"
+ ".cargo/config.toml"
+ ".docker/config.json"
+)
+for file in "${dotfiles[@]}"; do
+ ok directory "$(dirname $file)"
+ ok symlink "$HOME/$file" "$HOME/.dotfiles/$file"
+done
+
+### macos
+
+ok brew
+ok brew-bundle "$HOME/.dotfiles/bork/Brewfile"
+
+ok symlink "$HOME/Library/Application\ Support/LibreWolf/NativeMessagingHosts" "$HOME/Library/Application\ Support/Mozilla/NativeMessagingHosts"
+
+### jujutsu
+
+jj_config=$(jj config path --user)
+ok directory "$(dirname "$jj_config")"
+ok symlink "$jj_config" "$HOME/.config/jj/config.toml"
+
+include _defaults.sh
+
+# TODO set the default shell to /bin/sh
+# dscl . -read ~/ UserShell
+# https://tratt.net/laurie/blog/2024/faster_shell_startup_with_shell_switching.html
+# - name: Set default shell to sh
+# ansible.builtin.user:
+# name: alpha
+# shell: /bin/sh --login
+
+# TODO
+# luarocks install fennel
+
+# TODO tridactyl: https://librewolf.net/docs/faq/#how-can-i-get-tridactyls-native-messaging-to-work-when-i-install-librewolf-with-flatpak
diff --git a/bork/brew-bundle.sh b/bork/brew-bundle.sh
new file mode 100644
index 00000000..af74b7fd
--- /dev/null
+++ b/bork/brew-bundle.sh
@@ -0,0 +1,25 @@
+action=$1
+file=$2
+shift 2
+
+export HOMEBREW_BUNDLE_FILE=$file
+
+case $action in
+ status)
+ baking_platform_is "Darwin" || return $STATUS_UNSUPPORTED_PLATFORM
+ needs_exec "brew" || return $STATUS_FAILED_PRECONDITION
+ bake [ -e $file ] || return $STATUS_FAILED_PRECONDITION
+ HOMEBREW_NO_AUTO_UPDATE=true bake brew bundle check --no-upgrade > /dev/null || return $STATUS_MISSING
+ HOMEBREW_NO_AUTO_UPDATE=true bake brew bundle check > /dev/null || return $STATUS_OUTDATED
+ return $STATUS_OK ;;
+ install)
+ HOMEBREW_NO_AUTO_UPDATE=true bake brew bundle install
+ ;;
+ upgrade)
+ HOMEBREW_NO_AUTO_UPDATE=true bake brew bundle upgrade
+ ;;
+ remove)
+ return $STATUS_CONFLICT_HALT
+ ;;
+ *) return 1 ;;
+esac