Non-admin · old Intel (no Homebrew)
An Intel (x86_64) Mac with no admin rights, where Homebrew is impractical (it
compiles from source). Everything installs into your home directory — no brew, no
admin — via two scripts: perfect-bottles.sh (CLI tools as prebuilt binaries) and
manual-tools.sh (GUI apps + a few extras).
1. Terminal + font first (no admin)
Get iTerm2 and the FiraCode Nerd Font into your home directory, configure iTerm, and
switch to it — then install everything else from inside it. (No /tmp on this box, so
downloads go under ~/.cache.)
mkdir -p ~/Applications ~/Library/Fonts ~/.cache/dl
# iTerm2 -> ~/Applications (no admin)
curl -fsSL https://iterm2.com/downloads/stable/latest -o ~/.cache/dl/iterm.zip
ditto -x -k ~/.cache/dl/iterm.zip ~/Applications/
xattr -dr com.apple.quarantine ~/Applications/iTerm.app
# FiraCode Nerd Font -> ~/Library/Fonts
curl -fsSL https://github.com/ryanoasis/nerd-fonts/releases/latest/download/FiraCode.zip -o ~/.cache/dl/FiraCode.zip
unzip -qo ~/.cache/dl/FiraCode.zip -d ~/.cache/dl/fc && cp ~/.cache/dl/fc/*.ttf ~/Library/Fonts/
iTerm2 configuration
(Once iTerm2 is installed — by brew cask on the admin path, or into ~/Applications
by manual-tools.sh on the non-admin paths — configure it as below.)
Global options (non-admin friendly)


2. Install everything else
From your configured iTerm:
base=https://raw.githubusercontent.com/teeckoo/mac-setup/main
curl -fsSL "$base/perfect-bottles.sh" | bash # CLI tools (+ refreshes the font) -> $HOME
curl -fsSL "$base/manual-tools.sh" | bash # GUI apps + extras (+ refreshes iTerm2)
perfect-bottles.sh installs gkit, rg, gum, glow, pandoc, mdbook, jq, git-filter-repo, uv, starship, claude, node+npm/npx, sdk (SDKMAN), antidote,
fzf (and the FiraCode font). It auto-loads the shared lib-no-brew.sh (local copy if present, else
fetched), so the one-liner works with no clone. The scripts re-install iTerm2 + the
font from step 1 in place — harmless.
GUI apps (no admin password)
manual-tools.sh installs these into ~/Applications by copying the .app out of
the vendor .dmg/.zip and clearing the Gatekeeper quarantine flag — no sudo,
no admin prompt (unlike brew casks, which install to /Applications and ask for an
admin password):
| App | Source | Arch |
|---|---|---|
| iTerm2 | iterm2.com/downloads/stable/latest | universal |
| VS Code | update.code.visualstudio.com/latest/<darwin|darwin-arm64>/stable | per-arch |
| IntelliJ IDEA | download.jetbrains.com/product?code=IIU&latest&distribution=<mac|macM1> | per-arch |
| Google Chrome | dl.google.com/.../googlechrome.dmg | universal |
| Firefox | download.mozilla.org/?product=firefox-latest-ssl&os=osx | universal |
| Bruno | latest GitHub release, *_<x64_mac|arm64_mac>.dmg | per-arch |
| Slack | slack.com/ssb/download-osx | universal |
A failed download is logged and skipped — re-run to retry; existing apps are replaced in place. (Opera and Chromium aren’t automated — no clean signed download; grab them by hand if needed.)
On Intel,
manual-tools.shalso installscoursier(cs),direnv,batas prebuilt binaries,awsclivia the no-admin user pkg,R(the CRAN build — its.pkgis expanded andR.frameworkrelocated into~/.local, withR/Rscripton PATH; no admin), and container dev (colima+lima+ thedockerCLI —colima start --vm-type vz, no admin). It prints guidance fortinytex(its installer prompts for admin on Ventura),eza(no macOS binary — usels),wget(usecurl), andgnupg(no no-admin path — consider SSH commit signing).
3. Shell config (~/.zprofile + ~/.zshrc)
perfect-bottles.sh appends both blocks for you (marker-guarded; your own content is
left untouched). Shown here for reference / manual paste:
~/.zprofile:
# ---- no-brew base config (perfect-bottles.sh) ----
export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
export DIRENV_LOG_FORMAT=""
~/.zshrc:
. "$HOME/.cargo/env"
. "$HOME/.local/bin/env"
# ---- no-brew base config (perfect-bottles.sh) ----
# ---- System ----
ulimit -n 4096 # prevent "too many open files"
setopt AUTO_CD # cd by typing a folder name
# ---- Unified Directory Change Hooks ----
autoload -U add-zsh-hook
# Global path tracking marker (safely preserved)
typeset -g _LAST_LISTED_DIR=""
# 1. Your implicit local environment script processor
_chpwd_trigger_lenvrc() {
if [[ -f ".dir.session" ]]; then
source ".dir.session"
fi
}
add-zsh-hook chpwd _chpwd_trigger_lenvrc
# ---- Completions (must run before SDKMAN/plugins, which call compdef) ----
autoload -Uz compinit
compinit -i # -i: ignore "insecure" fpath dirs instead of prompting
# ---- Zsh plugins (antidote, cloned to ~/.antidote) ----
source "$HOME/.antidote/antidote.zsh"
antidote load < ~/.zsh_plugins.txt
# ---- Prompt (starship) ----
eval "$(starship init zsh)"
# ---- fzf (installed to ~/.fzf) ----
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh
# ---- direnv (installed by manual-tools.sh; guarded so this is a no-op if absent) ----
command -v direnv >/dev/null 2>&1 && eval "$(direnv hook zsh)"
#THIS MUST BE AT THE END OF THE FILE FOR SDKMAN TO WORK!!!
export SDKMAN_DIR="$HOME/.sdkman"
[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"
SDKMAN.
perfect-bottles.shinstalls SDKMAN into~/.sdkmanby running its official installer under zsh — SDKMAN’s bash-4 gate only fires under bash, and stock Intel macOS has only bash 3.2. SDKMAN’s installer appends its ownsdkman-init.shblock to the end of~/.zshrc(aftercompinit -i, where it must be) — don’t add one yourself. Just need a JDK?cs java --setup(coursier).
starship prompt
Create ~/.config/starship.toml:
format = """
${env_var.OPTIONAL_PROMPT_HEADER}\
$directory $git_branch$git_status$all${env_var.OPTIONAL_PROMPT_FOOTER}
$character"""
[line_break]
disabled = false
[directory]
truncation_length = 0
truncate_to_repo = false
[env_var.OPTIONAL_PROMPT_HEADER]
variable = "OPTIONAL_PROMPT_HEADER"
# The \n is placed inside the string format so it only triggers if text exists
format = "[$env_value]($style)\n"
style = "bold blue"
[env_var.OPTIONAL_PROMPT_FOOTER]
variable = "OPTIONAL_PROMPT_FOOTER"
format = "[$env_value]($style)\n" # Newline at the START
style = "bold yellow"
(starship reads this automatically; the ~/.zshrc block below runs starship init zsh.)
global direnv to add support for source_up_sdkmanrc
Create ~/.config/direnv/direnvrc:
# ==============================================================================
# 1. Fully Generic SDKMAN! Environment Loader (Local Folder Scope)
# ==============================================================================
layout_sdkman() {
: "${SDKMAN_DIR:=$HOME/.sdkman}"
if [[ -f ".sdkmanrc" && -f "$SDKMAN_DIR/bin/sdkman-init.sh" ]]; then
local zsh_safeguards='export ZSH_DISABLE_COMPFIX="true"; autoload -U compinit; compinit -u;'
direnv_load "$SHELL" -c "$zsh_safeguards source '$SDKMAN_DIR/bin/sdkman-init.sh' && sdk env >/dev/null 2>&1 && direnv dump"
fi
}
# ==============================================================================
# 2. Agnostic Ancestor Discovery Hook (Strict Explicit Execution)
# ==============================================================================
source_up_sdkmanrc() {
: "${SDKMAN_DIR:=$HOME/.sdkman}"
# Start hunting from the parent folder upward
local current_dir
current_dir=$(dirname "$PWD")
# Climb all the way to the filesystem root (/) until a file is found
while [[ "$current_dir" != "/" ]]; do
if [[ -f "$current_dir/.sdkmanrc" ]]; then
if [[ -f "$SDKMAN_DIR/bin/sdkman-init.sh" ]]; then
# Explicit Logging: Always print because this line is explicitly uncommented here
echo "direnv: loading $current_dir/.sdkmanrc" >&2
# Force evaluation inside that specific directory layer using an isolated subshell
local zsh_safeguards='export ZSH_DISABLE_COMPFIX="true"; autoload -U compinit; compinit -u;'
direnv_load "$SHELL" -c "cd '$current_dir' && $zsh_safeguards source '$SDKMAN_DIR/bin/sdkman-init.sh' && sdk env >/dev/null 2>&1 && direnv dump"
# Mark that the local context explicitly requested and applied an SDK toolchain
_LOCAL_SDKMAN_APPLIED="true"
fi
return 0
fi
current_dir=$(dirname "$current_dir")
done
}
# ==============================================================================
# 3. Safe Global Overwrite: Protect Child Contexts During Tree Traversal
# ==============================================================================
source_up() {
# 1. Take a clean snapshot of all environment variables BEFORE climbing the tree
local env_snapshot
env_snapshot=$(env | grep -v '^DIRENV_' | grep -v '^direnv_')
# 2. Find and execute the parent configuration natively without relying on 'source_up' function hooks
local parent_envrc
parent_envrc=$(find_up .envrc)
if [[ -n "$parent_envrc" && "$parent_envrc" != "$PWD/.envrc" ]]; then
source_env "$parent_envrc"
fi
# 3. THE USER PERSPECTIVE ISOLATION ENFORCEMENT:
# If 'source_up_sdkmanrc' is NOT explicitly uncommented and executed in this folder,
# we restore the toolchain variables back to your original baseline shell state.
if [[ "$_LOCAL_SDKMAN_APPLIED" != "true" && ! -f ".sdkmanrc" ]]; then
while read -r line; do
if [[ "$line" == *=* ]]; then
local key="${line%%=*}"
local val="${line#*=}"
# If a variable ends with _HOME or was modified by SDKMAN, revert it
if [[ "$key" == *_HOME ]] || [[ "$key" == sdkman_* ]]; then
export "$key"="$val"
fi
fi
done <<< "$env_snapshot"
# Also wipe out any new software candidates added to the path by parent folders
PATH_rm "$SDKMAN_DIR/candidates/*/*" 2>/dev/null
PATH_rm "$HOME/.sdkman/candidates/*/*" 2>/dev/null
fi
}
# ==============================================================================
# 4. Automated Local Tool Placement Trigger
# ==============================================================================
auto_load_local_tools() {
if [[ -f ".sdkmanrc" ]]; then
layout_sdkman
fi
}
auto_load_local_tools
Zsh plugins (antidote)
Create ~/.zsh_plugins.txt:
zsh-users/zsh-autosuggestions
zsh-users/zsh-syntax-highlighting
antidote loads this on every interactive shell (see the ~/.zshrc block). Add more
plugins here as you like.
fzf (fuzzy finder)
When the fzf installer prompts, answer:
| Prompt | Answer |
|---|---|
| fuzzy auto-completion | yes |
| key bindings | yes |
| update shell config files | no |
(“no” on the last one because the ~/.zshrc block already sources ~/.fzf.zsh.)
git log pager
Keep git log on one screen (git ships with the Xcode Command Line Tools):
git config --global --replace-all core.pager "less -F -X"
Troubleshooting
Gatekeeper “cannot be opened”. macOS may quarantine a downloaded binary on first run. Clear it:
xattr -dr com.apple.quarantine ~/.local/bin
compinit prompts every shell / command not found: compdef. A leftover Homebrew
site-functions dir owned by the old admin makes a bare compinit refuse to run. The
~/.zshrc block uses compinit -i (ignore insecure dirs) before the plugin/SDKMAN
lines — keep that order.
npm i -g permission denied. Global installs are redirected to ~/.local
(npm config set prefix ~/.local), so packages land in ~/.local/bin (already on
PATH). After a major Node LTS bump, packages with native bindings may need a
npm i -g reinstall to match the new ABI.
Re-running is safe (idempotent)
Both perfect-bottles.sh and manual-tools.sh re-run safely — to finish a partial
install or refresh to latest. CLI binaries and vendor installers overwrite in place
(no duplicates); Node wipes/reinstalls latest LTS (global packages survive); GUI apps
are replaced in place (a failed download leaves the old .app); antidote/fzf are
left alone if present; an unreachable source keeps its existing copy and is logged.
There’s no local cache, so a refresh re-downloads (~200 MB Node, ~2 GB GUI apps).








